From 74ec92e787a076c64a919e60c4f07584567e6dfc Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Wed, 30 Sep 2026 22:02:07 +0200 Subject: [PATCH 001/187] Add the menus' touch gesture unit --- .github/workflows/build.yml | 2 +- port/linux/src/touch_menu.c | 115 ++++++ port/linux/src/touch_menu.h | 120 ++++++ port/linux/tests/touch_menu_test.c | 576 +++++++++++++++++++++++++++++ tools/mutate_touch_menu.py | 286 ++++++++++++++ tools/test_touch_menu.py | 40 ++ 6 files changed, 1138 insertions(+), 1 deletion(-) create mode 100644 port/linux/src/touch_menu.c create mode 100644 port/linux/src/touch_menu.h create mode 100644 port/linux/tests/touch_menu_test.c create mode 100644 tools/mutate_touch_menu.py create mode 100644 tools/test_touch_menu.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4a51010..fc5cccf5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: run: python tools/ci_build.py linux release - name: Test - run: python -m pytest -q tools/test_linux_port.py + run: python -m pytest -q tools/test_linux_port.py tools/test_touch_menu.py - name: Compile cache run: ccache --show-stats diff --git a/port/linux/src/touch_menu.c b/port/linux/src/touch_menu.c new file mode 100644 index 00000000..97d24723 --- /dev/null +++ b/port/linux/src/touch_menu.c @@ -0,0 +1,115 @@ +/* +TOUCH_MENU.C + +The menus' pointer from a touchscreen (touch_menu.h). +*/ + +#include "touch_menu.h" + +#include +#include + +void touch_menu_init(struct touch_menu *menu, const struct touch_menu_settings *settings) +{ + memset(menu, 0, sizeof(*menu)); + menu->settings = *settings; +} + +void touch_menu_reset(struct touch_menu *menu) +{ + struct touch_menu_settings settings = menu->settings; + + touch_menu_init(menu, &settings); +} + +void touch_menu_down(struct touch_menu *menu, unsigned long long finger, float x, float y) +{ + if (menu->finger_down) + return; + menu->finger_down = 1; + menu->finger = finger; + menu->down_x = menu->last_x = x; + menu->down_y = menu->last_y = y; + menu->scrolling = 0; + menu->remainder = 0.0f; + /* a finger on the screen stops a list still scrolling, so that a tap + hits what the finger sees */ + menu->pending_steps = 0; +} + +void touch_menu_move(struct touch_menu *menu, unsigned long long finger, float x, float y) +{ + float along; + + if (!menu->finger_down || finger != menu->finger) + return; + if (!menu->scrolling) + { + float dx = x - menu->down_x, dy = y - menu->down_y; + + if (dx * dx + dy * dy <= menu->settings.slop * menu->settings.slop) + return; + /* the steps count from the slop's edge on the axis: the slop is not + a step, and a fast first sample is not lost */ + menu->scrolling = 1; + menu->axis = fabsf(dx) > fabsf(dy); + if (menu->axis) + menu->last_x = menu->down_x + (dx > 0.0f ? menu->settings.slop : -menu->settings.slop); + else + menu->last_y = menu->down_y + (dy > 0.0f ? menu->settings.slop : -menu->settings.slop); + } + along = menu->axis ? x - menu->last_x : y - menu->last_y; + menu->last_x = x; + menu->last_y = y; + menu->remainder += along; + while (menu->remainder >= menu->settings.step) + { + menu->pending_steps++; + menu->remainder -= menu->settings.step; + } + while (menu->remainder <= -menu->settings.step) + { + menu->pending_steps--; + menu->remainder += menu->settings.step; + } +} + +void touch_menu_up(struct touch_menu *menu, unsigned long long finger, float x, float y) +{ + if (!menu->finger_down || finger != menu->finger) + return; + touch_menu_move(menu, finger, x, y); + if (!menu->scrolling) + { + menu->output.moved = 1; + menu->output.x = menu->output.click_x = menu->down_x; + menu->output.y = menu->output.click_y = menu->down_y; + menu->output.clicks++; + } + menu->finger_down = 0; + menu->scrolling = 0; +} + +void touch_menu_cancel(struct touch_menu *menu) +{ + menu->finger_down = 0; + menu->scrolling = 0; + menu->remainder = 0.0f; + menu->pending_steps = 0; +} + +void touch_menu_read(struct touch_menu *menu, struct touch_menu_output *output) +{ + int limit = menu->settings.steps_per_read; + int steps = menu->pending_steps; + + if (steps > limit) + steps = limit; + if (steps < -limit) + steps = -limit; + *output = menu->output; + output->wheel_steps = steps; + menu->pending_steps -= steps; + menu->output.moved = 0; + menu->output.clicks = 0; +} diff --git a/port/linux/src/touch_menu.h b/port/linux/src/touch_menu.h new file mode 100644 index 00000000..4fb90e37 --- /dev/null +++ b/port/linux/src/touch_menu.h @@ -0,0 +1,120 @@ +/* +TOUCH_MENU.H + +The menus' pointer from a touchscreen: taps and drags of one finger +become what the desktop mouse gives the menus (port/linux/src/sdl_platform.c, +platform_ui_pointer_read). A finger that goes down and up without moving +further than the slop is a tap: the pointer goes where it went down, and +clicks there. A finger that moves further scrolls instead: each step of +its drag along the axis that dominated when it started is one notch of the +wheel, counted from the edge of the slop. No tap and no hover come from a +scroll, so the focus does not follow the finger. Other fingers are +ignored while one is down, and a new finger stops the steps still waiting. + +No SDL or game types, so that the build machine can test it +(port/linux/tests/touch_menu_test.c). +*/ + +#ifndef TOUCH_MENU_H +#define TOUCH_MENU_H + +struct touch_menu_settings +{ + /* pixels a tap may move */ + float slop; + /* pixels of drag for one wheel step */ + float step; + /* at most this many steps per read: the rest waits, so that a fast + swipe moves a list gradually, as fast as the menus take the steps */ + int steps_per_read; +}; + +struct touch_menu_output +{ + /* where the latest tap was, in pixels */ + float x, y; + float click_x, click_y; + /* a tap happened: the pointer is at x, y */ + int moved; + /* taps since the last read */ + int clicks; + /* positive: back (the finger moved down or right) */ + int wheel_steps; +}; + +struct touch_menu +{ + struct touch_menu_settings settings; + int finger_down; + unsigned long long finger; + float down_x, down_y; + float last_x, last_y; + int scrolling; + /* 0: vertical, 1: horizontal */ + int axis; + /* drag not yet a whole step */ + float remainder; + int pending_steps; + struct touch_menu_output output; +}; + +/** + * @brief Prepares a menu pointer; the settings are copied. + * @param menu the state to set up (all of it is cleared) + * @param settings the gesture sizes, in pixels, and the steps a read + */ +void touch_menu_init(struct touch_menu *menu, const struct touch_menu_settings *settings); + +/** + * @brief Forgets the finger, the pending steps and the unread taps but + * keeps the settings: what a finger began before a menu opened or closed + * must not act in the other one. + * @param menu the state to clear + */ +void touch_menu_reset(struct touch_menu *menu); + +/** + * @brief A finger went down. A second finger is ignored while one is + * down. The steps still waiting are dropped, so that a tap hits what the + * finger sees on a list still scrolling. + * @param menu the state + * @param finger the finger's id + * @param x,y where it went down, in pixels + */ +void touch_menu_down(struct touch_menu *menu, unsigned long long finger, float x, float y); + +/** + * @brief The finger moved. Once it is further than the slop it scrolls; + * the steps count from the slop's edge, so that the slop is not a step + * and a fast first sample is not lost. + * @param menu the state + * @param finger the finger's id; other fingers are ignored + * @param x,y where it is, in pixels + */ +void touch_menu_move(struct touch_menu *menu, unsigned long long finger, float x, float y); + +/** + * @brief The finger went up: a tap if it never scrolled, at the place + * where it went down. + * @param menu the state + * @param finger the finger's id; other fingers are ignored + * @param x,y where it went up, in pixels (the last part of a drag counts) + */ +void touch_menu_up(struct touch_menu *menu, unsigned long long finger, float x, float y); + +/** + * @brief The system took the touch away: the gesture neither taps nor + * scrolls. + * @param menu the state + */ +void touch_menu_cancel(struct touch_menu *menu); + +/** + * @brief Hands out what happened since the last read. At most + * steps_per_read wheel steps come out; the rest waits for the next reads. + * @param menu the state + * @param output receives the pointer, the taps and the wheel steps + */ +void touch_menu_read(struct touch_menu *menu, struct touch_menu_output *output); + +#endif diff --git a/port/linux/tests/touch_menu_test.c b/port/linux/tests/touch_menu_test.c new file mode 100644 index 00000000..04f160fa --- /dev/null +++ b/port/linux/tests/touch_menu_test.c @@ -0,0 +1,576 @@ +/* +TOUCH_MENU_TEST.C + +Tests of the menus' touch gestures (port/linux/src/touch_menu.c). Built and +run by tools/test_touch_menu.py; exits nonzero on a failure. +*/ + +#include "../src/touch_menu.h" + +#include + +static int failures; + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + failures++; \ + } \ + } while (0) + +/* 3 pixels a dp, as on the S23: slop 12 dp, step 40 dp; 2 steps a read, to +test the limit (the game uses 1) */ +static void setup(struct touch_menu *menu) +{ + struct touch_menu_settings settings = { 36.0f, 120.0f, 2 }; + + touch_menu_init(menu, &settings); +} + +/* first, so that a unit that lost its settings (a step of 0 never ends its +loops) fails here and not by hanging a later test */ +static void test_init_stores_the_settings(void) +{ + struct touch_menu menu; + + setup(&menu); + CHECK(menu.settings.slop == 36.0f); + CHECK(menu.settings.step == 120.0f); + CHECK(menu.settings.steps_per_read == 2); +} + +static void test_tap_clicks_where_the_finger_went_down(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 7, 500.0f, 300.0f); + touch_menu_move(&menu, 7, 510.0f, 305.0f); + touch_menu_up(&menu, 7, 512.0f, 306.0f); + touch_menu_read(&menu, &out); + CHECK(out.moved == 1); + CHECK(out.clicks == 1); + CHECK(out.x == 500.0f && out.y == 300.0f); + CHECK(out.click_x == 500.0f && out.click_y == 300.0f); + CHECK(out.wheel_steps == 0); +} + +static void test_read_clears_the_tap(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 10.0f, 10.0f); + touch_menu_up(&menu, 1, 10.0f, 10.0f); + touch_menu_read(&menu, &out); + touch_menu_read(&menu, &out); + CHECK(out.moved == 0); + CHECK(out.clicks == 0); +} + +static void test_nothing_until_the_finger_lifts(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 10.0f, 10.0f); + touch_menu_read(&menu, &out); + CHECK(out.moved == 0); + CHECK(out.clicks == 0); +} + +static void test_a_tap_may_move_the_whole_slop(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 100.0f, 100.0f); + touch_menu_up(&menu, 1, 136.0f, 100.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); +} + +static void test_two_taps_between_reads(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 10.0f, 10.0f); + touch_menu_up(&menu, 1, 10.0f, 10.0f); + touch_menu_down(&menu, 2, 70.0f, 80.0f); + touch_menu_up(&menu, 2, 70.0f, 80.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 2); + CHECK(out.click_x == 70.0f && out.click_y == 80.0f); +} + +static void test_drag_down_scrolls_back_without_clicking(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 100.0f); + /* past the slop (counted from 136), then 250 further: 2 steps */ + touch_menu_move(&menu, 1, 500.0f, 140.0f); + touch_menu_move(&menu, 1, 500.0f, 390.0f); + touch_menu_up(&menu, 1, 500.0f, 390.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 2); + CHECK(out.clicks == 0); + CHECK(out.moved == 0); +} + +static void test_drag_up_scrolls_forward(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 800.0f); + touch_menu_move(&menu, 1, 500.0f, 760.0f); + touch_menu_move(&menu, 1, 500.0f, 630.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -1); +} + +static void test_horizontal_drag_uses_the_x_axis(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 1500.0f, 500.0f); + /* mostly left: forward */ + touch_menu_move(&menu, 1, 1460.0f, 510.0f); + touch_menu_move(&menu, 1, 1330.0f, 540.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -1); +} + +static void test_equal_dx_dy_scrolls_vertically(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 500.0f); + touch_menu_move(&menu, 1, 530.0f, 530.0f); + touch_menu_move(&menu, 1, 530.0f, 680.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 1); +} + +static void test_the_axis_stays_after_the_start(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 500.0f); + touch_menu_move(&menu, 1, 500.0f, 540.0f); + touch_menu_move(&menu, 1, 900.0f, 540.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); +} + +static void test_a_fast_drag_counts_from_the_slop(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + int total = 0, index; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 0.0f); + /* one sample far past the slop, as a flick or a slow frame gives */ + touch_menu_move(&menu, 1, 500.0f, 600.0f); + touch_menu_up(&menu, 1, 500.0f, 600.0f); + for (index = 0; index < 5; index++) + { + touch_menu_read(&menu, &out); + total += out.wheel_steps; + } + /* (600 - 36) / 120 = 4.7 */ + CHECK(total == 4); + CHECK(out.clicks == 0); +} + +static void test_steps_come_out_gradually(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 0.0f); + touch_menu_move(&menu, 1, 500.0f, 40.0f); + /* five steps at once */ + touch_menu_move(&menu, 1, 500.0f, 640.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 2); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 2); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 1); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); +} + +static void test_forward_steps_come_out_gradually(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 1000.0f); + touch_menu_move(&menu, 1, 500.0f, 960.0f); + touch_menu_move(&menu, 1, 500.0f, 360.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -1); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); +} + +static void test_part_of_a_step_is_kept(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 0.0f); + touch_menu_move(&menu, 1, 500.0f, 40.0f); + touch_menu_move(&menu, 1, 500.0f, 100.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); + touch_menu_move(&menu, 1, 500.0f, 160.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 1); +} + +static void test_lift_far_away_is_no_tap(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 100.0f, 100.0f); + touch_menu_up(&menu, 1, 100.0f, 600.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + CHECK(out.moved == 0); + /* the lift's own move scrolls: (600 - 100 - 36) / 120 = 3.9 steps, 2 a read */ + CHECK(out.wheel_steps == 2); +} + +static void test_a_scroll_keeps_the_last_tap_point_without_moving(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 10.0f, 20.0f); + touch_menu_up(&menu, 1, 10.0f, 20.0f); + touch_menu_read(&menu, &out); + touch_menu_down(&menu, 2, 500.0f, 0.0f); + touch_menu_move(&menu, 2, 500.0f, 40.0f); + touch_menu_move(&menu, 2, 500.0f, 200.0f); + touch_menu_up(&menu, 2, 500.0f, 200.0f); + touch_menu_read(&menu, &out); + CHECK(out.moved == 0); + CHECK(out.x == 10.0f && out.y == 20.0f); +} + +static void test_a_new_finger_stops_pending_steps(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 0.0f); + touch_menu_move(&menu, 1, 500.0f, 40.0f); + touch_menu_move(&menu, 1, 500.0f, 640.0f); + touch_menu_up(&menu, 1, 500.0f, 640.0f); + /* the tap hits the list as the finger saw it */ + touch_menu_down(&menu, 2, 300.0f, 300.0f); + touch_menu_up(&menu, 2, 300.0f, 300.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.wheel_steps == 0); +} + +static void test_second_finger_is_ignored(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 100.0f, 100.0f); + touch_menu_down(&menu, 2, 900.0f, 900.0f); + touch_menu_move(&menu, 2, 900.0f, 100.0f); + touch_menu_up(&menu, 2, 900.0f, 100.0f); + touch_menu_up(&menu, 1, 100.0f, 100.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.x == 100.0f && out.y == 100.0f); + CHECK(out.wheel_steps == 0); +} + +static void test_lift_of_another_finger_is_ignored(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 100.0f, 100.0f); + touch_menu_up(&menu, 9, 100.0f, 100.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0 && out.moved == 0); + touch_menu_up(&menu, 1, 100.0f, 100.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); +} + +static void test_move_and_lift_without_down_do_nothing(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_move(&menu, 1, 100.0f, 900.0f); + touch_menu_up(&menu, 1, 100.0f, 900.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0 && out.moved == 0 && out.wheel_steps == 0); +} + +static void test_cancel_drops_the_gesture(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 0.0f); + touch_menu_move(&menu, 1, 500.0f, 40.0f); + touch_menu_move(&menu, 1, 500.0f, 400.0f); + touch_menu_cancel(&menu); + touch_menu_up(&menu, 1, 500.0f, 400.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); + CHECK(out.clicks == 0); + /* and the next finger works */ + touch_menu_down(&menu, 3, 20.0f, 20.0f); + touch_menu_up(&menu, 3, 20.0f, 20.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); +} + +static void test_reset_clears_everything(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 10.0f, 10.0f); + touch_menu_up(&menu, 1, 10.0f, 10.0f); + touch_menu_reset(&menu); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + CHECK(out.moved == 0); +} + +static void test_reset_during_a_touch_ignores_its_lift(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 100.0f, 100.0f); + touch_menu_reset(&menu); + touch_menu_move(&menu, 1, 100.0f, 500.0f); + touch_menu_up(&menu, 1, 100.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0 && out.wheel_steps == 0); +} + +/* a step is exactly the step setting: 120 pixels, not 119.9 or 120.1 */ +static void test_a_step_is_exactly_the_step_setting(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 0.0f); + touch_menu_move(&menu, 1, 500.0f, 40.0f); + touch_menu_move(&menu, 1, 500.0f, 156.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 1); + touch_menu_cancel(&menu); + touch_menu_down(&menu, 2, 500.0f, 1000.0f); + touch_menu_move(&menu, 2, 500.0f, 960.0f); + touch_menu_move(&menu, 2, 500.0f, 844.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -1); +} + +static void test_horizontal_drag_right_scrolls_back(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 100.0f, 500.0f); + /* no vertical movement at all: the distance is all in x */ + touch_menu_move(&menu, 1, 140.0f, 500.0f); + touch_menu_move(&menu, 1, 180.0f, 500.0f); + touch_menu_move(&menu, 1, 245.0f, 500.0f); + touch_menu_read(&menu, &out); + /* 245 - 136 = 109 of the 120 */ + CHECK(out.wheel_steps == 0); + touch_menu_move(&menu, 1, 257.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 1); + CHECK(out.clicks == 0); +} + +static void test_a_new_finger_starts_a_fresh_step(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 500.0f, 0.0f); + touch_menu_move(&menu, 1, 500.0f, 40.0f); + touch_menu_move(&menu, 1, 500.0f, 100.0f); + touch_menu_up(&menu, 1, 500.0f, 100.0f); + /* the same 64 pixels again: 128 together would be a step */ + touch_menu_down(&menu, 2, 500.0f, 0.0f); + touch_menu_move(&menu, 2, 500.0f, 40.0f); + touch_menu_move(&menu, 2, 500.0f, 100.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); +} + +static void test_a_lifted_finger_cannot_move_again(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 100.0f, 100.0f); + touch_menu_up(&menu, 1, 100.0f, 100.0f); + touch_menu_read(&menu, &out); + /* the same id, no finger down */ + touch_menu_move(&menu, 1, 100.0f, 900.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); +} + +static void test_reset_keeps_the_settings(void) +{ + struct touch_menu menu; + + setup(&menu); + touch_menu_reset(&menu); + CHECK(menu.settings.slop == 36.0f); + CHECK(menu.settings.step == 120.0f); + CHECK(menu.settings.steps_per_read == 2); +} + +static void test_cancel_keeps_a_finished_tap(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 30.0f, 40.0f); + touch_menu_up(&menu, 1, 30.0f, 40.0f); + touch_menu_cancel(&menu); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.moved == 1); + CHECK(out.click_x == 30.0f && out.click_y == 40.0f); +} + +/* the game's value: one step a read */ +static void test_one_step_a_read(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + struct touch_menu_settings settings = { 36.0f, 120.0f, 1 }; + int index; + + touch_menu_init(&menu, &settings); + touch_menu_down(&menu, 1, 500.0f, 0.0f); + touch_menu_move(&menu, 1, 500.0f, 40.0f); + touch_menu_move(&menu, 1, 500.0f, 640.0f); + for (index = 0; index < 5; index++) + { + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 1); + } + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); + touch_menu_cancel(&menu); + touch_menu_down(&menu, 2, 500.0f, 1000.0f); + touch_menu_move(&menu, 2, 500.0f, 960.0f); + touch_menu_move(&menu, 2, 500.0f, 360.0f); + for (index = 0; index < 5; index++) + { + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -1); + } + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); +} + +int main(void) +{ + test_init_stores_the_settings(); + if (failures) + return 1; + test_tap_clicks_where_the_finger_went_down(); + test_read_clears_the_tap(); + test_nothing_until_the_finger_lifts(); + test_a_tap_may_move_the_whole_slop(); + test_two_taps_between_reads(); + test_drag_down_scrolls_back_without_clicking(); + test_drag_up_scrolls_forward(); + test_horizontal_drag_uses_the_x_axis(); + test_equal_dx_dy_scrolls_vertically(); + test_the_axis_stays_after_the_start(); + test_a_fast_drag_counts_from_the_slop(); + test_steps_come_out_gradually(); + test_forward_steps_come_out_gradually(); + test_part_of_a_step_is_kept(); + test_lift_far_away_is_no_tap(); + test_a_scroll_keeps_the_last_tap_point_without_moving(); + test_a_new_finger_stops_pending_steps(); + test_second_finger_is_ignored(); + test_lift_of_another_finger_is_ignored(); + test_move_and_lift_without_down_do_nothing(); + test_cancel_drops_the_gesture(); + test_reset_clears_everything(); + test_reset_during_a_touch_ignores_its_lift(); + test_a_step_is_exactly_the_step_setting(); + test_horizontal_drag_right_scrolls_back(); + test_a_new_finger_starts_a_fresh_step(); + test_a_lifted_finger_cannot_move_again(); + test_reset_keeps_the_settings(); + test_cancel_keeps_a_finished_tap(); + test_one_step_a_read(); + if (failures) + printf("%d failures\n", failures); + else + printf("all tests passed\n"); + return failures != 0; +} diff --git a/tools/mutate_touch_menu.py b/tools/mutate_touch_menu.py new file mode 100644 index 00000000..8ede58c4 --- /dev/null +++ b/tools/mutate_touch_menu.py @@ -0,0 +1,286 @@ +"""Mutation test of the menus' touch gestures (port/linux/src/touch_menu.c). + +Each mutant is a copy of touch_menu.c with ONE textual change, built with +touch_menu_test.c exactly as tools/test_touch_menu.py builds it. A mutant is +KILLED when the build or the test run fails, SURVIVED when the tests pass. +The tracked source is never touched. Exits non-zero if a mutant survives. + + python3 tools/mutate_touch_menu.py # the mutants + python3 tools/mutate_touch_menu.py --equivalent # also prove the equivalents survive +""" +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SOURCE = ROOT / "port" / "linux" / "src" / "touch_menu.c" +HEADER = ROOT / "port" / "linux" / "src" / "touch_menu.h" +TEST = ROOT / "port" / "linux" / "tests" / "touch_menu_test.c" + +GUARD = "if (!menu->finger_down || finger != menu->finger)\n\t\treturn;\n" +SLOP_X = "(dx > 0.0f ? menu->settings.slop : -menu->settings.slop)" +SLOP_Y = "(dy > 0.0f ? menu->settings.slop : -menu->settings.slop)" +CLICK_X = "menu->output.x = menu->output.click_x = menu->down_x;" +CLICK_Y = "menu->output.y = menu->output.click_y = menu->down_y;" + +# (name, text found exactly once in touch_menu.c, replacement) +MUTANTS = [ + # init / reset + ("init: settings not stored", "menu->settings = *settings;", "(void)settings;"), + ("init: no memset", "memset(menu, 0, sizeof(*menu));", ""), + ("reset: settings lost", "struct touch_menu_settings settings = menu->settings;", + "struct touch_menu_settings settings = {0};"), + # down + ("down: second finger accepted", "if (menu->finger_down)\n\t\treturn;\n\tmenu->finger_down = 1;", + "if (0)\n\t\treturn;\n\tmenu->finger_down = 1;"), + ("down: finger_down not set", "menu->finger_down = 1;", ""), + ("down: finger id not stored", "menu->finger = finger;", "(void)finger;"), + ("down: remainder kept", "\tmenu->remainder = 0.0f;\n\t/* a finger", "\t/* a finger"), + ("down: pending steps kept", "menu->pending_steps = 0;\n}\n\nvoid touch_menu_move", + "}\n\nvoid touch_menu_move"), + # move guards + ("move: guard || -> &&", GUARD + "\tif (!menu->scrolling)", + GUARD.replace("||", "&&") + "\tif (!menu->scrolling)"), + ("move: finger id ignored", GUARD + "\tif (!menu->scrolling)", + "if (!menu->finger_down)\n\t\treturn;\n\t(void)finger;\n\tif (!menu->scrolling)"), + ("move: finger_down ignored", GUARD + "\tif (!menu->scrolling)", + "if (finger != menu->finger)\n\t\treturn;\n\tif (!menu->scrolling)"), + ("move: != -> ==", GUARD + "\tif (!menu->scrolling)", + GUARD.replace("!=", "==") + "\tif (!menu->scrolling)"), + ("move: slop test <= -> <", "dy * dy <= menu->settings.slop", "dy * dy < menu->settings.slop"), + ("move: slop test <= -> >", "dy * dy <= menu->settings.slop", "dy * dy > menu->settings.slop"), + ("move: slop not squared", "<= menu->settings.slop * menu->settings.slop", "<= menu->settings.slop"), + ("move: distance dx*dx - dy*dy", "dx * dx + dy * dy", "dx * dx - dy * dy"), + ("move: distance ignores dy", "dx * dx + dy * dy", "dx * dx"), + ("move: distance ignores dx", "dx * dx + dy * dy", "dy * dy"), + ("move: dx sign", "float dx = x - menu->down_x", "float dx = menu->down_x - x"), + ("move: dy sign", "dy = y - menu->down_y", "dy = menu->down_y - y"), + ("move: scrolling not set", "menu->scrolling = 1;\n\t\tmenu->axis", "menu->axis"), + # axis choice + ("axis: > -> >= (tie horizontal)", "fabsf(dx) > fabsf(dy)", "fabsf(dx) >= fabsf(dy)"), + ("axis: > -> <", "fabsf(dx) > fabsf(dy)", "fabsf(dx) < fabsf(dy)"), + ("axis: always vertical", "menu->axis = fabsf(dx) > fabsf(dy);", "menu->axis = 0;"), + ("axis: always horizontal", "menu->axis = fabsf(dx) > fabsf(dy);", "menu->axis = 1;"), + ("axis: no fabsf on dx", "fabsf(dx) > fabsf(dy)", "dx > fabsf(dy)"), + ("axis: no fabsf on dy", "fabsf(dx) > fabsf(dy)", "fabsf(dx) > dy"), + ("axis: edge branch inverted", "if (menu->axis)\n\t\t\tmenu->last_x", "if (!menu->axis)\n\t\t\tmenu->last_x"), + # slop-edge reset of the start point + ("edge: x not reset", "\t\t\tmenu->last_x = menu->down_x + " + SLOP_X + ";\n", + "\t\t\tmenu->last_x = menu->down_x;\n"), + ("edge: y not reset", "\t\t\tmenu->last_y = menu->down_y + " + SLOP_Y + ";\n", + "\t\t\tmenu->last_y = menu->down_y;\n"), + ("edge: x sign of slop swapped", SLOP_X, "(dx > 0.0f ? -menu->settings.slop : menu->settings.slop)"), + ("edge: y sign of slop swapped", SLOP_Y, "(dy > 0.0f ? -menu->settings.slop : menu->settings.slop)"), + ("edge: x + -> -", "menu->down_x + (dx", "menu->down_x - (dx"), + ("edge: y + -> -", "menu->down_y + (dy", "menu->down_y - (dy"), + ("edge: x test dx > -> <", "(dx > 0.0f ?", "(dx < 0.0f ?"), + ("edge: y test dy > -> <", "(dy > 0.0f ?", "(dy < 0.0f ?"), + ("edge: x from down_y", "menu->last_x = menu->down_x + (", "menu->last_x = menu->down_y + ("), + ("edge: x sign from dy", "menu->last_x = menu->down_x + (dx", "menu->last_x = menu->down_x + (dy"), + ("edge: y sign from dx", "menu->last_y = menu->down_y + (dy", "menu->last_y = menu->down_y + (dx"), + ("edge: half a slop", "(dx > 0.0f ? menu->settings.slop :", "(dx > 0.0f ? menu->settings.slop / 2.0f :"), + # along, remainder, steps + ("along: axis swapped", "along = menu->axis ? x - menu->last_x : y - menu->last_y;", + "along = menu->axis ? y - menu->last_y : x - menu->last_x;"), + ("along: x sign", "menu->axis ? x - menu->last_x", "menu->axis ? menu->last_x - x"), + ("along: y sign", ": y - menu->last_y;", ": menu->last_y - y;"), + ("along: x against down", "menu->axis ? x - menu->last_x", "menu->axis ? x - menu->down_x"), + ("along: y against down", ": y - menu->last_y;", ": y - menu->down_y;"), + ("last_x not updated", "menu->last_x = x;\n\tmenu->last_y = y;", "menu->last_y = y;"), + ("last_y not updated", "menu->last_x = x;\n\tmenu->last_y = y;", "menu->last_x = x;"), + ("remainder = instead of +=", "menu->remainder += along;", "menu->remainder = along;"), + ("remainder -= along", "menu->remainder += along;", "menu->remainder -= along;"), + ("remainder not added", "menu->remainder += along;", "(void)along;"), + ("back loop: >= -> >", "menu->remainder >= menu->settings.step", "menu->remainder > menu->settings.step"), + ("back loop: >= -> <=", "menu->remainder >= menu->settings.step", "menu->remainder <= menu->settings.step"), + ("back loop: step doubled", "menu->remainder >= menu->settings.step", "menu->remainder >= menu->settings.step * 2.0f"), + ("back loop: ++ -> --", "menu->pending_steps++;", "menu->pending_steps--;"), + ("back loop: -= -> +=", "menu->remainder -= menu->settings.step;", "menu->remainder += menu->settings.step;"), + ("back loop: subtraction dropped", + "\t\tmenu->pending_steps++;\n\t\tmenu->remainder -= menu->settings.step;\n\t}\n\twhile", + "\t\tmenu->pending_steps++;\n\t\tmenu->remainder = 0.0f;\n\t}\n\twhile"), + ("back loop: while -> if", "while (menu->remainder >= menu->settings.step)", "if (menu->remainder >= menu->settings.step)"), + ("forward loop: <= -> <", "menu->remainder <= -menu->settings.step", "menu->remainder < -menu->settings.step"), + ("forward loop: <= -> >=", "menu->remainder <= -menu->settings.step", "menu->remainder >= -menu->settings.step"), + ("forward loop: no minus", "menu->remainder <= -menu->settings.step", "menu->remainder <= menu->settings.step"), + ("forward loop: -- -> ++", "menu->pending_steps--;", "menu->pending_steps++;"), + ("forward loop: += -> -=", "menu->remainder += menu->settings.step;\n\t}\n}", "menu->remainder -= menu->settings.step;\n\t}\n}"), + ("forward loop: subtraction dropped", + "\t\tmenu->pending_steps--;\n\t\tmenu->remainder += menu->settings.step;", + "\t\tmenu->pending_steps--;\n\t\tmenu->remainder = 0.0f;"), + ("forward loop: while -> if", "while (menu->remainder <= -menu->settings.step)", "if (menu->remainder <= -menu->settings.step)"), + # up + ("up: guard || -> &&", GUARD + "\ttouch_menu_move", GUARD.replace("||", "&&") + "\ttouch_menu_move"), + ("up: finger id ignored", GUARD + "\ttouch_menu_move", "if (!menu->finger_down)\n\t\treturn;\n\ttouch_menu_move"), + ("up: finger_down ignored", GUARD + "\ttouch_menu_move", "if (finger != menu->finger)\n\t\treturn;\n\ttouch_menu_move"), + ("up: != -> ==", GUARD + "\ttouch_menu_move", GUARD.replace("!=", "==") + "\ttouch_menu_move"), + ("up: final move dropped", "\ttouch_menu_move(menu, finger, x, y);\n\tif (!menu->scrolling)", "\t(void)x;\n\t(void)y;\n\tif (!menu->scrolling)"), + ("up: tap test inverted", "\tif (!menu->scrolling)\n\t{\n\t\tmenu->output.moved", "\tif (menu->scrolling)\n\t{\n\t\tmenu->output.moved"), + ("up: tap always", "\tif (!menu->scrolling)\n\t{\n\t\tmenu->output.moved", "\tif (1)\n\t{\n\t\tmenu->output.moved"), + ("up: moved not set", "menu->output.moved = 1;", ""), + ("up: moved = 0", "menu->output.moved = 1;", "menu->output.moved = 0;"), + ("up: x from the lift", CLICK_X, CLICK_X.replace("menu->down_x", "x")), + ("up: y from the lift", CLICK_Y, CLICK_Y.replace("menu->down_y", "y")), + ("up: x from down_y", CLICK_X, CLICK_X.replace("down_x", "down_y")), + ("up: y from down_x", CLICK_Y, CLICK_Y.replace("down_y", "down_x")), + ("up: pointer x not set", CLICK_X, "menu->output.click_x = menu->down_x;"), + ("up: click x not set", CLICK_X, "menu->output.x = menu->down_x;"), + ("up: pointer y not set", CLICK_Y, "menu->output.click_y = menu->down_y;"), + ("up: click y not set", CLICK_Y, "menu->output.y = menu->down_y;"), + ("up: clicks++ -> = 1", "menu->output.clicks++;", "menu->output.clicks = 1;"), + ("up: clicks not counted", "menu->output.clicks++;", ""), + ("up: clicks++ -> --", "menu->output.clicks++;", "menu->output.clicks--;"), + ("up: finger stays down", "\tmenu->finger_down = 0;\n\tmenu->scrolling = 0;\n}\n\nvoid touch_menu_cancel", + "\tmenu->scrolling = 0;\n}\n\nvoid touch_menu_cancel"), + # cancel + ("cancel: finger stays down", "void touch_menu_cancel(struct touch_menu *menu)\n{\n\tmenu->finger_down = 0;\n", + "void touch_menu_cancel(struct touch_menu *menu)\n{\n"), + ("cancel: pending steps kept", "\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n}", + "\tmenu->remainder = 0.0f;\n}"), + ("cancel: unread tap dropped too", "menu->pending_steps = 0;\n}\n\nvoid touch_menu_read", + "menu->pending_steps = 0;\n\tmenu->output.clicks = 0;\n\tmenu->output.moved = 0;\n}\n\nvoid touch_menu_read"), + # read + ("read: cap > -> <", "if (steps > limit)", "if (steps < limit)"), + ("read: upper cap dropped", "\tif (steps > limit)\n\t\tsteps = limit;\n", ""), + ("read: upper cap wrong value", "\t\tsteps = limit;", "\t\tsteps = limit + 1;"), + ("read: upper cap sign", "\t\tsteps = limit;", "\t\tsteps = -limit;"), + ("read: lower cap < -> >", "if (steps < -limit)", "if (steps > -limit)"), + ("read: lower cap dropped", "\tif (steps < -limit)\n\t\tsteps = -limit;\n", ""), + ("read: lower cap wrong value", "\t\tsteps = -limit;", "\t\tsteps = -limit - 1;"), + ("read: lower cap sign", "\t\tsteps = -limit;", "\t\tsteps = limit;"), + ("read: limit is 1", "int limit = menu->settings.steps_per_read;", "int limit = 1;"), + ("read: limit + 1", "int limit = menu->settings.steps_per_read;", "int limit = menu->settings.steps_per_read + 1;"), + ("read: limit - 1", "int limit = menu->settings.steps_per_read;", "int limit = menu->settings.steps_per_read - 1;"), + ("read: steps from nothing", "int steps = menu->pending_steps;", "int steps = 0;"), + ("read: output not copied", "\t*output = menu->output;\n", ""), + ("read: wheel steps not set", "output->wheel_steps = steps;", ""), + ("read: wheel steps negated", "output->wheel_steps = steps;", "output->wheel_steps = -steps;"), + ("read: pending not consumed", "menu->pending_steps -= steps;", ""), + ("read: pending -= -> +=", "menu->pending_steps -= steps;", "menu->pending_steps += steps;"), + ("read: pending cleared", "menu->pending_steps -= steps;", "menu->pending_steps = 0;"), + ("read: moved kept", "menu->output.moved = 0;\n\tmenu->output.clicks = 0;", "menu->output.clicks = 0;"), + ("read: clicks kept", "menu->output.moved = 0;\n\tmenu->output.clicks = 0;", "menu->output.moved = 0;"), +] + +# Mutants that cannot change behaviour, and why. Run only with --equivalent, +# to prove that each one really survives. +EQUIVALENT = [ + ("down: last_x/last_y not initialised", + "menu->down_x = menu->last_x = x;\n\tmenu->down_y = menu->last_y = y;", + "menu->down_x = x;\n\tmenu->down_y = y;", + "the first move past the slop rewrites last on the scroll axis before it is read, " + "and the other axis's last is never read; a move inside the slop reads neither"), + ("down: scrolling not cleared", + "menu->scrolling = 0;\n\tmenu->remainder = 0.0f;\n\t/* a finger", + "menu->remainder = 0.0f;\n\t/* a finger", + "scrolling is already 0 whenever finger_down is 0: init, up and cancel clear it, and down " + "runs only when finger_down is 0"), + ("up: scrolling not cleared", + "\tmenu->finger_down = 0;\n\tmenu->scrolling = 0;\n}\n\nvoid touch_menu_cancel", + "\tmenu->finger_down = 0;\n}\n\nvoid touch_menu_cancel", + "scrolling is read only while finger_down is set, and down clears it before the next finger"), + ("cancel: scrolling not cleared", + "menu->finger_down = 0;\n\tmenu->scrolling = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;", + "menu->finger_down = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;", + "same as up: scrolling is read only while finger_down is set, and down clears it"), + ("cancel: remainder not cleared", + "menu->scrolling = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n}", + "menu->scrolling = 0;\n\tmenu->pending_steps = 0;\n}", + "remainder is read only by move, which needs finger_down, and down zeroes it"), + ("edge: dx >= 0 for the sign of the x edge", "(dx > 0.0f ?", "(dx >= 0.0f ?", + "the x edge is computed only when |dx| > |dy| >= 0, so dx is never 0 there"), + ("edge: dy >= 0 for the sign of the y edge", "(dy > 0.0f ?", "(dy >= 0.0f ?", + "on the vertical axis |dy| >= |dx| and dx*dx+dy*dy > slop*slop > 0, so dy is never 0"), + ("move: dx measured from last_x", "float dx = x - menu->down_x", "float dx = x - menu->last_x", + "dx is computed only before scrolling starts, and until then last_x still equals down_x"), + ("move: dy measured from last_y", "dy = y - menu->down_y", "dy = y - menu->last_y", + "same as dx: last_y equals down_y until scrolling starts"), + ("up: tap x taken from last_x", "menu->output.x = menu->output.click_x = menu->down_x;", + "menu->output.x = menu->output.click_x = menu->last_x;", + "a tap is a gesture that never scrolled, so last_x was never moved off down_x"), + ("up: tap y taken from last_y", "menu->output.y = menu->output.click_y = menu->down_y;", + "menu->output.y = menu->output.click_y = menu->last_y;", + "a tap is a gesture that never scrolled, so last_y was never moved off down_y"), + ("read: upper cap >= (assigns limit to limit)", "if (steps > limit)", "if (steps >= limit)", + "at steps == limit the assignment changes nothing"), + ("read: lower cap <= (assigns -limit to -limit)", "if (steps < -limit)", "if (steps <= -limit)", + "at steps == -limit the assignment changes nothing"), +] + + +def compiler(): + """Find the build machine's C compiler. + + Returns the path of clang, cc or gcc, whichever exists first; exits if none. + """ + for name in ("clang", "cc", "gcc"): + path = shutil.which(name) + if path: + return path + sys.exit("no C compiler") + + +def run_mutant(cc, work, text): + """Build touch_menu_test.c against one mutant and run it. + + cc: the compiler path. work: a temporary directory that holds the mutant + and a copy of touch_menu.h. text: the mutant's source. + Returns ("KILLED", reason) when the tests fail or hang, ("SURVIVED", "") + when they pass. A build failure exits the script: it is a broken mutation, + not a kill. + """ + source = work / "touch_menu.c" + source.write_text(text, newline="\n") + binary = work / "touch_menu_test" + build = subprocess.run( + [cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-O1", str(TEST), str(source), + "-lm", "-o", str(binary)], + capture_output=True, text=True) + if build.returncode != 0: + # a mutant that does not compile proves nothing about the tests + sys.exit(f"script error: the mutant does not build:\n{build.stderr}") + try: + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10) + except subprocess.TimeoutExpired: + return "KILLED", "timeout" + if result.returncode == 0: + return "SURVIVED", "" + return "KILLED", "test: " + (result.stdout.splitlines() or ["?"])[0] + + +def main(): + """Run every mutant, and with --equivalent the equivalent ones too. + + Returns the exit code: 1 if a mutant survived or an equivalent one was + killed, else 0. + """ + cc = compiler() + original = SOURCE.read_text().replace("\r\n", "\n") + failed = False + with tempfile.TemporaryDirectory() as tmp: + work = Path(tmp) + # the mutant includes "touch_menu.h" from its own directory + shutil.copy(HEADER, work / "touch_menu.h") + killed = 0 + for name, old, new in MUTANTS: + if original.count(old) != 1: + sys.exit(f"mutation '{name}': {original.count(old)} matches, need exactly 1") + verdict, how = run_mutant(cc, work, original.replace(old, new)) + print(f"{verdict:8} {name} {how}".rstrip()) + killed += verdict == "KILLED" + print(f"killed {killed}/{len(MUTANTS)}") + failed = killed != len(MUTANTS) + if "--equivalent" in sys.argv: + for name, old, new, why in EQUIVALENT: + if original.count(old) != 1: + sys.exit(f"equivalent '{name}': {original.count(old)} matches, need exactly 1") + verdict, _ = run_mutant(cc, work, original.replace(old, new)) + print(f"equivalent {'ok' if verdict == 'SURVIVED' else 'KILLED'}: {name} ({why})") + failed = failed or verdict != "SURVIVED" + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/test_touch_menu.py b/tools/test_touch_menu.py new file mode 100644 index 00000000..5f220ba4 --- /dev/null +++ b/tools/test_touch_menu.py @@ -0,0 +1,40 @@ +"""Runs the C tests of the menus' touch gestures (port/linux/src/touch_menu.c) +with the build machine's C compiler. The unit has no SDL or game +dependency, so it compiles anywhere.""" +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent + + +def compiler(): + """Finds a C compiler on the build machine. + + Returns the path of clang, cc or gcc (the first found), or None. + """ + for name in ("clang", "cc", "gcc"): + path = shutil.which(name) + if path: + return path + return None + + +def test_touch_menu(tmp_path): + """Builds the gesture unit with its tests, warnings as errors, and runs them.""" + cc = compiler() + if not cc: + pytest.skip("no C compiler") + binary = tmp_path / "touch_menu_test" + build = subprocess.run( + [cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-O1", + str(ROOT / "port" / "linux" / "tests" / "touch_menu_test.c"), + str(ROOT / "port" / "linux" / "src" / "touch_menu.c"), + "-lm", "-o", str(binary)], + capture_output=True, text=True, + ) + assert build.returncode == 0, build.stdout + build.stderr + result = subprocess.run([str(binary)], capture_output=True, text=True) + assert result.returncode == 0, result.stdout + result.stderr From 3373a3556d6da4302e698efc55103e72964949e2 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Wed, 30 Sep 2026 22:02:13 +0200 Subject: [PATCH 002/187] Operate the Android menus by touch --- port/android/README.md | 15 +- port/android/host/host_main.c | 20 ++ port/linux/README.md | 2 + port/linux/include/halo_ui_pointer.h | 23 +- port/linux/src/d3d8_gl.c | 21 +- port/linux/src/sdl_platform.c | 46 ++++ port/linux/src/sdl_platform.h | 32 ++- port/linux/src/touch_input.c | 119 ++++++++++ port/linux/src/touch_input.h | 42 ++++ source/interface/ui_widget.c | 335 +++++++++++++++++++++++++-- source/interface/virtual_keyboard.c | 64 +++++ source/interface/virtual_keyboard.h | 18 ++ 12 files changed, 694 insertions(+), 43 deletions(-) create mode 100644 port/linux/src/touch_input.c create mode 100644 port/linux/src/touch_input.h diff --git a/port/android/README.md b/port/android/README.md index 204645ca..3886a145 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -92,8 +92,17 @@ with the positions on the Xbox controller: | Create | back | | The controller gets the rumble. The back gesture of Android is the B -button. A Bluetooth or USB keyboard operates as on Linux. The screen does -not accept touch input. +button. A Bluetooth or USB keyboard operates as on Linux. + +The touchscreen operates the menus: tap an item to select it (on a +setting with values, tap its left or right half), tap a button of the key +at the bottom of a screen (for example "B = Back") to push it, and drag to +scroll a list (down or right steps back, up or left steps forward). A drag +stops at the first and the last item, and it does not change a setting's +value. The gameplay does not accept touch input yet. + +On the on-screen keyboard, tap a key to press it, "B =BACK" to cancel and +"A =ENTER" to accept the name. ## Settings @@ -310,6 +319,6 @@ assembly of the port is necessary: - The device must let the app reserve the fixed guest addresses, from `0x80000000` to approximately `0x89000000`. If the addresses are not available, the app shows a message. -- The game does not accept touch input. Use a controller or a keyboard. +- Touch operates only the menus. In the game, use a controller or a keyboard. - Kernels with 16 KB pages (a developer option of Android 15) do not operate. The Xbox memory uses 4 KB pages. diff --git a/port/android/host/host_main.c b/port/android/host/host_main.c index ba5d9e6a..d4a778de 100644 --- a/port/android/host/host_main.c +++ b/port/android/host/host_main.c @@ -233,6 +233,13 @@ static uint32_t make_boot(const struct environment *environment) #define MAIN_STACK_SIZE (16 * 1024 * 1024) +/** + * @brief The thread that runs the game: passes the display and its density, + * the time zone and the data and save folders to the guest through its + * environment, and runs the guest's main. + * @param unused the thread argument, not used + * @return never returns normally + */ static void *game_main(void *unused) { struct environment environment = { { 0 }, 0 }; @@ -278,6 +285,19 @@ static void *game_main(void *unused) environment_set(&environment, "HALO_DISPLAY_WIDTH", width); host_logf(HOST_LOG_INFO, "display %dx%d: rendering %sx480", mode->w, mode->h, width); } + { + /* dp for the touch controls (port/linux/src/touch_input.c): SDL + gives Android's densityDpi / 160 */ + float density = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay()); + char text[16]; + + if (density > 0.0f) + { + snprintf(text, sizeof(text), "%g", density); + environment_set(&environment, "HALO_DISPLAY_DENSITY", text); + host_logf(HOST_LOG_INFO, "display density %s", text); + } + } } time_zone(zone, sizeof(zone)); environment_set(&environment, "TZ", zone); diff --git a/port/linux/README.md b/port/linux/README.md index fb1b6341..76fd3936 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -125,6 +125,8 @@ In the menus, the mouse moves a pointer: - A left click selects the item. On a setting with values, a click on the left or right half changes the value. On a button in the key of a screen (for example "B = Back"), a click pushes that button. +- On the on-screen keyboard (a profile's name), a left click presses the + key below the pointer, or pushes the "B =BACK" or "A =ENTER" legend. - A right click goes back. - The mouse wheel moves through the items. diff --git a/port/linux/include/halo_ui_pointer.h b/port/linux/include/halo_ui_pointer.h index bc7035a1..d0650d34 100644 --- a/port/linux/include/halo_ui_pointer.h +++ b/port/linux/include/halo_ui_pointer.h @@ -1,9 +1,11 @@ /* HALO_UI_POINTER.H -The mouse in the menus of the desktop builds. While a menu is up, the mouse -is released and its pointer shows (port/linux/src/sdl_platform.c); each frame -the menus (source/interface/ui_widget.c) ask where it is, in their own 640x480 +The mouse in the menus of the desktop builds, and the touchscreen in those +of the Android build. While a menu is up, the mouse is released and its +pointer shows (port/linux/src/sdl_platform.c), or taps and drags go to the +menus (port/linux/src/touch_input.c); each frame the menus +(source/interface/ui_widget.c) ask where the pointer is, in their own 640x480 coordinates (port/linux/src/d3d8_gl.c undoes the letterbox, the scale and the widescreen centering), and what it did since. */ @@ -21,11 +23,20 @@ struct halo_ui_pointer unsigned char right_clicks; /* whole wheel notches, away from the user positive */ signed char wheel_steps; + /* nonzero when the pointer is the touchscreen, not a mouse: a drag and a + wheel notch are the same steps, but the menus treat them differently */ + unsigned char touch; }; -/* frees the mouse for the menus while menus_active, and captures it again -for aiming when not; while menus are active returns nonzero and what the -pointer did since the last call. Always 0 on Android. */ +/** + * @brief Gives the menus the pointer. Frees the mouse for the menus while + * menus_active and captures it again for aiming when not (the + * touchscreen: taps and drags go to the menus while menus_active). + * @param menus_active nonzero while a menu is up + * @param pointer receives what the pointer did since the last call, in the + * menus' 640x480 coordinates + * @return nonzero while menus are active and the pointer could be read + */ int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer); #endif diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 5b16fa47..976d41e5 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -1136,17 +1136,14 @@ HRESULT WINAPI Direct3D_CreateDevice(UINT adapter, D3DDEVTYPE device_type, void /* ---------- the menus' pointer */ -#ifdef HALO_ANDROID -int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer) -{ - (void)menus_active; - (void)pointer; - return 0; -} -#else -/* a point in the window, as SDL reports it, in the menus' coordinates: the -inverse of the letterboxed display blit at presentation, the screen's -width and the menus' centering (halo_screen_ui_offset) */ +/** + * @brief Turns a point in the window, as SDL reports it, into the menus' + * coordinates: the inverse of the letterboxed display blit at presentation, + * the screen's width and the menus' centering (halo_screen_ui_offset). + * @param window_x,window_y the point, in the window's units + * @param x,y receive the point in 640x480 menu units; -1 if there is no + * back buffer or window yet + */ static void ui_point_from_window(float window_x, float window_y, short *x, short *y) { struct render_target_entry *back_buffer = render_target_get(&device.back_buffer); @@ -1189,9 +1186,9 @@ int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer) pointer->left_clicks = (unsigned char)(state.left_clicks < 255 ? state.left_clicks : 255); pointer->right_clicks = (unsigned char)(state.right_clicks < 255 ? state.right_clicks : 255); pointer->wheel_steps = (signed char)(state.wheel_steps < -8 ? -8 : state.wheel_steps > 8 ? 8 : state.wheel_steps); + pointer->touch = state.touch != FALSE; return 1; } -#endif /* takes up the display's shape and resolution, or the window's, if they have changed; between frames, since the game's layout and the targets must diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index a03c4450..7d591e67 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -15,6 +15,7 @@ and the debug keyboard that the game's console reads. #include "port_config.h" #include "p2p.h" #include "xiso.h" +#include "touch_input.h" #include #include @@ -824,6 +825,9 @@ void platform_pump_events(void) memset(input_state.keys, 0, sizeof(input_state.keys)); memset(input_state.mouse_buttons, 0, sizeof(input_state.mouse_buttons)); input_state.focused = FALSE; +#ifdef HALO_ANDROID + touch_input_cancel(); +#endif break; case SDL_EVENT_WINDOW_FOCUS_GAINED: input_state.focused = TRUE; @@ -833,6 +837,14 @@ void platform_pump_events(void) platform_mouse_capture(TRUE); #endif break; +#ifdef HALO_ANDROID + case SDL_EVENT_FINGER_DOWN: + case SDL_EVENT_FINGER_MOTION: + case SDL_EVENT_FINGER_UP: + case SDL_EVENT_FINGER_CANCELED: + touch_input_event(event.type, &event.tfinger); + break; +#endif case SDL_EVENT_GAMEPAD_ADDED: SDL_OpenGamepad(event.gdevice.which); break; @@ -899,6 +911,40 @@ void platform_video_window_size(int *width, int *height) SDL_GetWindowSize(platform_window, width, height); } +#else +/* ---------- the menus' pointer (the touchscreen) + +While a menu is up, taps and drags go to the menus (touch_input.c, +halo_ui_pointer_update in d3d8_gl.c). */ +void platform_ui_pointer_set_active(BOOL active) +{ + pthread_mutex_lock(&input_lock); + if ((active != FALSE) != (input_state.ui_pointer != FALSE)) + { + input_state.ui_pointer = active; + touch_input_menu_set_active(active != FALSE); + } + pthread_mutex_unlock(&input_lock); +} + +BOOL platform_ui_pointer_read(struct platform_ui_pointer *pointer) +{ + BOOL active; + + pthread_mutex_lock(&input_lock); + active = input_state.ui_pointer; + touch_input_menu_read(pointer); + pthread_mutex_unlock(&input_lock); + return active; +} + +/* the window is its pixels on Android (no display scaling): touch_input.c +scales the fingers' 0..1 by the drawable's size */ +void platform_video_window_size(int *width, int *height) +{ + platform_video_drawable_size(width, height); +} + #endif void platform_input_read(struct platform_input_state *state, BOOL consume_motion) { diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index fe7e84ab..2bacee99 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -45,25 +45,49 @@ display.interpolation is false (port/linux/game/render_interpolation.c) */ int halo_interpolation_enabled(void); void platform_mouse_capture(BOOL capture); -/* main thread only; a no-op elsewhere */ +/** + * @brief Handles the window's and the input devices' events. Main thread + * only, a no-op elsewhere. On Android this is where the finger events reach + * touch_input.c, which is why that module's state is only touched from the + * main thread (see touch_input.h). + */ void platform_pump_events(void); /* a snapshot of the input state; consume_motion resets the mouse deltas */ void platform_input_read(struct platform_input_state *state, BOOL consume_motion); -#ifndef HALO_ANDROID /* the pointer in the menus (d3d8_gl.c, halo_ui_pointer_update) */ struct platform_ui_pointer { - /* in window coordinates, as SDL reports them */ + /* in window coordinates, as SDL reports them (on Android the window's + pixels: touch_input.c) */ float x, y; float click_x, click_y; BOOL moved; int left_clicks, right_clicks; int wheel_steps; + /* the pointer is the touchscreen (touch_input.c), not a mouse */ + BOOL touch; }; +/** + * @brief Says whether a menu is up. The mouse is released for the menus + * (desktop), or the touchscreen goes to them (Android). A change of mode + * drops the gesture in progress. + * @param active nonzero while a menu is up + */ void platform_ui_pointer_set_active(BOOL active); + +/** + * @brief What the pointer did since the last call. + * @param pointer receives the position, the clicks and the wheel steps + * @return nonzero while a menu is up (platform_ui_pointer_set_active) + */ BOOL platform_ui_pointer_read(struct platform_ui_pointer *pointer); + +/** + * @brief The window's size in the units that pointer positions come in, + * which differ from the drawable's pixels on displays that scale. + * @param width,height receive the size + */ void platform_video_window_size(int *width, int *height); -#endif BOOL platform_next_keystroke(struct platform_keystroke *keystroke); #endif diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c new file mode 100644 index 00000000..b7450767 --- /dev/null +++ b/port/linux/src/touch_input.c @@ -0,0 +1,119 @@ +/* +TOUCH_INPUT.C + +The touchscreen of the Android build. SDL's finger events become the +menus' pointer (touch_menu.c) while a menu is up, as the desktop mouse +does (sdl_platform.c, platform_ui_pointer_read). Sizes are in dp, from the +display density the app passes (HALO_DISPLAY_DENSITY, host_main.c). +*/ + +#include "touch_input.h" +#include "touch_menu.h" + +#include + +/* a tap moves at most this far; a drag of this length is one wheel step */ +#define TOUCH_TAP_SLOP_DP 12.0f +#define TOUCH_SCROLL_STEP_DP 40.0f +/* the menus post one press a frame and drop presses beyond their queue +(ui_widget.c): one step a read keeps up with them without losing any */ +#define TOUCH_STEPS_PER_READ 1 + +static struct touch_menu menu; +static int menu_ready; +static int menu_active; +/** + * @brief The pixels in a dp: Android's densityDpi / 160, or else a guess + * from the height. + * @return pixels a dp; 0 while neither is known (the window does not exist + * yet) + */ +static float pixels_per_dp(void) +{ + const char *density = getenv("HALO_DISPLAY_DENSITY"); + int width = 0, height = 0; + + if (density && atof(density) > 0.0) + return (float)atof(density); + platform_video_drawable_size(&width, &height); + /* a phone held in landscape is about 360 dp high, whatever its density */ + return height > 0 ? (float)height / 360.0f : 0.0f; +} + +/** + * @brief Sets the gestures' sizes up, once they can be known; until then a + * dp is a pixel and the next call tries again. + */ +static void menu_setup(void) +{ + if (!menu_ready) + { + struct touch_menu_settings settings; + float dp = pixels_per_dp(); + + settings.slop = TOUCH_TAP_SLOP_DP * (dp > 0.0f ? dp : 1.0f); + settings.step = TOUCH_SCROLL_STEP_DP * (dp > 0.0f ? dp : 1.0f); + settings.steps_per_read = TOUCH_STEPS_PER_READ; + touch_menu_init(&menu, &settings); + menu_ready = dp > 0.0f; + } +} + +void touch_input_event(unsigned int type, const SDL_TouchFingerEvent *finger) +{ + int width = 0, height = 0; + float x, y; + + menu_setup(); + /* SDL gives 0..1 of the window; the window is its pixels on Android */ + platform_video_drawable_size(&width, &height); + x = finger->x * (float)width; + y = finger->y * (float)height; + switch (type) + { + case SDL_EVENT_FINGER_DOWN: + touch_menu_down(&menu, finger->fingerID, x, y); + break; + case SDL_EVENT_FINGER_MOTION: + touch_menu_move(&menu, finger->fingerID, x, y); + break; + case SDL_EVENT_FINGER_UP: + touch_menu_up(&menu, finger->fingerID, x, y); + break; + case SDL_EVENT_FINGER_CANCELED: + touch_menu_cancel(&menu); + break; + } +} + +void touch_input_cancel(void) +{ + menu_setup(); + touch_menu_cancel(&menu); +} + +void touch_input_menu_set_active(int active) +{ + /* what a finger began before the menu opened or closed must not + click in it */ + menu_setup(); + touch_menu_reset(&menu); + menu_active = active; +} + +void touch_input_menu_read(struct platform_ui_pointer *pointer) +{ + struct touch_menu_output output; + + menu_setup(); + touch_menu_read(&menu, &output); + pointer->x = output.x; + pointer->y = output.y; + pointer->click_x = output.click_x; + pointer->click_y = output.click_y; + pointer->moved = output.moved ? TRUE : FALSE; + pointer->left_clicks = output.clicks; + pointer->right_clicks = 0; + pointer->wheel_steps = output.wheel_steps; + pointer->touch = TRUE; +} diff --git a/port/linux/src/touch_input.h b/port/linux/src/touch_input.h new file mode 100644 index 00000000..cba4988a --- /dev/null +++ b/port/linux/src/touch_input.h @@ -0,0 +1,42 @@ +/* +TOUCH_INPUT.H + +The touchscreen (touch_input.c). Everything here runs on the game's main +thread. The finger events and touch_input_menu_read run under +sdl_platform.c's input lock. +*/ + +#ifndef TOUCH_INPUT_H +#define TOUCH_INPUT_H + +#include "platform.h" +#include "sdl_platform.h" + +#include + +/** + * @brief Feeds one finger event to the menus' pointer. + * @param type SDL_EVENT_FINGER_DOWN, _MOTION, _UP or _CANCELED + * @param finger the event; its x and y are 0..1 of the window + */ +void touch_input_event(unsigned int type, const SDL_TouchFingerEvent *finger); + +/** + * @brief The window lost the focus: every touch ends without effect. + */ +void touch_input_cancel(void); + +/** + * @brief Says whether the menus' pointer is used (a menu is up). Any + * touch in progress is dropped, so that it does not act in the other mode. + * @param active nonzero while a menu is up + */ +void touch_input_menu_set_active(int active); + +/** + * @brief What the fingers did in the menus since the last read. + * @param pointer receives the taps and the wheel steps; marked as touch + */ +void touch_input_menu_read(struct platform_ui_pointer *pointer); + +#endif diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 6555cb2a..757ffbe9 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5228,6 +5228,11 @@ struct ui_mouse_target rectangle2d bounds; short kind; short button_index; + /* a button's parts, kept so its area can be settled once the whole frame's + buttons are known (ui_mouse_fit_button_targets) */ + rectangle2d icon_bounds; + struct widget_instance *label; + rectangle2d label_bounds; }; static struct ui_mouse_target ui_mouse_targets[UI_MOUSE_MAXIMUM_TARGETS]; @@ -5246,6 +5251,9 @@ static boolean ui_mouse_hover_pending = FALSE; static boolean ui_mouse_click_pending = FALSE; static short ui_mouse_hover_x, ui_mouse_hover_y; static short ui_mouse_click_x, ui_mouse_click_y; +/* whether the latest pointer read was the touchscreen: the taller legend +areas are for a finger only, the desktop mouse keeps their original height */ +static boolean ui_mouse_pointer_is_touch = FALSE; static void ui_mouse_press( short button_index) @@ -5352,6 +5360,14 @@ static boolean ui_mouse_widget_is_item( widget->type == _ui_widget_type_column_list; } +/** + * @brief Notes a widget as a tap target of this frame, if it is one: an + * item, a value, a list slot, or a legend button (kept in parts, which + * ui_mouse_fit_button_targets settles into the tap area). + * @param widget the widget being rendered + * @param definition the widget's definition + * @param offset where the widget's parent is drawn, in menu coordinates + */ static void ui_mouse_note_target( struct widget_instance *widget, struct ui_widget_definition const *definition, @@ -5360,6 +5376,9 @@ static void ui_mouse_note_target( struct widget_instance *parent = widget->parent; struct ui_mouse_target *target; rectangle2d bounds = definition->bounds; + rectangle2d icon_bounds = bounds; + rectangle2d target_label_bounds = bounds; + struct widget_instance *target_label = NULL; short button_index; short kind; @@ -5378,7 +5397,8 @@ static void ui_mouse_note_target( { /* the icon and its label: the text beginning just right of it */ struct widget_instance *sibling; - rectangle2d label_bounds; + rectangle2d label_bounds = bounds; + struct widget_instance *label = NULL; short best_distance = 17; kind = _ui_mouse_target_button; @@ -5402,14 +5422,12 @@ static void ui_mouse_note_target( { best_distance = distance; label_bounds = sibling_bounds; + label = sibling; } } - if (best_distance < 17) - { - bounds.x1 = MAX(bounds.x1, label_bounds.x1); - bounds.y0 = MIN(bounds.y0, label_bounds.y0); - bounds.y1 = MAX(bounds.y1, label_bounds.y1); - } + icon_bounds = bounds; + target_label = best_distance < 17 ? label : NULL; + target_label_bounds = label_bounds; } else if (parent && ui_mouse_list_shows_several(parent)) { @@ -5441,6 +5459,133 @@ static void ui_mouse_note_target( target->bounds = bounds; target->kind = kind; target->button_index = button_index; + target->icon_bounds = icon_bounds; + target->label = target_label; + target->label_bounds = target_label_bounds; + + return; +} + +/** + * @brief The right edge of a legend label's text as the game draws it, or + * NONE when it cannot be measured (no text, no font, or icons in the string, + * which the measuring does not account for). + * @param label the label's text box widget + * @param label_bounds the text box's bounds where it is drawn, in menu coordinates + */ +static short ui_mouse_label_text_right( + struct widget_instance *label, + rectangle2d const *label_bounds) +{ + struct ui_widget_definition *definition = ui_widget_definition_get(label->definition_tag_index); + wchar_t const *text = label->parameters.text_box.text; + real_argb_color color = { 1.0f, 1.0f, 1.0f, 1.0f }; + rectangle2d bounds = *label_bounds; + rectangle2d text_bounds; + rectangle2d cursor_bounds; + + if (!text || !*text || definition->text_font.index == NONE || + definition->justification < 0 || definition->justification >= NUMBER_OF_TEXT_JUSTIFICATIONS || + string_has_icons_to_draw(text)) + { + return NONE; + } + /* as widget_instance_render_text_box places the text */ + bounds.x0 += definition->horizontal_offset; + bounds.y0 += definition->vertical_offset; + /* this leaves the label's font, justification and a white colour as the + global draw mode. That is harmless: it runs at the end of the widget + render, where the menu's own text has already left a menu font there, and + the menu, HUD, cinematic and terminal draws set their font before they + draw; the debug texts that set only a colour or format find a menu font + either way */ + draw_string_set_draw_mode(definition->text_font.index, NONE, definition->justification, 0, &color); + draw_unicode_string_compute_bounds(&bounds, text, &text_bounds, &cursor_bounds); + + return text_bounds.x1 == SHORT_MIN ? NONE : text_bounds.x1; +} + +/** + * @brief Settles the areas of the frame's legend buttons: each reaches its + * label's drawn text (the text box can be far wider than the text, and would + * cover the next legend), stops before the next legend's icon on its row, + * and, for a touchscreen only, grows up to 8 units upward (not past an item, + * value or list slot above it) and in the screen's bottom strip runs down to + * the screen's edge. Recomputed from the parts kept at noting, so running it + * again changes nothing. + */ +static void ui_mouse_fit_button_targets( + void) +{ + long index; + long other; + + for (index = 0; index < ui_mouse_target_count; index++) + { + struct ui_mouse_target *target = &ui_mouse_targets[index]; + short text_right; + + if (target->kind != _ui_mouse_target_button) + continue; + target->bounds = target->icon_bounds; + if (target->label) + { + target->bounds.x1 = MAX(target->bounds.x1, target->label_bounds.x1); + target->bounds.y0 = MIN(target->bounds.y0, target->label_bounds.y0); + target->bounds.y1 = MAX(target->bounds.y1, target->label_bounds.y1); + text_right = ui_mouse_label_text_right(target->label, &target->label_bounds); + if (text_right != NONE) + { + target->bounds.x1 = MAX(target->icon_bounds.x1, MIN(target->label_bounds.x1, text_right + 2)); + } + } + target->bounds.x0 = target->icon_bounds.x0; + } + for (index = 0; index < ui_mouse_target_count; index++) + { + struct ui_mouse_target *target = &ui_mouse_targets[index]; + + if (target->kind != _ui_mouse_target_button) + continue; + for (other = 0; other < ui_mouse_target_count; other++) + { + struct ui_mouse_target const *next = &ui_mouse_targets[other]; + + if (other != index && next->kind == _ui_mouse_target_button && + next->icon_bounds.x0 > target->icon_bounds.x0 && + next->bounds.y0 < target->bounds.y1 && next->bounds.y1 > target->bounds.y0 && + next->icon_bounds.x0 - 1 < target->bounds.x1) + { + target->bounds.x1 = MAX(next->icon_bounds.x0 - 1, target->icon_bounds.x1); + } + } + } + for (index = 0; index < ui_mouse_target_count; index++) + { + struct ui_mouse_target *target = &ui_mouse_targets[index]; + short top; + + if (target->kind != _ui_mouse_target_button || !ui_mouse_pointer_is_touch) + continue; + if (target->bounds.y1 >= 400) + target->bounds.y1 = 480; + /* growing upward must not take the tap from an item, value or list + slot above the legend: those keep their areas, and the legend is noted + later, so it would win the overlap */ + top = target->bounds.y0 - 8; + for (other = 0; other < ui_mouse_target_count; other++) + { + struct ui_mouse_target const *above = &ui_mouse_targets[other]; + + if (above->kind != _ui_mouse_target_button && + above->bounds.y1 <= target->bounds.y0 && + above->bounds.x0 < target->bounds.x1 && above->bounds.x1 > target->bounds.x0) + { + top = MAX(top, above->bounds.y1); + } + } + target->bounds.y0 = top; + } return; } @@ -5540,10 +5685,22 @@ static void ui_mouse_step_list_to_slot( return; } -/* the widget the wheel steps: the innermost on the focus's way that the -d-pad steps through */ +/** + * @brief The widget the wheel steps: the innermost on the focus's way that + * the d-pad steps through. + * + * A touch drag skips a list showing one value at a time (a setting): it + * moves between the rows, around the setting, so a drag never changes a + * value; that takes a tap on one of its halves. The desktop wheel keeps the + * game's own d-pad reach and steps a setting's value. + * + * @param root the menu + * @param skip_settings TRUE for a touch drag + * @return the widget, or NULL when none takes the d-pad + */ static struct widget_instance *ui_mouse_wheel_widget( - struct widget_instance *root) + struct widget_instance *root, + boolean skip_settings) { struct widget_instance *result = NULL; struct widget_instance *widget; @@ -5552,6 +5709,8 @@ static struct widget_instance *ui_mouse_wheel_widget( { struct ui_widget_definition *definition = ui_widget_definition_get(widget->definition_tag_index); + if (skip_settings && widget->type == _ui_widget_type_spinner_list && !ui_mouse_list_shows_several(widget)) + continue; if (definition->flags & (FLAG(_widget_dpad_updown_tabs_thru_children_bit) | FLAG(_widget_dpad_leftright_tabs_thru_children_bit) | FLAG(_widget_dpad_updown_tabs_thru_list_items_bit) | @@ -5564,6 +5723,104 @@ static struct widget_instance *ui_mouse_wheel_widget( return result; } +/** + * @brief How many presses of a d-pad button can step the focus before it + * would wrap around to the other end. + * + * The game's lists wrap (widget_event_function_list_widget_goto_next_item + * and _previous_item, and widget_instance_tab_to_next_valid_widget and + * _previous_), so a long touch drag would lap a short list and stop + * anywhere: the drag stops at the ends. The press goes down from the + * screen to the first widget on the focus's way that tabs in its direction + * (widget_instance_process_one_event_recursive), and the ends are that + * widget's. + * + * @param root the menu + * @param button the d-pad button that would be pressed + * @param forward TRUE to count toward the end after the focus, FALSE toward + * the start + * @return the presses that step before the end, 0 at the end or for a + * setting (changed by clicks only), NONE when no widget tabs that way + */ +static long ui_mouse_wheel_room( + struct widget_instance *root, + short button, + boolean forward) +{ + boolean horizontal = button == _widget_event_dpad_left || button == _widget_event_dpad_right; + struct widget_instance *widget; + + for (widget = root; widget; widget = widget->focused_child) + { + struct ui_widget_definition *definition = ui_widget_definition_get(widget->definition_tag_index); + boolean list = widget->type == _ui_widget_type_spinner_list || widget->type == _ui_widget_type_column_list; + struct widget_instance *child; + long room = 0; + + if (TEST_FLAG(definition->flags, horizontal ? + _widget_dpad_leftright_tabs_thru_children_bit : + _widget_dpad_updown_tabs_thru_children_bit) && + widget->focused_child) + { + /* a child the tab functions would skip gives no press a place to + land, so only the ones that take events, pass them on, or sit in + a list count */ + for (child = forward ? widget->focused_child->next : widget->focused_child->previous; + child; + child = forward ? child->next : child->previous) + { + struct ui_widget_definition *child_definition = ui_widget_definition_get(child->definition_tag_index); + + if (child_definition->event_handlers.count > 0 || + TEST_FLAG(child_definition->flags, _widget_pass_unhandled_events_to_children_bit) || + list) + { + room++; + } + } + + return room; + } + if (list && TEST_FLAG(definition->flags, horizontal ? + _widget_dpad_leftright_tabs_thru_list_items_bit : + _widget_dpad_updown_tabs_thru_list_items_bit)) + { + /* a setting is changed by clicks only */ + if (widget->type == _ui_widget_type_spinner_list && !ui_mouse_list_shows_several(widget)) + return 0; + if (widget->parameters.list.list_items && widget->parameters.list.number_of_items > 0) + { + room = forward ? + widget->parameters.list.number_of_items - 1 - widget->parameters.list.selected_index : + widget->parameters.list.selected_index; + } + else if (widget->focused_child) + { + /* a list of the tag's children, which the focus walks; the + focus skips a disabled child to the next that takes events, or + wraps (widget_instance_give_focus_directly): the main menu ends + with its hidden "game demos" */ + for (child = forward ? widget->focused_child->next : widget->focused_child->previous; + child; + child = forward ? child->next : child->previous) + { + if (!child->disabled) + room++; + } + if (forward && widget->parameters.list.number_of_items > 0) + { + room = MIN(room, widget->parameters.list.number_of_items - 1 - + widget->parameters.list.selected_index); + } + } + + return MAX(room, 0); + } + } + + return NONE; +} + /* the menu the mouse drives: the first player's, or everyone's */ static struct widget_instance *ui_mouse_menu( void) @@ -5590,17 +5847,31 @@ static boolean ui_mouse_menus_active( return virtual_keyboard_active() || ui_mouse_menu() != NULL; } -/* the pointer's motion, clicks and wheel since the last frame, as the first -player's controller events */ +/** + * @brief Turns the pointer's motion, clicks and wheel since the last frame + * into the first player's controller events. While the virtual keyboard is + * up it gets the clicks and the menu behind gets nothing. A touch drag + * stops at the list's ends and skips settings (ui_mouse_wheel_widget, + * ui_mouse_wheel_room); the desktop wheel wraps as the d-pad does. + */ static void ui_widgets_process_mouse( void) { struct halo_ui_pointer pointer; struct ui_mouse_target *target; short controller_index = 0; + boolean pointer_active; + boolean keyboard_active; - if (!halo_ui_pointer_update(ui_mouse_menus_active(), &pointer) || - virtual_keyboard_active()) + pointer_active = halo_ui_pointer_update(ui_mouse_menus_active(), &pointer) != 0; + if (pointer_active) + ui_mouse_pointer_is_touch = pointer.touch != 0; + /* the virtual keyboard takes the pointer's clicks itself; a click that + closes it (Done) is not also a click on the menu behind */ + keyboard_active = virtual_keyboard_active(); + if (pointer_active && keyboard_active && pointer.left_clicks) + virtual_keyboard_click(pointer.click_x, pointer.click_y); + if (!pointer_active || keyboard_active) { ui_mouse_press_count = 0; ui_mouse_hover_pending = FALSE; @@ -5624,14 +5895,34 @@ static void ui_widgets_process_mouse( ui_mouse_press(_widget_event_b_button); if (pointer.wheel_steps && ui_mouse_menu()) { - struct widget_instance *widget = ui_mouse_wheel_widget(ui_mouse_menu()); - short back = _widget_event_dpad_up, forward = _widget_event_dpad_down; + boolean touch = pointer.touch != 0; + struct widget_instance *widget = ui_mouse_wheel_widget(ui_mouse_menu(), touch); + boolean going_forward = pointer.wheel_steps < 0; + short back = _widget_event_dpad_up, forward = _widget_event_dpad_down, button; + long room = NONE; + long index; long step; if (widget) ui_mouse_list_directions(widget, &back, &forward); - for (step = 0; step < ABS(pointer.wheel_steps) && step < 4; step++) - ui_mouse_press(pointer.wheel_steps > 0 ? back : forward); + button = going_forward ? forward : back; + /* only a touch drag stops at the ends, and does nothing with no list + to step; the desktop wheel wraps as the d-pad does, and falls back + to up and down */ + if (touch) + { + room = widget ? ui_mouse_wheel_room(ui_mouse_menu(), button, going_forward) : 0; + /* the presses still waiting step the list first */ + for (index = 0; widget && room != NONE && index < ui_mouse_press_count; index++) + { + if (ui_mouse_presses[index] == button) + room = MAX(room - 1, 0); + else if (ui_mouse_presses[index] == (going_forward ? back : forward)) + room++; + } + } + for (step = 0; step < ABS(pointer.wheel_steps) && step < 4 && (room == NONE || step < room); step++) + ui_mouse_press(button); } if (!ui_mouse_press_count && ui_mouse_hover_pending) { @@ -5969,6 +6260,13 @@ void render_ui_widgets_postgame( return; } +/** + * @brief Renders the active widgets for one local player's viewport (or the + * whole screen), noting the first player's tap targets, and draws the + * debug view of the targets and the virtual keyboard when they are up. + * @param local_player_index the viewport's player, or NONE for the whole screen + * @param window_bounds the viewport's bounds on the screen + */ void render_ui_widgets( short local_player_index, rectangle2d const *window_bounds) @@ -6066,6 +6364,7 @@ void render_ui_widgets( } } } + ui_mouse_fit_button_targets(); if (widget_globals.fade_to_black >= 0.0f && widget_globals.fade_to_black <= 1.0f) { diff --git a/source/interface/virtual_keyboard.c b/source/interface/virtual_keyboard.c index bd924d49..faa42c48 100644 --- a/source/interface/virtual_keyboard.c +++ b/source/interface/virtual_keyboard.c @@ -1314,3 +1314,67 @@ static void virtual_keyboard_process_internal( return; } + +/* The "B =BACK" and "A =ENTER" legends are part of the keyboard's background +bitmap, not widgets, so nothing else can hit-test them; measured on the +rendered screen (icon and label, with a few pixels of margin). */ +static rectangle2d const keyboard_back_legend_rect= {412, 368, 438, 462}; +static rectangle2d const keyboard_enter_legend_rect= {412, 468, 438, 572}; + +/* (virtual_keyboard.h) */ +boolean virtual_keyboard_click( + short x, + short y) +{ + short key_index, row, column; + + if (!virtual_keyboard_globals.active) + return FALSE; + if (x >= keyboard_back_legend_rect.x0 && x < keyboard_back_legend_rect.x1 && + y >= keyboard_back_legend_rect.y0 && y < keyboard_back_legend_rect.y1) + { + if (virtual_keyboard_cancel() == TRUE) + { + virtual_keyboard_globals.time_of_last_event = system_milliseconds(); + virtual_keyboard_globals.last_event = _event_cancel; + } + return TRUE; + } + if (x >= keyboard_enter_legend_rect.x0 && x < keyboard_enter_legend_rect.x1 && + y >= keyboard_enter_legend_rect.y0 && y < keyboard_enter_legend_rect.y1) + { + key_index = virtual_keyboard_layout_table[0][0]; + } + else + { + for (key_index = 0; key_index < NUMBER_OF_VIRTUAL_KEYS; key_index++) + { + rectangle2d const *rectangle = &keyboard_rect[key_index]; + + if (x >= rectangle->x0 && x < rectangle->x1 && y >= rectangle->y0 && y < rectangle->y1) + break; + } + if (key_index == NUMBER_OF_VIRTUAL_KEYS) + return FALSE; + } + for (row = 0; row < VIRTUAL_KEYBOARD_ROW_COUNT; row++) + { + for (column = 0; column < VIRTUAL_KEYBOARD_COLUMN_COUNT; column++) + { + if (virtual_keyboard_layout_table[row][column] == key_index) + { + virtual_keyboard_globals.row = row; + virtual_keyboard_globals.column = column; + virtual_keyboard_globals.last_key = key_index; + if (virtual_keyboard_select() == TRUE) + { + virtual_keyboard_globals.time_of_last_event = system_milliseconds(); + virtual_keyboard_globals.last_event = _event_key_select; + } + return TRUE; + } + } + } + + return FALSE; +} diff --git a/source/interface/virtual_keyboard.h b/source/interface/virtual_keyboard.h index 6f406413..21d74d16 100644 --- a/source/interface/virtual_keyboard.h +++ b/source/interface/virtual_keyboard.h @@ -39,6 +39,24 @@ void virtual_keyboard_process( void virtual_keyboard_render( void); +/** + * @brief Applies a click or tap, in the menus' 640x480 coordinates that the + * keyboard draws in, to the keyboard. + * + * A key takes the focus and is pressed as A presses the focused key. BACK + * cancels as B does. ENTER goes to Done and presses it as Start does, not as + * A does: a touch has no focused key to confirm with. Keys that span several + * cells take the focus at their first. + * + * @param x horizontal position of the click + * @param y vertical position of the click + * @return TRUE if the click was on a key or on the BACK or ENTER legend, + * which then acted; FALSE otherwise + */ +boolean virtual_keyboard_click( + short x, + short y); + /* ---------- globals */ /* ---------- public code */ From 798f3887321cd548b788ea9383c92a59bb1f56da Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Wed, 30 Sep 2026 22:02:15 +0200 Subject: [PATCH 003/187] Skip cinematics with a tap --- port/android/README.md | 9 ++++++--- port/linux/game/touch_game.c | 26 ++++++++++++++++++++++++++ port/linux/src/touch_input.c | 31 +++++++++++++++++++++++++++++++ port/linux/src/touch_input.h | 12 +++++++++++- port/linux/src/xinput_sdl.c | 10 ++++++++++ 5 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 port/linux/game/touch_game.c diff --git a/port/android/README.md b/port/android/README.md index 3886a145..a4628c36 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -99,9 +99,11 @@ setting with values, tap its left or right half), tap a button of the key at the bottom of a screen (for example "B = Back") to push it, and drag to scroll a list (down or right steps back, up or left steps forward). A drag stops at the first and the last item, and it does not change a setting's -value. The gameplay does not accept touch input yet. +value. Apart from skipping cinematics, the gameplay does not accept touch +input. -On the on-screen keyboard, tap a key to press it, "B =BACK" to cancel and +A tap during a cinematic that can be skipped skips it, as A does. On the +on-screen keyboard, tap a key to press it, "B =BACK" to cancel and "A =ENTER" to accept the name. ## Settings @@ -319,6 +321,7 @@ assembly of the port is necessary: - The device must let the app reserve the fixed guest addresses, from `0x80000000` to approximately `0x89000000`. If the addresses are not available, the app shows a message. -- Touch operates only the menus. In the game, use a controller or a keyboard. +- Touch operates the menus and skips cinematics. In the game, use a controller + or a keyboard. - Kernels with 16 KB pages (a developer option of Android 15) do not operate. The Xbox memory uses 4 KB pages. diff --git a/port/linux/game/touch_game.c b/port/linux/game/touch_game.c new file mode 100644 index 00000000..18fa43bd --- /dev/null +++ b/port/linux/game/touch_game.c @@ -0,0 +1,26 @@ +/* +TOUCH_GAME.C + +What the touch controls (port/linux/src/touch_input.c) need to know of the +game. The platform layer cannot see the game's types, so it asks here +(this file is compiled as the game's own sources are). +*/ + +#include "cseries.h" +#include "game/game.h" +#include "cutscene/cinematics.h" + +/** + * @brief Asks whether a cinematic is playing that A would skip + * (player_control.c, player_control_action_test_check_reset_input_blob). + * The controller is read before the game exists (input_initialize, during + * shell_initialize), so cinematic_globals is tested first: game_initialize + * makes it after the game time globals that game_in_progress reads + * (game.c). + * @return nonzero if A would skip the cinematic now + */ +int touch_game_cinematic_skippable(void) +{ + return cinematic_globals && game_in_progress() && cinematic_in_progress() && + cinematic_can_be_skipped(); +} diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index b7450767..d0b48e35 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -12,6 +12,9 @@ display density the app passes (HALO_DISPLAY_DENSITY, host_main.c). #include +/* the game's (port/linux/game/touch_game.c) */ +int touch_game_cinematic_skippable(void); + /* a tap moves at most this far; a drag of this length is one wheel step */ #define TOUCH_TAP_SLOP_DP 12.0f #define TOUCH_SCROLL_STEP_DP 40.0f @@ -22,6 +25,11 @@ display density the app passes (HALO_DISPLAY_DENSITY, host_main.c). static struct touch_menu menu; static int menu_ready; static int menu_active; +/* polls left to hold A: a tap is shorter than the game's poll, and the +game must see the press */ +static int skip_polls; +#define TOUCH_PRESS_POLLS 2 + /** * @brief The pixels in a dp: Android's densityDpi / 160, or else a guess * from the height. @@ -117,3 +125,26 @@ void touch_input_menu_read(struct platform_ui_pointer *pointer) pointer->wheel_steps = output.wheel_steps; pointer->touch = TRUE; } + +void touch_input_gamepad(XINPUT_GAMEPAD *pad) +{ + /* outside the menus the touchscreen only skips a cinematic that can be + skipped: a tap there presses A */ + if (!menu_active) + { + struct touch_menu_output output; + + menu_setup(); + touch_menu_read(&menu, &output); + if (output.clicks && touch_game_cinematic_skippable()) + { + skip_polls = TOUCH_PRESS_POLLS; + platform_log("touch: A pressed to skip a cinematic"); + } + } + if (skip_polls > 0) + { + pad->bAnalogButtons[XINPUT_GAMEPAD_A] = 0xff; + skip_polls--; + } +} diff --git a/port/linux/src/touch_input.h b/port/linux/src/touch_input.h index cba4988a..92257507 100644 --- a/port/linux/src/touch_input.h +++ b/port/linux/src/touch_input.h @@ -3,7 +3,10 @@ TOUCH_INPUT.H The touchscreen (touch_input.c). Everything here runs on the game's main thread. The finger events and touch_input_menu_read run under -sdl_platform.c's input lock. +sdl_platform.c's input lock; touch_input_gamepad, called from +XInputGetState, does not, and reads the same touch_menu state without it. +That is safe only because platform_pump_events and XInputGetState both run +on the game's main thread: a call from any other thread needs the lock. */ #ifndef TOUCH_INPUT_H @@ -39,4 +42,11 @@ void touch_input_menu_set_active(int active); */ void touch_input_menu_read(struct platform_ui_pointer *pointer); +/** + * @brief Adds the touch controls to controller 1's state. Outside the menus + * a tap presses A for a few polls when a cinematic can be skipped. + * @param pad the state XInputGetState is about to return + */ +void touch_input_gamepad(XINPUT_GAMEPAD *pad); + #endif diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index 68e32a0a..2b199300 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -35,6 +35,7 @@ drive the controller. #include "platform.h" #include "sdl_platform.h" #include "port_config.h" +#include "touch_input.h" #include #include @@ -525,6 +526,14 @@ static int controller_port(HANDLE device) return -1; } +/** + * @brief Reads a controller's state. For port 0 the keyboard, the mouse, + * the debug input and the touchscreen are merged into the first gamepad's. + * Runs on the game's main thread (touch_input_gamepad relies on it). + * @param device the controller's handle + * @param state receives the state (cleared first) + * @return ERROR_SUCCESS, or ERROR_DEVICE_NOT_CONNECTED for an unknown port + */ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state) { int port = controller_port(device); @@ -548,6 +557,7 @@ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state) if (count > 0) sdl_gamepad_state(gamepads[0], &state->Gamepad); test_input_gamepad(&state->Gamepad); + touch_input_gamepad(&state->Gamepad); if (abs(state->Gamepad.sThumbRX) > STICK_AIMING_DEFLECTION || abs(state->Gamepad.sThumbRY) > STICK_AIMING_DEFLECTION) { From 4712923c673af3933f09546e9ff5c26ef49de9db Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Wed, 30 Sep 2026 22:02:17 +0200 Subject: [PATCH 004/187] Add debug.touch_targets --- port/linux/README.md | 1 + port/linux/include/halo_ui_pointer.h | 4 + port/linux/src/d3d8_gl.c | 2 + port/linux/src/port_config.c | 5 + port/linux/src/sdl_platform.h | 4 + port/linux/src/touch_input.c | 3 + port/linux/src/touch_menu.c | 5 + port/linux/src/touch_menu.h | 9 +- port/linux/tests/touch_menu_test.c | 80 +++++++++ source/interface/ui_widget.c | 250 ++++++++++++++++++++++++++- source/interface/virtual_keyboard.c | 29 +++- source/interface/virtual_keyboard.h | 18 +- tools/mutate_touch_menu.py | 24 ++- 13 files changed, 423 insertions(+), 11 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index 76fd3936..b8a9b65b 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -185,6 +185,7 @@ the setting for one start of the game. It has priority over the file. | `debug.gpu_stats`, `debug.gpu_trace_frame`, `debug.gpu_trace_constants`, `debug.gpu_dump_shaders`, `debug.texture_dump_directory`, `debug.texture_log`, `debug.gl_debug`, `debug.texture_no_cache` | off | `HALO_GPU_STATS`, `HALO_GPU_TRACE`, `HALO_GPU_TRACE_CONSTANTS`, `HALO_GPU_DUMP_SHADERS`, `HALO_TEXTURE_DUMP`, `HALO_TEXTURE_LOG`, `HALO_GL_DEBUG`, `HALO_TEXTURE_NO_CACHE` | Tools to find problems in the graphics: counts for each frame, all the GL state of one frame, the GLSL code, the textures. | | `debug.gpu_skip_vertex_shaders`, `debug.gpu_debug_expression`, `debug.gpu_debug_flat`, `debug.gpu_debug_texture0` | off | `HALO_GPU_SKIP_VS`, `HALO_GPU_DEBUG_EXPR`, `HALO_GPU_DEBUG_FLAT`, `HALO_GPU_DEBUG_T0` | Tools to find problems in the graphics: skip the draws of a vertex shader, or replace the output of all pixel shaders with a GLSL expression (for example `t0.rgb`). | | `debug.network_test`, `debug.network_test_start`, `debug.network_test_kill`, `debug.network_test_score`, `debug.network_test_shoot`, `debug.network_test_vehicle`, `debug.network_test_pickup`, `debug.network_test_pickup_weapon`, `debug.test_input` | off | `HALO_NETWORK_TEST`, `HALO_NETWORK_TEST_START`, `HALO_NETWORK_TEST_KILL`, `HALO_NETWORK_TEST_SCORE`, `HALO_NETWORK_TEST_SHOOT`, `HALO_NETWORK_TEST_VEHICLE`, `HALO_NETWORK_TEST_PICKUP`, `HALO_NETWORK_TEST_PICKUP_WEAPON`, `HALO_TEST_INPUT` | Automatic tests of system link (`game/network_test.c`). Refer to `NETCODE.md`. | +| `debug.touch_targets` | `false` | `HALO_TOUCH_TARGETS` | Outlines the tap targets of the menus (item green, value blue, list slot yellow, legend button red, keys of the on-screen keyboard white), marks where the last finger went down and the last tap landed for 3 seconds, and logs each tap with the target that it hit: to judge the accuracy of touch. | | `debug.network_latency`, `debug.network_loss` | `0` | `HALO_NETWORK_LATENCY`, `HALO_NETWORK_LOSS` | The game holds all the data that it receives for this number of milliseconds, and ignores this percentage of the datagrams. Use these settings to test the netcode as on the internet. | | `debug.telnet_console`, `debug.telnet_console_port` | `false`, `2323` | `HALO_TELNET_CONSOLE`, `HALO_TELNET_CONSOLE_PORT` | The game listens on 127.0.0.1, on this port, for a script console (connect with telnet). The console has no password, so only this computer can reach it. | diff --git a/port/linux/include/halo_ui_pointer.h b/port/linux/include/halo_ui_pointer.h index d0650d34..417cf3b1 100644 --- a/port/linux/include/halo_ui_pointer.h +++ b/port/linux/include/halo_ui_pointer.h @@ -26,6 +26,10 @@ struct halo_ui_pointer /* nonzero when the pointer is the touchscreen, not a mouse: a drag and a wheel notch are the same steps, but the menus treat them differently */ unsigned char touch; + /* fingers down since the last read and where the latest went down; only + the touchscreen reports them (debug.touch_targets shows them) */ + unsigned char downs; + short down_x, down_y; }; /** diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 976d41e5..f67da081 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -1187,6 +1187,8 @@ int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer) pointer->right_clicks = (unsigned char)(state.right_clicks < 255 ? state.right_clicks : 255); pointer->wheel_steps = (signed char)(state.wheel_steps < -8 ? -8 : state.wheel_steps > 8 ? 8 : state.wheel_steps); pointer->touch = state.touch != FALSE; + pointer->downs = (unsigned char)(state.downs < 255 ? state.downs : 255); + ui_point_from_window(state.down_x, state.down_y, &pointer->down_x, &pointer->down_y); return 1; } diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 1d54378d..c5657e51 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -207,6 +207,11 @@ static const struct config_setting config_settings[] = _platform_all, "The port of the script console (telnet_console); the Xbox's was 23, which\n" "only the administrator can listen on." }, + { "debug.touch_targets", _config_boolean, "false", "HALO_TOUCH_TARGETS", _environment_set_is_true, _platform_all, + "Outline the menus' tap targets (item green, value blue, list slot yellow,\n" + "legend button red; the virtual keyboard's keys white), mark where the\n" + "last finger went down and the last tap landed for 3 seconds, and log\n" + "each tap with the target it hit; to judge touch accuracy." }, { "debug.network_latency", _config_real, "0.0", "HALO_NETWORK_LATENCY", _environment_value, _platform_all, "Milliseconds everything received is held back (a round trip between two\n" "machines of twice it), to test the netcode as over the internet; 0 none." }, diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index 2bacee99..eae1fa8d 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -64,6 +64,10 @@ struct platform_ui_pointer BOOL moved; int left_clicks, right_clicks; int wheel_steps; + /* fingers down since the last read, and where the latest went down + (the touchscreen only; the debug view of the menus' targets shows it) */ + int downs; + float down_x, down_y; /* the pointer is the touchscreen (touch_input.c), not a mouse */ BOOL touch; }; diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index d0b48e35..7bd5047b 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -123,6 +123,9 @@ void touch_input_menu_read(struct platform_ui_pointer *pointer) pointer->left_clicks = output.clicks; pointer->right_clicks = 0; pointer->wheel_steps = output.wheel_steps; + pointer->downs = output.downs; + pointer->down_x = output.down_x; + pointer->down_y = output.down_y; pointer->touch = TRUE; } diff --git a/port/linux/src/touch_menu.c b/port/linux/src/touch_menu.c index 97d24723..15936b7b 100644 --- a/port/linux/src/touch_menu.c +++ b/port/linux/src/touch_menu.c @@ -30,6 +30,9 @@ void touch_menu_down(struct touch_menu *menu, unsigned long long finger, float x menu->finger = finger; menu->down_x = menu->last_x = x; menu->down_y = menu->last_y = y; + menu->output.downs++; + menu->output.down_x = x; + menu->output.down_y = y; menu->scrolling = 0; menu->remainder = 0.0f; /* a finger on the screen stops a list still scrolling, so that a tap @@ -96,6 +99,7 @@ void touch_menu_cancel(struct touch_menu *menu) menu->scrolling = 0; menu->remainder = 0.0f; menu->pending_steps = 0; + menu->output.downs = 0; } void touch_menu_read(struct touch_menu *menu, struct touch_menu_output *output) @@ -112,4 +116,5 @@ void touch_menu_read(struct touch_menu *menu, struct touch_menu_output *output) menu->pending_steps -= steps; menu->output.moved = 0; menu->output.clicks = 0; + menu->output.downs = 0; } diff --git a/port/linux/src/touch_menu.h b/port/linux/src/touch_menu.h index 4fb90e37..7f111146 100644 --- a/port/linux/src/touch_menu.h +++ b/port/linux/src/touch_menu.h @@ -40,6 +40,11 @@ struct touch_menu_output int clicks; /* positive: back (the finger moved down or right) */ int wheel_steps; + /* fingers that went down since the last read, and where the latest + did: a drag reports no tap, and the debug view (debug.touch_targets) + wants to show where it began */ + int downs; + float down_x, down_y; }; struct touch_menu @@ -76,7 +81,9 @@ void touch_menu_reset(struct touch_menu *menu); /** * @brief A finger went down. A second finger is ignored while one is * down. The steps still waiting are dropped, so that a tap hits what the - * finger sees on a list still scrolling. + * finger sees on a list still scrolling. The down is counted and its point + * kept for the next read (downs, down_x, down_y), even when the finger goes + * on to scroll. * @param menu the state * @param finger the finger's id * @param x,y where it went down, in pixels diff --git a/port/linux/tests/touch_menu_test.c b/port/linux/tests/touch_menu_test.c index 04f160fa..25a6671c 100644 --- a/port/linux/tests/touch_menu_test.c +++ b/port/linux/tests/touch_menu_test.c @@ -533,6 +533,81 @@ static void test_one_step_a_read(void) CHECK(out.wheel_steps == 0); } +/* the debug view (debug.touch_targets) shows where a drag began, which no +tap or hover reports */ +static void test_a_drag_reports_where_the_finger_went_down(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 3, 400.0f, 200.0f); + touch_menu_move(&menu, 3, 400.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.downs == 1); + CHECK(out.down_x == 400.0f && out.down_y == 200.0f); + CHECK(out.clicks == 0); + touch_menu_read(&menu, &out); + CHECK(out.downs == 0); +} + +static void test_a_tap_reports_its_down(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 50.0f, 60.0f); + touch_menu_up(&menu, 1, 50.0f, 60.0f); + touch_menu_read(&menu, &out); + CHECK(out.downs == 1); + CHECK(out.down_x == 50.0f && out.down_y == 60.0f); +} + +static void test_two_downs_between_reads_count_twice(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 50.0f, 60.0f); + touch_menu_up(&menu, 1, 50.0f, 60.0f); + touch_menu_down(&menu, 2, 70.0f, 80.0f); + touch_menu_read(&menu, &out); + CHECK(out.downs == 2); + CHECK(out.down_x == 70.0f && out.down_y == 80.0f); +} + +/* an ignored second finger is not a down */ +static void test_a_second_finger_is_no_down(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 50.0f, 60.0f); + touch_menu_down(&menu, 2, 70.0f, 80.0f); + touch_menu_read(&menu, &out); + CHECK(out.downs == 1); + CHECK(out.down_x == 50.0f && out.down_y == 60.0f); +} + +static void test_reset_and_cancel_drop_the_unread_downs(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 50.0f, 60.0f); + touch_menu_reset(&menu); + touch_menu_read(&menu, &out); + CHECK(out.downs == 0); + touch_menu_down(&menu, 1, 50.0f, 60.0f); + touch_menu_cancel(&menu); + touch_menu_read(&menu, &out); + CHECK(out.downs == 0); +} + int main(void) { test_init_stores_the_settings(); @@ -568,6 +643,11 @@ int main(void) test_reset_keeps_the_settings(); test_cancel_keeps_a_finished_tap(); test_one_step_a_read(); + test_a_drag_reports_where_the_finger_went_down(); + test_a_tap_reports_its_down(); + test_two_downs_between_reads_count_twice(); + test_a_second_finger_is_no_down(); + test_reset_and_cancel_drop_the_unread_downs(); if (failures) printf("%d failures\n", failures); else diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 757ffbe9..12991c39 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5255,6 +5255,163 @@ static short ui_mouse_click_x, ui_mouse_click_y; areas are for a finger only, the desktop mouse keeps their original height */ static boolean ui_mouse_pointer_is_touch = FALSE; +/* ---------- the debug view of the targets (debug.touch_targets) + +Drawn by render_ui_widgets in the pass that noted the targets: the list is +only valid from the render that noted them to the next +ui_widgets_process_mouse, which clears it. */ + +#define UI_DEBUG_MARK_MILLISECONDS 3000 +#define UI_DEBUG_KEYBOARD_RECTANGLES 96 + +struct ui_debug_mark +{ + boolean shown; + short x, y; + unsigned long time; +}; + +static struct ui_debug_mark ui_debug_down_mark, ui_debug_tap_mark; +/* the frames ui_widgets_process_mouse has run, and the one the latest tap arrived in */ +static long ui_debug_frame, ui_debug_click_frame; + +void platform_log(char const *format, ...); +int config_boolean(char const *name); + +/** + * @brief Whether debug.touch_targets is on. Read once: the settings do not + * change while the game runs. + */ +static boolean ui_debug_targets_enabled( + void) +{ + static long enabled = NONE; + + if (enabled == NONE) + enabled = config_boolean("debug.touch_targets") != 0; + + return enabled != 0; +} + +/** + * @brief Outlines a rectangle, 2 menu pixels thick, with the game's own quad + * drawing, in the menus' coordinates (the caller has the menus' centering + * offset on). + * @param bounds the rectangle to outline, in menu coordinates + * @param color the outline's ARGB color + */ +static void ui_debug_draw_outline( + rectangle2d const *bounds, + pixel32 color) +{ + rectangle2d edge; + + edge = *bounds; + edge.y1 = edge.y0 + 2; + draw_quad(&edge, color); + edge = *bounds; + edge.y0 = edge.y1 - 2; + draw_quad(&edge, color); + edge = *bounds; + edge.x1 = edge.x0 + 2; + draw_quad(&edge, color); + edge = *bounds; + edge.x0 = edge.x1 - 2; + draw_quad(&edge, color); + + return; +} + +/** + * @brief Draws a cross of about 8 menu pixels at a mark that is still + * within its 3 seconds. + * @param mark the remembered point and when it happened + * @param color the cross's ARGB color + */ +static void ui_debug_draw_mark( + struct ui_debug_mark const *mark, + pixel32 color) +{ + rectangle2d arm; + + if (!mark->shown || + system_milliseconds() - mark->time > UI_DEBUG_MARK_MILLISECONDS) + { + return; + } + arm.x0 = mark->x - 4; + arm.x1 = mark->x + 4; + arm.y0 = mark->y - 1; + arm.y1 = mark->y + 1; + draw_quad(&arm, color); + arm.x0 = mark->x - 1; + arm.x1 = mark->x + 1; + arm.y0 = mark->y - 4; + arm.y1 = mark->y + 4; + draw_quad(&arm, color); + + return; +} + +/** + * @brief Outlines the targets the pointer code collected this frame (or + * the virtual keyboard's keys while it is up), and the last finger-down and + * tap points. Only for the render that notes the targets (the first + * player's): a split-screen viewport that is not hit-tested shows nothing, + * or it would show outlines that no tap uses. + * @param first_players_render whether this render is the one that notes targets + */ +static void ui_debug_draw_targets( + boolean first_players_render) +{ + long index; + + if (!first_players_render || !ui_debug_targets_enabled()) + return; + if (virtual_keyboard_active()) + { + rectangle2d rectangles[UI_DEBUG_KEYBOARD_RECTANGLES]; + long count = virtual_keyboard_target_rectangles(rectangles, UI_DEBUG_KEYBOARD_RECTANGLES); + + for (index = 0; index < count; index++) + ui_debug_draw_outline(&rectangles[index], 0xc0ffffff); + } + else + { + for (index = 0; index < ui_mouse_target_count; index++) + { + static pixel32 const colors[] = { 0xc000ff00, 0xc04080ff, 0xc0ffff00, 0xc0ff0000 }; + short kind = ui_mouse_targets[index].kind; + + ui_debug_draw_outline(&ui_mouse_targets[index].bounds, colors[PIN(kind, 0, 3)]); + } + } + ui_debug_draw_mark(&ui_debug_down_mark, 0xffff00ff); + ui_debug_draw_mark(&ui_debug_tap_mark, 0xff00ffff); + + return; +} + +/** + * @brief Remembers a finger-down or tap point for the debug view, to show + * it for 3 seconds. + * @param mark receives the point and the time + * @param x horizontal position, in menu coordinates + * @param y vertical position, in menu coordinates + */ +static void ui_debug_set_mark( + struct ui_debug_mark *mark, + short x, + short y) +{ + mark->shown = TRUE; + mark->x = x; + mark->y = y; + mark->time = system_milliseconds(); + + return; +} + static void ui_mouse_press( short button_index) { @@ -5847,12 +6004,80 @@ static boolean ui_mouse_menus_active( return virtual_keyboard_active() || ui_mouse_menu() != NULL; } +/** + * @brief Logs a tap the menus resolved: where it was and the target the + * click acts on. A click waits for the presses the mouse queued (a list + * slot's hover steps the list first), so it is resolved against the + * targets of a later frame than the one the tap arrived in; the frames + * between are logged so the target can be read against the screen that + * showed then. + * @param x horizontal position of the tap, in menu coordinates + * @param y vertical position of the tap, in menu coordinates + * @param target what the click acts on, or NULL for none + * @param frames frames between the tap's arrival and its resolution + */ +static void ui_debug_log_click( + short x, + short y, + struct ui_mouse_target const *target, + long frames) +{ + static char const *const kinds[] = { "item", "value", "list slot", "button" }; + + if (target) + { + platform_log("touch targets: tap at %d,%d hit %s %s [%d,%d,%d,%d] after %ld frames", x, y, + kinds[PIN(target->kind, 0, 3)], + tag_get_name(target->widget->definition_tag_index), + target->bounds.x0, target->bounds.y0, target->bounds.x1, target->bounds.y1, frames); + } + else + { + platform_log("touch targets: tap at %d,%d hit none after %ld frames", x, y, frames); + } + + return; +} + +/** + * @brief Logs a tap while the virtual keyboard is up: the key or legend + * that took it. + * @param x horizontal position of the tap, in menu coordinates + * @param y vertical position of the tap, in menu coordinates + * @param hit what virtual_keyboard_click matched (an index into + * virtual_keyboard_target_rectangles), or NONE + */ +static void ui_debug_log_keyboard_tap( + short x, + short y, + long hit) +{ + rectangle2d rectangles[UI_DEBUG_KEYBOARD_RECTANGLES]; + long count = virtual_keyboard_target_rectangles(rectangles, UI_DEBUG_KEYBOARD_RECTANGLES); + + if (hit == NONE || hit >= count) + { + platform_log("touch targets: tap at %d,%d hit no keyboard key", x, y); + } + else + { + platform_log("touch targets: tap at %d,%d hit keyboard %s %ld [%d,%d,%d,%d]", x, y, + hit == count - 2 ? "BACK legend" : hit == count - 1 ? "ENTER legend" : "key", + hit, rectangles[hit].x0, rectangles[hit].y0, rectangles[hit].x1, rectangles[hit].y1); + } + + return; +} + /** * @brief Turns the pointer's motion, clicks and wheel since the last frame * into the first player's controller events. While the virtual keyboard is * up it gets the clicks and the menu behind gets nothing. A touch drag * stops at the list's ends and skips settings (ui_mouse_wheel_widget, - * ui_mouse_wheel_room); the desktop wheel wraps as the d-pad does. + * ui_mouse_wheel_room); the desktop wheel wraps as the d-pad does. With + * debug.touch_targets on it also notes the finger-down and tap points for + * the debug view, and logs each tap: a menu click where it is resolved, a + * keyboard click where it is taken. */ static void ui_widgets_process_mouse( void) @@ -5863,14 +6088,31 @@ static void ui_widgets_process_mouse( boolean pointer_active; boolean keyboard_active; + ui_debug_frame++; pointer_active = halo_ui_pointer_update(ui_mouse_menus_active(), &pointer) != 0; if (pointer_active) ui_mouse_pointer_is_touch = pointer.touch != 0; + if (pointer_active && ui_debug_targets_enabled()) + { + if (pointer.downs) + ui_debug_set_mark(&ui_debug_down_mark, pointer.down_x, pointer.down_y); + if (pointer.left_clicks) + { + ui_debug_set_mark(&ui_debug_tap_mark, pointer.click_x, pointer.click_y); + ui_debug_click_frame = ui_debug_frame; + } + } /* the virtual keyboard takes the pointer's clicks itself; a click that closes it (Done) is not also a click on the menu behind */ keyboard_active = virtual_keyboard_active(); if (pointer_active && keyboard_active && pointer.left_clicks) - virtual_keyboard_click(pointer.click_x, pointer.click_y); + { + long hit; + + virtual_keyboard_click(pointer.click_x, pointer.click_y, &hit); + if (ui_debug_targets_enabled()) + ui_debug_log_keyboard_tap(pointer.click_x, pointer.click_y, hit); + } if (!pointer_active || keyboard_active) { ui_mouse_press_count = 0; @@ -5946,6 +6188,8 @@ static void ui_widgets_process_mouse( { ui_mouse_click_pending = FALSE; target = ui_mouse_target_at(ui_mouse_click_x, ui_mouse_click_y); + if (ui_debug_targets_enabled()) + ui_debug_log_click(ui_mouse_click_x, ui_mouse_click_y, target, ui_debug_frame - ui_debug_click_frame); if (target) { switch (target->kind) @@ -6273,6 +6517,7 @@ void render_ui_widgets( { rectangle2d bounds; long widget_index; + boolean first_players_render = local_player_index == NONE || local_player_index == 0; match_assert( "c:\\halo\\SOURCE\\interface\\ui_widget.c", @@ -6385,6 +6630,7 @@ void render_ui_widgets( { virtual_keyboard_render(); } + ui_debug_draw_targets(first_players_render); return; } diff --git a/source/interface/virtual_keyboard.c b/source/interface/virtual_keyboard.c index faa42c48..cf8a588b 100644 --- a/source/interface/virtual_keyboard.c +++ b/source/interface/virtual_keyboard.c @@ -1321,18 +1321,41 @@ rendered screen (icon and label, with a few pixels of margin). */ static rectangle2d const keyboard_back_legend_rect= {412, 368, 438, 462}; static rectangle2d const keyboard_enter_legend_rect= {412, 468, 438, 572}; +/* (virtual_keyboard.h) */ +long virtual_keyboard_target_rectangles( + rectangle2d *rectangles, + long maximum) +{ + long count = 0; + short key_index; + + for (key_index = 0; key_index < NUMBER_OF_VIRTUAL_KEYS && count < maximum; key_index++) + rectangles[count++] = keyboard_rect[key_index]; + if (count < maximum) + rectangles[count++] = keyboard_back_legend_rect; + if (count < maximum) + rectangles[count++] = keyboard_enter_legend_rect; + + return count; +} + /* (virtual_keyboard.h) */ boolean virtual_keyboard_click( short x, - short y) + short y, + long *hit) { short key_index, row, column; + if (hit) + *hit = NONE; if (!virtual_keyboard_globals.active) return FALSE; if (x >= keyboard_back_legend_rect.x0 && x < keyboard_back_legend_rect.x1 && y >= keyboard_back_legend_rect.y0 && y < keyboard_back_legend_rect.y1) { + if (hit) + *hit = NUMBER_OF_VIRTUAL_KEYS; if (virtual_keyboard_cancel() == TRUE) { virtual_keyboard_globals.time_of_last_event = system_milliseconds(); @@ -1343,6 +1366,8 @@ boolean virtual_keyboard_click( if (x >= keyboard_enter_legend_rect.x0 && x < keyboard_enter_legend_rect.x1 && y >= keyboard_enter_legend_rect.y0 && y < keyboard_enter_legend_rect.y1) { + if (hit) + *hit = NUMBER_OF_VIRTUAL_KEYS + 1; key_index = virtual_keyboard_layout_table[0][0]; } else @@ -1356,6 +1381,8 @@ boolean virtual_keyboard_click( } if (key_index == NUMBER_OF_VIRTUAL_KEYS) return FALSE; + if (hit) + *hit = key_index; } for (row = 0; row < VIRTUAL_KEYBOARD_ROW_COUNT; row++) { diff --git a/source/interface/virtual_keyboard.h b/source/interface/virtual_keyboard.h index 21d74d16..c551f29c 100644 --- a/source/interface/virtual_keyboard.h +++ b/source/interface/virtual_keyboard.h @@ -50,12 +50,28 @@ void virtual_keyboard_render( * * @param x horizontal position of the click * @param y vertical position of the click + * @param hit receives which rectangle matched, as its index in + * virtual_keyboard_target_rectangles (the keys, then BACK, then ENTER), or + * NONE; may be NULL * @return TRUE if the click was on a key or on the BACK or ENTER legend, * which then acted; FALSE otherwise */ boolean virtual_keyboard_click( short x, - short y); + short y, + long *hit); + +/** + * @brief Lists the rectangles that virtual_keyboard_click hit-tests, for + * the debug view of the touch targets (debug.touch_targets). + * @param rectangles receives the keys' rectangles, then the BACK and ENTER + * legends' + * @param maximum room in rectangles + * @return how many were written + */ +long virtual_keyboard_target_rectangles( + rectangle2d *rectangles, + long maximum); /* ---------- globals */ diff --git a/tools/mutate_touch_menu.py b/tools/mutate_touch_menu.py index 8ede58c4..e033d729 100644 --- a/tools/mutate_touch_menu.py +++ b/tools/mutate_touch_menu.py @@ -37,9 +37,21 @@ MUTANTS = [ "if (0)\n\t\treturn;\n\tmenu->finger_down = 1;"), ("down: finger_down not set", "menu->finger_down = 1;", ""), ("down: finger id not stored", "menu->finger = finger;", "(void)finger;"), + ("down: down_x not stored", "menu->down_x = menu->last_x = x;", "menu->last_x = x;"), + ("down: down_y not stored", "menu->down_y = menu->last_y = y;", "menu->last_y = y;"), ("down: remainder kept", "\tmenu->remainder = 0.0f;\n\t/* a finger", "\t/* a finger"), ("down: pending steps kept", "menu->pending_steps = 0;\n}\n\nvoid touch_menu_move", "}\n\nvoid touch_menu_move"), + ("down: downs++ -> = 1", "menu->output.downs++;", "menu->output.downs = 1;"), + ("down: downs not counted", "menu->output.downs++;", ""), + ("down: downs++ -> --", "menu->output.downs++;", "menu->output.downs--;"), + ("down: down_x not reported", "menu->output.down_x = x;", ""), + ("down: down_y not reported", "menu->output.down_y = y;", ""), + ("down: reported x is y", "menu->output.down_x = x;", "menu->output.down_x = y;"), + ("down: reported y is x", "menu->output.down_y = y;", "menu->output.down_y = x;"), + ("cancel: downs kept", "\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}\n\nvoid touch_menu_read", + "\tmenu->pending_steps = 0;\n}\n\nvoid touch_menu_read"), + ("read: downs kept", "\tmenu->output.clicks = 0;\n\tmenu->output.downs = 0;", "\tmenu->output.clicks = 0;"), # move guards ("move: guard || -> &&", GUARD + "\tif (!menu->scrolling)", GUARD.replace("||", "&&") + "\tif (!menu->scrolling)"), @@ -137,10 +149,10 @@ MUTANTS = [ # cancel ("cancel: finger stays down", "void touch_menu_cancel(struct touch_menu *menu)\n{\n\tmenu->finger_down = 0;\n", "void touch_menu_cancel(struct touch_menu *menu)\n{\n"), - ("cancel: pending steps kept", "\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n}", - "\tmenu->remainder = 0.0f;\n}"), - ("cancel: unread tap dropped too", "menu->pending_steps = 0;\n}\n\nvoid touch_menu_read", - "menu->pending_steps = 0;\n\tmenu->output.clicks = 0;\n\tmenu->output.moved = 0;\n}\n\nvoid touch_menu_read"), + ("cancel: pending steps kept", "\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", + "\tmenu->remainder = 0.0f;\n\tmenu->output.downs = 0;\n}"), + ("cancel: unread tap dropped too", "menu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}\n\nvoid touch_menu_read", + "menu->pending_steps = 0;\n\tmenu->output.downs = 0;\n\tmenu->output.clicks = 0;\n\tmenu->output.moved = 0;\n}\n\nvoid touch_menu_read"), # read ("read: cap > -> <", "if (steps > limit)", "if (steps < limit)"), ("read: upper cap dropped", "\tif (steps > limit)\n\t\tsteps = limit;\n", ""), @@ -186,8 +198,8 @@ EQUIVALENT = [ "menu->finger_down = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;", "same as up: scrolling is read only while finger_down is set, and down clears it"), ("cancel: remainder not cleared", - "menu->scrolling = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n}", - "menu->scrolling = 0;\n\tmenu->pending_steps = 0;\n}", + "menu->scrolling = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", + "menu->scrolling = 0;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", "remainder is read only by move, which needs finger_down, and down zeroes it"), ("edge: dx >= 0 for the sign of the x edge", "(dx > 0.0f ?", "(dx >= 0.0f ?", "the x edge is computed only when |dx| > |dy| >= 0, so dx is never 0 there"), From f32a6a791e05b3c258b1c4b9dbde583cc667f264 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Fri, 2 Oct 2026 07:15:49 +0200 Subject: [PATCH 005/187] Ignore touches in the system gesture zones --- port/android/README.md | 6 +- .../java/com/halo/decomp/HaloActivity.java | 23 + port/android/host/host_main.c | 59 +++ port/linux/src/touch_input.c | 36 +- port/linux/src/touch_menu.c | 47 +- port/linux/src/touch_menu.h | 17 + port/linux/tests/touch_menu_test.c | 465 +++++++++++++++++- tools/mutate_touch_menu.py | 78 ++- 8 files changed, 708 insertions(+), 23 deletions(-) diff --git a/port/android/README.md b/port/android/README.md index a4628c36..26670def 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -99,8 +99,10 @@ setting with values, tap its left or right half), tap a button of the key at the bottom of a screen (for example "B = Back") to push it, and drag to scroll a list (down or right steps back, up or left steps forward). A drag stops at the first and the last item, and it does not change a setting's -value. Apart from skipping cinematics, the gameplay does not accept touch -input. +value. Touches that start in the edge-gesture zones of Android do not tap +or scroll at the sides, and do not scroll at the top and bottom, because the +first swipe from an edge in full screen only shows the system bars. Apart +from skipping cinematics, the gameplay does not accept touch input. A tap during a cinematic that can be skipped skips it, as A does. On the on-screen keyboard, tap a key to press it, "B =BACK" to cancel and diff --git a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java index 3e4497df..9853192c 100644 --- a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java +++ b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java @@ -3,7 +3,10 @@ package com.halo.decomp; import android.content.Context; import android.net.wifi.WifiManager; import android.os.Bundle; +import android.graphics.Insets; +import android.os.Build; import android.view.Display; +import android.view.WindowInsets; import android.view.WindowManager; import org.libsdl.app.SDLActivity; @@ -39,6 +42,26 @@ public class HaloActivity extends SDLActivity { super.onDestroy(); } + /** + * The edges of the screen where Android keeps its gestures. In sticky + * full screen the first swipe from an edge only shows the system bars, + * and Android hands that swipe to the game as an ordinary finger, so the + * game (port/linux/src/touch_input.c) must ignore touches that begin + * there. The game's thread calls this through JNI at startup. + * + * @return {left, top, right, bottom} in pixels; all 0 before Android 10, + * which has no such insets, and while the window has none yet + */ + public int[] getSystemGestureInsetsPixels() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) + return new int[] { 0, 0, 0, 0 }; + WindowInsets root = getWindow().getDecorView().getRootWindowInsets(); + if (root == null) + return new int[] { 0, 0, 0, 0 }; + Insets gesture = root.getSystemGestureInsets(); + return new int[] { gesture.left, gesture.top, gesture.right, gesture.bottom }; + } + /** * Many phones drop the Wi-Fi's broadcast and multicast datagrams to * save power unless an app holds this: without it they would not see diff --git a/port/android/host/host_main.c b/port/android/host/host_main.c index d4a778de..d7ca54da 100644 --- a/port/android/host/host_main.c +++ b/port/android/host/host_main.c @@ -25,6 +25,7 @@ that runs here. #include #include #include +#include #include #include #include @@ -135,6 +136,51 @@ struct environment int count; }; +/** + * @brief Asks the activity for Android's system gesture insets, in pixels + * (HaloActivity.getSystemGestureInsetsPixels), as the value of + * HALO_GESTURE_INSETS. The method is found on the activity's own class: the + * game's native thread has the system's class loader, which does not know + * the app's classes. + * @param text receives "left,top,right,bottom" + * @param size the size of text + * @return 1 when text is set, 0 on any failure (the touch controls then use + * the whole screen) + */ +static int gesture_insets(char *text, size_t size) +{ + JNIEnv *env = (JNIEnv *)SDL_GetAndroidJNIEnv(); + jobject activity = (jobject)SDL_GetAndroidActivity(); + jint values[4] = { 0, 0, 0, 0 }; + int ok = 0; + + if (env && activity) + { + jclass activity_class = (*env)->GetObjectClass(env, activity); + jmethodID method = activity_class ? (*env)->GetMethodID(env, activity_class, "getSystemGestureInsetsPixels", "()[I") : NULL; + jintArray array = method ? (jintArray)(*env)->CallObjectMethod(env, activity, method) : NULL; + + if (array && !(*env)->ExceptionCheck(env) && (*env)->GetArrayLength(env, array) == 4) + { + (*env)->GetIntArrayRegion(env, array, 0, 4, values); + ok = !(*env)->ExceptionCheck(env); + } + /* a Java exception left pending would break the next JNI call of the + thread */ + if ((*env)->ExceptionCheck(env)) + (*env)->ExceptionClear(env); + if (array) + (*env)->DeleteLocalRef(env, array); + if (activity_class) + (*env)->DeleteLocalRef(env, activity_class); + } + if (env && activity) + (*env)->DeleteLocalRef(env, activity); + if (ok) + snprintf(text, size, "%d,%d,%d,%d", (int)values[0], (int)values[1], (int)values[2], (int)values[3]); + return ok; +} + static void environment_set(struct environment *environment, const char *name, const char *value) { size_t length = strlen(name); @@ -298,6 +344,19 @@ static void *game_main(void *unused) host_logf(HOST_LOG_INFO, "display density %s", text); } } + { + /* edges where Android keeps its gestures: touches that begin there + are ignored (port/linux/src/touch_input.c) */ + char insets[64]; + + if (gesture_insets(insets, sizeof(insets))) + { + environment_set(&environment, "HALO_GESTURE_INSETS", insets); + host_logf(HOST_LOG_INFO, "system gesture insets %s", insets); + } + else + host_log(HOST_LOG_INFO, "system gesture insets unavailable"); + } } time_zone(zone, sizeof(zone)); environment_set(&environment, "TZ", zone); diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index 7bd5047b..74268511 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -4,12 +4,15 @@ TOUCH_INPUT.C The touchscreen of the Android build. SDL's finger events become the menus' pointer (touch_menu.c) while a menu is up, as the desktop mouse does (sdl_platform.c, platform_ui_pointer_read). Sizes are in dp, from the -display density the app passes (HALO_DISPLAY_DENSITY, host_main.c). +display density the app passes (HALO_DISPLAY_DENSITY, host_main.c), and the +edges of the screen where Android keeps its gestures +(HALO_GESTURE_INSETS, host_main.c). */ #include "touch_input.h" #include "touch_menu.h" +#include #include /* the game's (port/linux/game/touch_game.c) */ @@ -48,9 +51,28 @@ static float pixels_per_dp(void) return height > 0 ? (float)height / 360.0f : 0.0f; } +/** + * @brief Reads the system gesture insets the app passes. + * @param left,top,right,bottom receive the insets in pixels; all 0 when the + * variable is missing or malformed, so that the whole screen works + */ +static void gesture_insets(float *left, float *top, float *right, float *bottom) +{ + const char *text = getenv("HALO_GESTURE_INSETS"); + int l, t, r, b; + + *left = *top = *right = *bottom = 0.0f; + if (!text || sscanf(text, "%d,%d,%d,%d", &l, &t, &r, &b) != 4 || l < 0 || t < 0 || r < 0 || b < 0) + return; + *left = (float)l; + *top = (float)t; + *right = (float)r; + *bottom = (float)b; +} + /** * @brief Sets the gestures' sizes up, once they can be known; until then a - * dp is a pixel and the next call tries again. + * dp is a pixel, there are no gesture zones and the next call tries again. */ static void menu_setup(void) { @@ -58,12 +80,20 @@ static void menu_setup(void) { struct touch_menu_settings settings; float dp = pixels_per_dp(); + int width = 0, height = 0; + + platform_video_drawable_size(&width, &height); settings.slop = TOUCH_TAP_SLOP_DP * (dp > 0.0f ? dp : 1.0f); settings.step = TOUCH_SCROLL_STEP_DP * (dp > 0.0f ? dp : 1.0f); settings.steps_per_read = TOUCH_STEPS_PER_READ; + /* the zones are measured against the window: without its size the + right and bottom zones would be set up against 0 */ + gesture_insets(&settings.edge_left, &settings.edge_top, &settings.edge_right, &settings.edge_bottom); + settings.width = (float)width; + settings.height = (float)height; touch_menu_init(&menu, &settings); - menu_ready = dp > 0.0f; + menu_ready = dp > 0.0f && width > 0 && height > 0; } } diff --git a/port/linux/src/touch_menu.c b/port/linux/src/touch_menu.c index 15936b7b..95e43210 100644 --- a/port/linux/src/touch_menu.c +++ b/port/linux/src/touch_menu.c @@ -22,10 +22,42 @@ void touch_menu_reset(struct touch_menu *menu) touch_menu_init(menu, &settings); } +/** + * @brief Whether x is in the left or right gesture zone: [0, edge_left) and + * [width - edge_right, width). Without a window size the right one does not + * exist, since it would otherwise start at a negative x and cover the screen. + * @param menu the state (the zones are in its settings) + * @param x the finger's x, in pixels + */ +static int in_side_zone(const struct touch_menu *menu, float x) +{ + const struct touch_menu_settings *s = &menu->settings; + + return x < s->edge_left || (s->width > 0.0f && x >= s->width - s->edge_right); +} + +/** + * @brief Whether y is in the top or bottom gesture zone; as the side zones, + * the bottom one needs the window's height. + * @param menu the state (the zones are in its settings) + * @param y the finger's y, in pixels + */ +static int in_top_or_bottom_zone(const struct touch_menu *menu, float y) +{ + const struct touch_menu_settings *s = &menu->settings; + + return y < s->edge_top || (s->height > 0.0f && y >= s->height - s->edge_bottom); +} + void touch_menu_down(struct touch_menu *menu, unsigned long long finger, float x, float y) { if (menu->finger_down) return; + /* no state for an ignored finger: its moves and its lift find no finger + down, or another finger's id, and do nothing. It must not set + finger_down, or it would shut out the next finger. */ + if (in_side_zone(menu, x)) + return; menu->finger_down = 1; menu->finger = finger; menu->down_x = menu->last_x = x; @@ -34,6 +66,8 @@ void touch_menu_down(struct touch_menu *menu, unsigned long long finger, float x menu->output.down_x = x; menu->output.down_y = y; menu->scrolling = 0; + menu->no_scroll = in_top_or_bottom_zone(menu, y); + menu->void_tap = 0; menu->remainder = 0.0f; /* a finger on the screen stops a list still scrolling, so that a tap hits what the finger sees */ @@ -52,6 +86,15 @@ void touch_menu_move(struct touch_menu *menu, unsigned long long finger, float x if (dx * dx + dy * dy <= menu->settings.slop * menu->settings.slop) return; + /* a finger from the top or bottom zone may be Android's own edge swipe, + which reaches the game too. It may tap (the legends sit in the bottom + zone), but beyond the slop it is neither a scroll nor a tap, so that + its lift does not click where it began. */ + if (menu->no_scroll) + { + menu->void_tap = 1; + return; + } /* the steps count from the slop's edge on the axis: the slop is not a step, and a fast first sample is not lost */ menu->scrolling = 1; @@ -82,7 +125,7 @@ void touch_menu_up(struct touch_menu *menu, unsigned long long finger, float x, if (!menu->finger_down || finger != menu->finger) return; touch_menu_move(menu, finger, x, y); - if (!menu->scrolling) + if (!menu->scrolling && !menu->void_tap) { menu->output.moved = 1; menu->output.x = menu->output.click_x = menu->down_x; @@ -91,12 +134,14 @@ void touch_menu_up(struct touch_menu *menu, unsigned long long finger, float x, } menu->finger_down = 0; menu->scrolling = 0; + menu->void_tap = 0; } void touch_menu_cancel(struct touch_menu *menu) { menu->finger_down = 0; menu->scrolling = 0; + menu->void_tap = 0; menu->remainder = 0.0f; menu->pending_steps = 0; menu->output.downs = 0; diff --git a/port/linux/src/touch_menu.h b/port/linux/src/touch_menu.h index 7f111146..574f179a 100644 --- a/port/linux/src/touch_menu.h +++ b/port/linux/src/touch_menu.h @@ -27,6 +27,19 @@ struct touch_menu_settings /* at most this many steps per read: the rest waits, so that a fast swipe moves a list gradually, as fast as the menus take the steps */ int steps_per_read; + /* Android's system gesture zones, in pixels of the window (all 0: none). + With the system bars hidden the first edge swipe only shows them, and + Android hands that swipe to the game as an ordinary finger, so a finger + that begins in a zone must not act as a menu touch: + - in the left or right zone it is ignored for its whole life, a corner + included; it does not keep another finger from being used; + - in the top or bottom zone it still taps (the A/B legends sit in the + bottom zone) but never scrolls: once it moves beyond the slop it is + neither a tap nor a scroll. */ + float edge_left, edge_top, edge_right, edge_bottom; + /* the window's size, for the right and bottom zones: 0 means those two + zones do not exist, so that a size not yet known cannot fill the screen */ + float width, height; }; struct touch_menu_output @@ -55,6 +68,10 @@ struct touch_menu float down_x, down_y; float last_x, last_y; int scrolling; + /* the finger began in the top or bottom zone: it may tap, not scroll */ + int no_scroll; + /* a no_scroll finger moved beyond the slop: no tap and no scroll */ + int void_tap; /* 0: vertical, 1: horizontal */ int axis; /* drag not yet a whole step */ diff --git a/port/linux/tests/touch_menu_test.c b/port/linux/tests/touch_menu_test.c index 25a6671c..0d4711c5 100644 --- a/port/linux/tests/touch_menu_test.c +++ b/port/linux/tests/touch_menu_test.c @@ -25,11 +25,31 @@ static int failures; test the limit (the game uses 1) */ static void setup(struct touch_menu *menu) { - struct touch_menu_settings settings = { 36.0f, 120.0f, 2 }; + struct touch_menu_settings settings = { 36.0f, 120.0f, 2, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; touch_menu_init(menu, &settings); } +/* the S23's system gesture insets, as dumpsys window reports them, on its +2340x1080 window: 90 px at the sides, 40 above (the status bar), 96 below */ +static void setup_zones(struct touch_menu *menu) +{ + struct touch_menu_settings settings = { 36.0f, 120.0f, 2, 90.0f, 40.0f, 90.0f, 96.0f, 2340.0f, 1080.0f }; + + touch_menu_init(menu, &settings); +} + +/* a finger from (x0, y0) to (x1, y1) in 12 samples, lifted at the end */ +static void swipe(struct touch_menu *menu, unsigned long long finger, float x0, float y0, float x1, float y1) +{ + int i; + + touch_menu_down(menu, finger, x0, y0); + for (i = 1; i <= 12; i++) + touch_menu_move(menu, finger, x0 + (x1 - x0) * i / 12.0f, y0 + (y1 - y0) * i / 12.0f); + touch_menu_up(menu, finger, x1, y1); +} + /* first, so that a unit that lost its settings (a step of 0 never ends its loops) fails here and not by hanging a later test */ static void test_init_stores_the_settings(void) @@ -40,6 +60,16 @@ static void test_init_stores_the_settings(void) CHECK(menu.settings.slop == 36.0f); CHECK(menu.settings.step == 120.0f); CHECK(menu.settings.steps_per_read == 2); + CHECK(menu.settings.edge_left == 0.0f && menu.settings.edge_top == 0.0f); + CHECK(menu.settings.edge_right == 0.0f && menu.settings.edge_bottom == 0.0f); + CHECK(menu.settings.width == 0.0f && menu.settings.height == 0.0f); + setup_zones(&menu); + CHECK(menu.settings.edge_left == 90.0f); + CHECK(menu.settings.edge_top == 40.0f); + CHECK(menu.settings.edge_right == 90.0f); + CHECK(menu.settings.edge_bottom == 96.0f); + CHECK(menu.settings.width == 2340.0f); + CHECK(menu.settings.height == 1080.0f); } static void test_tap_clicks_where_the_finger_went_down(void) @@ -506,7 +536,7 @@ static void test_one_step_a_read(void) { struct touch_menu menu; struct touch_menu_output out; - struct touch_menu_settings settings = { 36.0f, 120.0f, 1 }; + struct touch_menu_settings settings = { 36.0f, 120.0f, 1, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; int index; touch_menu_init(&menu, &settings); @@ -608,6 +638,416 @@ static void test_reset_and_cancel_drop_the_unread_downs(void) CHECK(out.downs == 0); } +static void test_a_tap_in_the_left_zone_is_ignored(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 10.0f, 500.0f); + touch_menu_up(&menu, 1, 10.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + CHECK(out.moved == 0); + CHECK(out.downs == 0); + CHECK(out.wheel_steps == 0); +} + +static void test_the_left_zone_ends_at_the_inset(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 89.0f, 500.0f); + touch_menu_up(&menu, 1, 89.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + CHECK(out.downs == 0); + touch_menu_down(&menu, 1, 90.0f, 500.0f); + touch_menu_up(&menu, 1, 90.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.downs == 1); + CHECK(out.click_x == 90.0f); +} + +static void test_the_right_zone_begins_at_width_minus_the_inset(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 2250.0f, 500.0f); + touch_menu_up(&menu, 1, 2250.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + CHECK(out.downs == 0); + touch_menu_down(&menu, 1, 2249.0f, 500.0f); + touch_menu_up(&menu, 1, 2249.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.downs == 1); + CHECK(out.click_x == 2249.0f); +} + +/* the reported bug: the first edge swipe in immersive mode scrolled the list */ +static void test_an_edge_swipe_does_not_scroll_or_click(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + int i, steps = 0; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 10.0f, 500.0f); + for (i = 1; i <= 14; i++) + { + touch_menu_move(&menu, 1, 10.0f + 50.0f * i, 500.0f); + touch_menu_read(&menu, &out); + steps += out.wheel_steps; + CHECK(out.clicks == 0); + } + touch_menu_up(&menu, 1, 710.0f, 500.0f); + touch_menu_read(&menu, &out); + steps += out.wheel_steps; + CHECK(steps == 0); + CHECK(out.clicks == 0); + CHECK(out.moved == 0); + CHECK(out.downs == 0); +} + +static void test_an_ignored_finger_does_not_stick(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + swipe(&menu, 1, 10.0f, 500.0f, 700.0f, 500.0f); + touch_menu_read(&menu, &out); + touch_menu_down(&menu, 1, 1170.0f, 540.0f); + touch_menu_up(&menu, 1, 1170.0f, 540.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.downs == 1); +} + +static void test_an_edge_finger_does_not_block_a_second_finger(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 10.0f, 500.0f); + touch_menu_down(&menu, 2, 1170.0f, 540.0f); + touch_menu_up(&menu, 2, 1170.0f, 540.0f); + touch_menu_up(&menu, 1, 10.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.downs == 1); + CHECK(out.click_x == 1170.0f); +} + +/* an edge finger must not stop the steps of a scroll by another finger */ +static void test_an_edge_finger_leaves_another_fingers_scroll_alone(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 2, 1170.0f, 800.0f); + touch_menu_move(&menu, 2, 1170.0f, 500.0f); + touch_menu_down(&menu, 1, 10.0f, 500.0f); + touch_menu_move(&menu, 1, 400.0f, 500.0f); + touch_menu_up(&menu, 1, 400.0f, 500.0f); + touch_menu_up(&menu, 2, 1170.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); + CHECK(out.clicks == 0); +} + +/* a finger in a zone is no finger on the screen: the list goes on scrolling */ +static void test_an_edge_finger_keeps_the_pending_steps(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + swipe(&menu, 1, 1170.0f, 800.0f, 1170.0f, 100.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); + touch_menu_down(&menu, 2, 10.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); +} + +static void test_a_tap_in_the_bottom_zone_clicks(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 1170.0f, 1050.0f); + touch_menu_up(&menu, 1, 1170.0f, 1050.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.downs == 1); + CHECK(out.click_x == 1170.0f && out.click_y == 1050.0f); +} + +static void test_a_swipe_up_from_the_bottom_zone_does_nothing(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + int i, steps = 0; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 1170.0f, 1060.0f); + for (i = 1; i <= 12; i++) + { + touch_menu_move(&menu, 1, 1170.0f, 1060.0f - 50.0f * i); + touch_menu_read(&menu, &out); + steps += out.wheel_steps; + } + touch_menu_up(&menu, 1, 1170.0f, 460.0f); + touch_menu_read(&menu, &out); + steps += out.wheel_steps; + CHECK(steps == 0); + CHECK(out.clicks == 0); + CHECK(out.moved == 0); +} + +static void test_a_swipe_down_from_the_top_zone_does_nothing(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + int i, steps = 0; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 1170.0f, 10.0f); + for (i = 1; i <= 12; i++) + { + touch_menu_move(&menu, 1, 1170.0f, 10.0f + 50.0f * i); + touch_menu_read(&menu, &out); + steps += out.wheel_steps; + } + touch_menu_up(&menu, 1, 1170.0f, 610.0f); + touch_menu_read(&menu, &out); + steps += out.wheel_steps; + CHECK(steps == 0); + CHECK(out.clicks == 0); + CHECK(out.moved == 0); +} + +/* a sample past the slop voids the tap for good: coming back to the start +must not bring it back */ +static void test_a_no_scroll_finger_stays_void_after_returning(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 1170.0f, 1050.0f); + touch_menu_move(&menu, 1, 1170.0f, 900.0f); + touch_menu_up(&menu, 1, 1170.0f, 1050.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + CHECK(out.moved == 0); +} + +/* a no-scroll finger may still wobble inside the slop and tap */ +static void test_a_no_scroll_finger_taps_within_the_slop(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 1170.0f, 1050.0f); + touch_menu_move(&menu, 1, 1190.0f, 1040.0f); + touch_menu_up(&menu, 1, 1190.0f, 1040.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.click_x == 1170.0f && out.click_y == 1050.0f); +} + +static void test_just_outside_the_zones_scrolls_as_usual(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 1170.0f, 900.0f); + touch_menu_move(&menu, 1, 1170.0f, 840.0f); + touch_menu_move(&menu, 1, 1170.0f, 300.0f); + touch_menu_up(&menu, 1, 1170.0f, 300.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); + CHECK(out.clicks == 0); +} + +static void test_the_bottom_and_top_zones_end_at_their_insets(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + /* 1080 - 96 = 984 is the first y of the bottom zone */ + setup_zones(&menu); + swipe(&menu, 1, 1170.0f, 984.0f, 1170.0f, 300.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); + swipe(&menu, 1, 1170.0f, 983.0f, 1170.0f, 300.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); + /* 39 is the last y of the top zone */ + setup_zones(&menu); + swipe(&menu, 1, 1170.0f, 39.0f, 1170.0f, 700.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); + swipe(&menu, 1, 1170.0f, 40.0f, 1170.0f, 700.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 2); +} + +/* a no-scroll finger must not leave its state to the next finger */ +static void test_a_scroll_after_a_no_scroll_finger_still_scrolls(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + swipe(&menu, 1, 1170.0f, 1060.0f, 1170.0f, 700.0f); + touch_menu_read(&menu, &out); + swipe(&menu, 1, 1170.0f, 800.0f, 1170.0f, 300.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); + CHECK(out.clicks == 0); +} + +/* the side zones win over the bottom: a corner touch is ignored */ +static void test_a_corner_touch_is_ignored(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 10.0f, 1060.0f); + touch_menu_up(&menu, 1, 10.0f, 1060.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + CHECK(out.downs == 0); + touch_menu_down(&menu, 1, 2300.0f, 10.0f); + touch_menu_up(&menu, 1, 2300.0f, 10.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + CHECK(out.downs == 0); +} + +/* no insets (button navigation, desktop, unreadable): the edges count */ +static void test_without_zones_the_edges_work(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup(&menu); + touch_menu_down(&menu, 1, 0.0f, 500.0f); + touch_menu_up(&menu, 1, 0.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + touch_menu_down(&menu, 1, 500.0f, 0.0f); + touch_menu_up(&menu, 1, 500.0f, 0.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + swipe(&menu, 1, 0.0f, 500.0f, 500.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 2); + CHECK(out.clicks == 0); +} + +/* insets without a window size: the right and bottom zones do not exist */ +static void test_without_a_size_only_the_left_and_top_zones_exist(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + struct touch_menu_settings settings = { 36.0f, 120.0f, 2, 90.0f, 40.0f, 90.0f, 96.0f, 0.0f, 0.0f }; + + touch_menu_init(&menu, &settings); + touch_menu_down(&menu, 1, 10.0f, 500.0f); + touch_menu_up(&menu, 1, 10.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + swipe(&menu, 1, 5000.0f, 2000.0f, 5000.0f, 1000.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); +} + +/* the S23's insets are symmetric, so only unequal ones tell the two sides +(and the top and the bottom) apart */ +static void test_each_side_has_its_own_inset(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + struct touch_menu_settings settings = { 36.0f, 120.0f, 2, 30.0f, 20.0f, 200.0f, 150.0f, 2340.0f, 1080.0f }; + + touch_menu_init(&menu, &settings); + touch_menu_down(&menu, 1, 29.0f, 500.0f); + touch_menu_up(&menu, 1, 29.0f, 500.0f); + touch_menu_down(&menu, 1, 2140.0f, 500.0f); + touch_menu_up(&menu, 1, 2140.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + touch_menu_down(&menu, 1, 30.0f, 500.0f); + touch_menu_up(&menu, 1, 30.0f, 500.0f); + touch_menu_down(&menu, 1, 2139.0f, 500.0f); + touch_menu_up(&menu, 1, 2139.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 2); + /* above 1080 - 150 = 930 and from 20 down a swipe scrolls; the top + zone is 20 high and the bottom one 150 */ + swipe(&menu, 1, 1170.0f, 929.0f, 1170.0f, 300.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); + swipe(&menu, 1, 1170.0f, 930.0f, 1170.0f, 300.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); + swipe(&menu, 1, 1170.0f, 19.0f, 1170.0f, 700.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 0); + swipe(&menu, 1, 1170.0f, 20.0f, 1170.0f, 700.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == 2); +} + +static void test_reset_and_cancel_drop_a_zone_finger(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 10.0f, 500.0f); + touch_menu_reset(&menu); + touch_menu_down(&menu, 2, 1170.0f, 540.0f); + touch_menu_up(&menu, 2, 1170.0f, 540.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + /* a bottom finger that moved on is void; after a cancel the next + finger taps */ + touch_menu_down(&menu, 1, 1170.0f, 1060.0f); + touch_menu_move(&menu, 1, 1170.0f, 800.0f); + touch_menu_cancel(&menu); + touch_menu_down(&menu, 2, 1170.0f, 540.0f); + touch_menu_up(&menu, 2, 1170.0f, 540.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.downs == 1); + /* and after a reset */ + touch_menu_down(&menu, 1, 1170.0f, 1060.0f); + touch_menu_move(&menu, 1, 1170.0f, 800.0f); + touch_menu_reset(&menu); + touch_menu_down(&menu, 2, 1170.0f, 540.0f); + touch_menu_up(&menu, 2, 1170.0f, 540.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); +} + int main(void) { test_init_stores_the_settings(); @@ -648,6 +1088,27 @@ int main(void) test_two_downs_between_reads_count_twice(); test_a_second_finger_is_no_down(); test_reset_and_cancel_drop_the_unread_downs(); + test_a_tap_in_the_left_zone_is_ignored(); + test_the_left_zone_ends_at_the_inset(); + test_the_right_zone_begins_at_width_minus_the_inset(); + test_an_edge_swipe_does_not_scroll_or_click(); + test_an_ignored_finger_does_not_stick(); + test_an_edge_finger_does_not_block_a_second_finger(); + test_an_edge_finger_leaves_another_fingers_scroll_alone(); + test_an_edge_finger_keeps_the_pending_steps(); + test_a_tap_in_the_bottom_zone_clicks(); + test_a_swipe_up_from_the_bottom_zone_does_nothing(); + test_a_swipe_down_from_the_top_zone_does_nothing(); + test_a_no_scroll_finger_stays_void_after_returning(); + test_a_no_scroll_finger_taps_within_the_slop(); + test_just_outside_the_zones_scrolls_as_usual(); + test_the_bottom_and_top_zones_end_at_their_insets(); + test_a_scroll_after_a_no_scroll_finger_still_scrolls(); + test_a_corner_touch_is_ignored(); + test_without_zones_the_edges_work(); + test_without_a_size_only_the_left_and_top_zones_exist(); + test_each_side_has_its_own_inset(); + test_reset_and_cancel_drop_a_zone_finger(); if (failures) printf("%d failures\n", failures); else diff --git a/tools/mutate_touch_menu.py b/tools/mutate_touch_menu.py index e033d729..dd8a990e 100644 --- a/tools/mutate_touch_menu.py +++ b/tools/mutate_touch_menu.py @@ -33,8 +33,8 @@ MUTANTS = [ ("reset: settings lost", "struct touch_menu_settings settings = menu->settings;", "struct touch_menu_settings settings = {0};"), # down - ("down: second finger accepted", "if (menu->finger_down)\n\t\treturn;\n\tmenu->finger_down = 1;", - "if (0)\n\t\treturn;\n\tmenu->finger_down = 1;"), + ("down: second finger accepted", "if (menu->finger_down)\n\t\treturn;\n\t/* no state", + "if (0)\n\t\treturn;\n\t/* no state"), ("down: finger_down not set", "menu->finger_down = 1;", ""), ("down: finger id not stored", "menu->finger = finger;", "(void)finger;"), ("down: down_x not stored", "menu->down_x = menu->last_x = x;", "menu->last_x = x;"), @@ -128,9 +128,9 @@ MUTANTS = [ ("up: finger id ignored", GUARD + "\ttouch_menu_move", "if (!menu->finger_down)\n\t\treturn;\n\ttouch_menu_move"), ("up: finger_down ignored", GUARD + "\ttouch_menu_move", "if (finger != menu->finger)\n\t\treturn;\n\ttouch_menu_move"), ("up: != -> ==", GUARD + "\ttouch_menu_move", GUARD.replace("!=", "==") + "\ttouch_menu_move"), - ("up: final move dropped", "\ttouch_menu_move(menu, finger, x, y);\n\tif (!menu->scrolling)", "\t(void)x;\n\t(void)y;\n\tif (!menu->scrolling)"), - ("up: tap test inverted", "\tif (!menu->scrolling)\n\t{\n\t\tmenu->output.moved", "\tif (menu->scrolling)\n\t{\n\t\tmenu->output.moved"), - ("up: tap always", "\tif (!menu->scrolling)\n\t{\n\t\tmenu->output.moved", "\tif (1)\n\t{\n\t\tmenu->output.moved"), + ("up: final move dropped", "\ttouch_menu_move(menu, finger, x, y);\n\tif (!menu->scrolling &&", "\t(void)x;\n\t(void)y;\n\tif (!menu->scrolling &&"), + ("up: tap test inverted", "\tif (!menu->scrolling && !menu->void_tap)\n\t{\n\t\tmenu->output.moved", "\tif (menu->scrolling)\n\t{\n\t\tmenu->output.moved"), + ("up: tap always", "\tif (!menu->scrolling && !menu->void_tap)\n\t{\n\t\tmenu->output.moved", "\tif (1)\n\t{\n\t\tmenu->output.moved"), ("up: moved not set", "menu->output.moved = 1;", ""), ("up: moved = 0", "menu->output.moved = 1;", "menu->output.moved = 0;"), ("up: x from the lift", CLICK_X, CLICK_X.replace("menu->down_x", "x")), @@ -144,8 +144,8 @@ MUTANTS = [ ("up: clicks++ -> = 1", "menu->output.clicks++;", "menu->output.clicks = 1;"), ("up: clicks not counted", "menu->output.clicks++;", ""), ("up: clicks++ -> --", "menu->output.clicks++;", "menu->output.clicks--;"), - ("up: finger stays down", "\tmenu->finger_down = 0;\n\tmenu->scrolling = 0;\n}\n\nvoid touch_menu_cancel", - "\tmenu->scrolling = 0;\n}\n\nvoid touch_menu_cancel"), + ("up: finger stays down", "\tmenu->finger_down = 0;\n\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel", + "\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel"), # cancel ("cancel: finger stays down", "void touch_menu_cancel(struct touch_menu *menu)\n{\n\tmenu->finger_down = 0;\n", "void touch_menu_cancel(struct touch_menu *menu)\n{\n"), @@ -153,6 +153,42 @@ MUTANTS = [ "\tmenu->remainder = 0.0f;\n\tmenu->output.downs = 0;\n}"), ("cancel: unread tap dropped too", "menu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}\n\nvoid touch_menu_read", "menu->pending_steps = 0;\n\tmenu->output.downs = 0;\n\tmenu->output.clicks = 0;\n\tmenu->output.moved = 0;\n}\n\nvoid touch_menu_read"), + # gesture zones + ("zone: left < -> <=", "return x < s->edge_left ||", "return x <= s->edge_left ||"), + ("zone: left check removed", "return x < s->edge_left ||", "return 0 ||"), + ("zone: left uses the right inset", "return x < s->edge_left ||", "return x < s->edge_right ||"), + ("zone: right >= -> >", "x >= s->width - s->edge_right", "x > s->width - s->edge_right"), + ("zone: right check removed", " || (s->width > 0.0f && x >= s->width - s->edge_right)", ""), + ("zone: right without the width guard", "(s->width > 0.0f && x >= s->width - s->edge_right)", + "(x >= s->width - s->edge_right)"), + ("zone: right uses the left inset", "x >= s->width - s->edge_right", "x >= s->width - s->edge_left"), + ("zone: top < -> <=", "return y < s->edge_top ||", "return y <= s->edge_top ||"), + ("zone: top check removed", "return y < s->edge_top ||", "return 0 ||"), + ("zone: top uses the bottom inset", "return y < s->edge_top ||", "return y < s->edge_bottom ||"), + ("zone: bottom >= -> >", "y >= s->height - s->edge_bottom", "y > s->height - s->edge_bottom"), + ("zone: bottom check removed", " || (s->height > 0.0f && y >= s->height - s->edge_bottom)", ""), + ("zone: bottom without the height guard", "(s->height > 0.0f && y >= s->height - s->edge_bottom)", + "(y >= s->height - s->edge_bottom)"), + ("zone: bottom uses the top inset", "y >= s->height - s->edge_bottom", "y >= s->height - s->edge_top"), + ("down: side zone finger accepted", "\tif (in_side_zone(menu, x))\n\t\treturn;\n", + "\tif (0 && in_side_zone(menu, x))\n\t\treturn;\n"), + ("down: side zone finger blocks the next", + "\tif (in_side_zone(menu, x))\n\t\treturn;\n", + "\tif (in_side_zone(menu, x))\n\t{\n\t\tmenu->finger_down = 1;\n\t\tmenu->finger = finger;\n\t\treturn;\n\t}\n"), + ("down: side zone finger counts as a down", + "\tif (in_side_zone(menu, x))\n\t\treturn;\n", + "\tif (in_side_zone(menu, x))\n\t{\n\t\tmenu->output.downs++;\n\t\treturn;\n\t}\n"), + ("down: side zone finger drops the pending steps", + "\tif (in_side_zone(menu, x))\n\t\treturn;\n", + "\tif (in_side_zone(menu, x))\n\t{\n\t\tmenu->pending_steps = 0;\n\t\treturn;\n\t}\n"), + ("down: no_scroll never set", "menu->no_scroll = in_top_or_bottom_zone(menu, y);", "menu->no_scroll = 0 && in_top_or_bottom_zone(menu, y);"), + ("down: no_scroll always set", "menu->no_scroll = in_top_or_bottom_zone(menu, y);", "menu->no_scroll = 1 || in_top_or_bottom_zone(menu, y);"), + ("down: no_scroll from x", "menu->no_scroll = in_top_or_bottom_zone(menu, y);", "menu->no_scroll = in_top_or_bottom_zone(menu, x);"), + ("move: no-scroll finger scrolls", "\t\t\tmenu->void_tap = 1;\n\t\t\treturn;", "\t\t\tmenu->void_tap = 1;"), + ("move: no_scroll test removed", "\t\tif (menu->no_scroll)\n", "\t\tif (0)\n"), + ("move: no-scroll finger keeps its tap", "\t\t\tmenu->void_tap = 1;\n", ""), + ("up: no-scroll finger clicks", "if (!menu->scrolling && !menu->void_tap)", "if (!menu->scrolling)"), + ("up: void test inverted", "if (!menu->scrolling && !menu->void_tap)", "if (!menu->scrolling && menu->void_tap)"), # read ("read: cap > -> <", "if (steps > limit)", "if (steps < limit)"), ("read: upper cap dropped", "\tif (steps > limit)\n\t\tsteps = limit;\n", ""), @@ -185,21 +221,33 @@ EQUIVALENT = [ "the first move past the slop rewrites last on the scroll axis before it is read, " "and the other axis's last is never read; a move inside the slop reads neither"), ("down: scrolling not cleared", - "menu->scrolling = 0;\n\tmenu->remainder = 0.0f;\n\t/* a finger", - "menu->remainder = 0.0f;\n\t/* a finger", + "menu->scrolling = 0;\n\tmenu->no_scroll", + "menu->no_scroll", "scrolling is already 0 whenever finger_down is 0: init, up and cancel clear it, and down " "runs only when finger_down is 0"), ("up: scrolling not cleared", - "\tmenu->finger_down = 0;\n\tmenu->scrolling = 0;\n}\n\nvoid touch_menu_cancel", - "\tmenu->finger_down = 0;\n}\n\nvoid touch_menu_cancel", + "\tmenu->finger_down = 0;\n\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel", + "\tmenu->finger_down = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel", "scrolling is read only while finger_down is set, and down clears it before the next finger"), + ("up: void_tap not cleared", + "\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel", + "\tmenu->scrolling = 0;\n}\n\nvoid touch_menu_cancel", + "void_tap is read only while finger_down is set (up), and down clears it before the next finger"), ("cancel: scrolling not cleared", - "menu->finger_down = 0;\n\tmenu->scrolling = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;", - "menu->finger_down = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;", + "menu->finger_down = 0;\n\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n\tmenu->remainder = 0.0f;", + "menu->finger_down = 0;\n\tmenu->void_tap = 0;\n\tmenu->remainder = 0.0f;", "same as up: scrolling is read only while finger_down is set, and down clears it"), + ("cancel: void_tap not cleared", + "menu->scrolling = 0;\n\tmenu->void_tap = 0;\n\tmenu->remainder = 0.0f;", + "menu->scrolling = 0;\n\tmenu->remainder = 0.0f;", + "same as up: void_tap is read only while finger_down is set, and down clears it"), + ("down: void_tap not cleared", + "menu->void_tap = 0;\n\tmenu->remainder = 0.0f;\n\t/* a finger", + "menu->remainder = 0.0f;\n\t/* a finger", + "up and cancel already clear void_tap, and it is set only while a finger is down"), ("cancel: remainder not cleared", - "menu->scrolling = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", - "menu->scrolling = 0;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", + "menu->void_tap = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", + "menu->void_tap = 0;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", "remainder is read only by move, which needs finger_down, and down zeroes it"), ("edge: dx >= 0 for the sign of the x edge", "(dx > 0.0f ?", "(dx >= 0.0f ?", "the x edge is computed only when |dx| > |dy| >= 0, so dx is never 0 there"), From 430f3d8287dd5d685ecf74cdddb81f4c6ffeb506 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Fri, 2 Oct 2026 08:21:20 +0200 Subject: [PATCH 006/187] Read the gesture insets at every finger down --- .../java/com/halo/decomp/HaloActivity.java | 53 +++++++++++--- port/android/guest/runtime/guest_host.h | 4 + port/android/host/host_main.c | 55 ++++++-------- port/android/host_imports.list | 1 + port/linux/src/touch_input.c | 60 ++++++++------- port/linux/src/touch_menu.c | 11 +-- port/linux/src/touch_menu.h | 4 +- port/linux/tests/touch_menu_test.c | 73 +++++++++++++++++++ tools/mutate_touch_menu.py | 16 ++-- 9 files changed, 197 insertions(+), 80 deletions(-) diff --git a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java index 9853192c..b713ab61 100644 --- a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java +++ b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java @@ -6,6 +6,7 @@ import android.os.Bundle; import android.graphics.Insets; import android.os.Build; import android.view.Display; +import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; @@ -18,6 +19,12 @@ import org.libsdl.app.SDLActivity; public class HaloActivity extends SDLActivity { /** lets system link's broadcasts in over Wi-Fi while the game runs */ private WifiManager.MulticastLock multicastLock; + /** + * The latest system gesture insets {left, top, right, bottom}, in pixels + * of the current orientation. Written on the UI thread, read by the + * game's thread: replaced as a whole, never changed in place. + */ + private volatile int[] gestureInsets = new int[] { 0, 0, 0, 0 }; @Override protected String[] getLibraries() { @@ -29,6 +36,7 @@ public class HaloActivity extends SDLActivity { super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); preferHighestRefreshRate(); + trackGestureInsets(); acquireMulticastLock(); // a new version looked for while the game starts Updater.start(this); @@ -42,24 +50,49 @@ public class HaloActivity extends SDLActivity { super.onDestroy(); } + /** + * Keeps gestureInsets current. Android sends the insets again when the + * activity rotates, which happens after the game's native code has + * started on a phone launched from portrait, so a single read at startup + * would keep the portrait values. The listener hands the insets on, so + * that SDL's own handling (on its surface) still sees them. + */ + private void trackGestureInsets() { + getWindow().getDecorView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { + @Override + public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { + gestureInsets = readGestureInsets(insets); + return view.onApplyWindowInsets(insets); + } + }); + } + + @SuppressWarnings("deprecation") // getSystemGestureInsets is the only call on Android 10 + private static int[] readGestureInsets(WindowInsets insets) { + Insets gesture; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + gesture = insets.getInsets(WindowInsets.Type.systemGestures()); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + gesture = insets.getSystemGestureInsets(); + else + return new int[] { 0, 0, 0, 0 }; + return new int[] { gesture.left, gesture.top, gesture.right, gesture.bottom }; + } + /** * The edges of the screen where Android keeps its gestures. In sticky * full screen the first swipe from an edge only shows the system bars, * and Android hands that swipe to the game as an ordinary finger, so the * game (port/linux/src/touch_input.c) must ignore touches that begin - * there. The game's thread calls this through JNI at startup. + * there. The game's thread calls this through JNI at every finger down. * - * @return {left, top, right, bottom} in pixels; all 0 before Android 10, - * which has no such insets, and while the window has none yet + * @return a copy of {left, top, right, bottom} in pixels; all 0 before + * Android 10, which has no such insets, and until the first + * insets arrive */ public int[] getSystemGestureInsetsPixels() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) - return new int[] { 0, 0, 0, 0 }; - WindowInsets root = getWindow().getDecorView().getRootWindowInsets(); - if (root == null) - return new int[] { 0, 0, 0, 0 }; - Insets gesture = root.getSystemGestureInsets(); - return new int[] { gesture.left, gesture.top, gesture.right, gesture.bottom }; + return gestureInsets.clone(); } /** diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h index 2c18715a..f5225dc1 100644 --- a/port/android/guest/runtime/guest_host.h +++ b/port/android/guest/runtime/guest_host.h @@ -103,4 +103,8 @@ void host_gl_wait_frame(unsigned int slot); /* the storage directories the port uses, copied into buffer */ void host_android_path(int which, char *buffer, unsigned int size); +/* the edges where Android keeps its gestures, as left, top, right, bottom +in pixels of the current orientation, into insets[4]; all 0 when unknown */ +void host_gesture_insets(int *insets); + #endif diff --git a/port/android/host/host_main.c b/port/android/host/host_main.c index d7ca54da..593f238a 100644 --- a/port/android/host/host_main.c +++ b/port/android/host/host_main.c @@ -138,21 +138,21 @@ struct environment /** * @brief Asks the activity for Android's system gesture insets, in pixels - * (HaloActivity.getSystemGestureInsetsPixels), as the value of - * HALO_GESTURE_INSETS. The method is found on the activity's own class: the - * game's native thread has the system's class loader, which does not know - * the app's classes. - * @param text receives "left,top,right,bottom" - * @param size the size of text - * @return 1 when text is set, 0 on any failure (the touch controls then use - * the whole screen) + * (HaloActivity.getSystemGestureInsetsPixels). The guest calls it at every + * finger down: the insets change when the phone rotates. The method is + * found on the activity's own class: the game's native thread has the + * system's class loader, which does not know the app's classes. A change is + * logged, so that the log shows the value the game works with. + * @param insets receives left, top, right, bottom; all 0 on any failure (the + * touch controls then use the whole screen) */ -static int gesture_insets(char *text, size_t size) +void host_gesture_insets(int *insets) { + static int logged[4] = { -1, -1, -1, -1 }; JNIEnv *env = (JNIEnv *)SDL_GetAndroidJNIEnv(); jobject activity = (jobject)SDL_GetAndroidActivity(); jint values[4] = { 0, 0, 0, 0 }; - int ok = 0; + int index; if (env && activity) { @@ -161,24 +161,28 @@ static int gesture_insets(char *text, size_t size) jintArray array = method ? (jintArray)(*env)->CallObjectMethod(env, activity, method) : NULL; if (array && !(*env)->ExceptionCheck(env) && (*env)->GetArrayLength(env, array) == 4) - { (*env)->GetIntArrayRegion(env, array, 0, 4, values); - ok = !(*env)->ExceptionCheck(env); - } /* a Java exception left pending would break the next JNI call of the - thread */ + thread, and a failed read must leave zeros */ if ((*env)->ExceptionCheck(env)) + { (*env)->ExceptionClear(env); + for (index = 0; index < 4; index++) + values[index] = 0; + } if (array) (*env)->DeleteLocalRef(env, array); if (activity_class) (*env)->DeleteLocalRef(env, activity_class); - } - if (env && activity) (*env)->DeleteLocalRef(env, activity); - if (ok) - snprintf(text, size, "%d,%d,%d,%d", (int)values[0], (int)values[1], (int)values[2], (int)values[3]); - return ok; + } + for (index = 0; index < 4; index++) + insets[index] = (int)values[index]; + if (memcmp(logged, insets, sizeof(logged))) + { + memcpy(logged, insets, sizeof(logged)); + host_logf(HOST_LOG_INFO, "system gesture insets %d,%d,%d,%d", insets[0], insets[1], insets[2], insets[3]); + } } static void environment_set(struct environment *environment, const char *name, const char *value) @@ -344,19 +348,6 @@ static void *game_main(void *unused) host_logf(HOST_LOG_INFO, "display density %s", text); } } - { - /* edges where Android keeps its gestures: touches that begin there - are ignored (port/linux/src/touch_input.c) */ - char insets[64]; - - if (gesture_insets(insets, sizeof(insets))) - { - environment_set(&environment, "HALO_GESTURE_INSETS", insets); - host_logf(HOST_LOG_INFO, "system gesture insets %s", insets); - } - else - host_log(HOST_LOG_INFO, "system gesture insets unavailable"); - } } time_zone(zone, sizeof(zone)); environment_set(&environment, "TZ", zone); diff --git a/port/android/host_imports.list b/port/android/host_imports.list index 2888fe99..fc298895 100644 --- a/port/android/host_imports.list +++ b/port/android/host_imports.list @@ -48,5 +48,6 @@ host_gl_buffer_write host_gl_fence_frame host_gl_wait_frame host_android_path +host_gesture_insets host_errno diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index 74268511..2129a2f0 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -4,17 +4,21 @@ TOUCH_INPUT.C The touchscreen of the Android build. SDL's finger events become the menus' pointer (touch_menu.c) while a menu is up, as the desktop mouse does (sdl_platform.c, platform_ui_pointer_read). Sizes are in dp, from the -display density the app passes (HALO_DISPLAY_DENSITY, host_main.c), and the -edges of the screen where Android keeps its gestures -(HALO_GESTURE_INSETS, host_main.c). +display density the app passes (HALO_DISPLAY_DENSITY, host_main.c). The edges +of the screen where Android keeps its gestures are asked of the host at every +finger down (host_gesture_insets). */ #include "touch_input.h" #include "touch_menu.h" -#include #include +#ifdef HALO_ANDROID +/* port/android/guest/runtime/guest_host.h */ +void host_gesture_insets(int *insets); +#endif + /* the game's (port/linux/game/touch_game.c) */ int touch_game_cinematic_skippable(void); @@ -51,25 +55,6 @@ static float pixels_per_dp(void) return height > 0 ? (float)height / 360.0f : 0.0f; } -/** - * @brief Reads the system gesture insets the app passes. - * @param left,top,right,bottom receive the insets in pixels; all 0 when the - * variable is missing or malformed, so that the whole screen works - */ -static void gesture_insets(float *left, float *top, float *right, float *bottom) -{ - const char *text = getenv("HALO_GESTURE_INSETS"); - int l, t, r, b; - - *left = *top = *right = *bottom = 0.0f; - if (!text || sscanf(text, "%d,%d,%d,%d", &l, &t, &r, &b) != 4 || l < 0 || t < 0 || r < 0 || b < 0) - return; - *left = (float)l; - *top = (float)t; - *right = (float)r; - *bottom = (float)b; -} - /** * @brief Sets the gestures' sizes up, once they can be known; until then a * dp is a pixel, there are no gesture zones and the next call tries again. @@ -87,9 +72,8 @@ static void menu_setup(void) settings.slop = TOUCH_TAP_SLOP_DP * (dp > 0.0f ? dp : 1.0f); settings.step = TOUCH_SCROLL_STEP_DP * (dp > 0.0f ? dp : 1.0f); settings.steps_per_read = TOUCH_STEPS_PER_READ; - /* the zones are measured against the window: without its size the - right and bottom zones would be set up against 0 */ - gesture_insets(&settings.edge_left, &settings.edge_top, &settings.edge_right, &settings.edge_bottom); + /* no zones until a finger goes down: touch_input_event reads them */ + settings.edge_left = settings.edge_top = settings.edge_right = settings.edge_bottom = 0.0f; settings.width = (float)width; settings.height = (float)height; touch_menu_init(&menu, &settings); @@ -97,6 +81,29 @@ static void menu_setup(void) } } +/** + * @brief Brings the gesture zones and the window's size up to date. It runs + * at every finger down, not once at startup: the phone rotates after the app + * has started, and the insets of the portrait screen would stay. The zones + * matter only at a down, so changing them between two fingers is safe. + * Without Android's insets (desktop) there are no zones. + * @param width,height the window's size in pixels + */ +static void refresh_zones(int width, int height) +{ +#ifdef HALO_ANDROID + int insets[4]; + + host_gesture_insets(insets); + menu.settings.edge_left = (float)insets[0]; + menu.settings.edge_top = (float)insets[1]; + menu.settings.edge_right = (float)insets[2]; + menu.settings.edge_bottom = (float)insets[3]; +#endif + menu.settings.width = (float)width; + menu.settings.height = (float)height; +} + void touch_input_event(unsigned int type, const SDL_TouchFingerEvent *finger) { int width = 0, height = 0; @@ -110,6 +117,7 @@ void touch_input_event(unsigned int type, const SDL_TouchFingerEvent *finger) switch (type) { case SDL_EVENT_FINGER_DOWN: + refresh_zones(width, height); touch_menu_down(&menu, finger->fingerID, x, y); break; case SDL_EVENT_FINGER_MOTION: diff --git a/port/linux/src/touch_menu.c b/port/linux/src/touch_menu.c index 95e43210..a7598baf 100644 --- a/port/linux/src/touch_menu.c +++ b/port/linux/src/touch_menu.c @@ -24,8 +24,9 @@ void touch_menu_reset(struct touch_menu *menu) /** * @brief Whether x is in the left or right gesture zone: [0, edge_left) and - * [width - edge_right, width). Without a window size the right one does not - * exist, since it would otherwise start at a negative x and cover the screen. + * [width - edge_right, width). A zone with no inset does not exist (x at the + * window's edge is not in it), and neither does the right one without a + * window size: it would start at a negative x and cover the screen. * @param menu the state (the zones are in its settings) * @param x the finger's x, in pixels */ @@ -33,12 +34,12 @@ static int in_side_zone(const struct touch_menu *menu, float x) { const struct touch_menu_settings *s = &menu->settings; - return x < s->edge_left || (s->width > 0.0f && x >= s->width - s->edge_right); + return x < s->edge_left || (s->edge_right > 0.0f && s->width > 0.0f && x >= s->width - s->edge_right); } /** * @brief Whether y is in the top or bottom gesture zone; as the side zones, - * the bottom one needs the window's height. + * the bottom one needs an inset and the window's height. * @param menu the state (the zones are in its settings) * @param y the finger's y, in pixels */ @@ -46,7 +47,7 @@ static int in_top_or_bottom_zone(const struct touch_menu *menu, float y) { const struct touch_menu_settings *s = &menu->settings; - return y < s->edge_top || (s->height > 0.0f && y >= s->height - s->edge_bottom); + return y < s->edge_top || (s->edge_bottom > 0.0f && s->height > 0.0f && y >= s->height - s->edge_bottom); } void touch_menu_down(struct touch_menu *menu, unsigned long long finger, float x, float y) diff --git a/port/linux/src/touch_menu.h b/port/linux/src/touch_menu.h index 574f179a..29c9c245 100644 --- a/port/linux/src/touch_menu.h +++ b/port/linux/src/touch_menu.h @@ -38,7 +38,9 @@ struct touch_menu_settings neither a tap nor a scroll. */ float edge_left, edge_top, edge_right, edge_bottom; /* the window's size, for the right and bottom zones: 0 means those two - zones do not exist, so that a size not yet known cannot fill the screen */ + zones do not exist, so that a size not yet known cannot fill the screen. + A zone with a 0 inset does not exist either. The zones may change + between two fingers (the phone rotates): they are read at a down. */ float width, height; }; diff --git a/port/linux/tests/touch_menu_test.c b/port/linux/tests/touch_menu_test.c index 0d4711c5..9c952d53 100644 --- a/port/linux/tests/touch_menu_test.c +++ b/port/linux/tests/touch_menu_test.c @@ -1016,6 +1016,76 @@ static void test_each_side_has_its_own_inset(void) CHECK(out.wheel_steps == 2); } +/* a window size without insets: its far edge is not a zone */ +static void test_without_insets_the_far_edges_are_no_zones(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + struct touch_menu_settings settings = { 36.0f, 120.0f, 2, 0.0f, 0.0f, 0.0f, 0.0f, 2340.0f, 1080.0f }; + + touch_menu_init(&menu, &settings); + touch_menu_down(&menu, 1, 2340.0f, 500.0f); + touch_menu_up(&menu, 1, 2340.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + CHECK(out.downs == 1); + swipe(&menu, 1, 1170.0f, 1080.0f, 1170.0f, 300.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); +} + +/* the phone rotates after the app started: the zones the host reads at a +down replace the old ones, for the next finger on */ +static void test_zones_changed_between_fingers_apply_at_the_next_down(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + struct touch_menu_settings settings = { 36.0f, 120.0f, 2, 0.0f, 0.0f, 0.0f, 0.0f, 1080.0f, 2340.0f }; + + touch_menu_init(&menu, &settings); + touch_menu_down(&menu, 1, 10.0f, 500.0f); + touch_menu_up(&menu, 1, 10.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); + menu.settings.edge_left = 90.0f; + menu.settings.edge_right = 90.0f; + menu.settings.width = 2340.0f; + menu.settings.height = 1080.0f; + touch_menu_down(&menu, 1, 10.0f, 500.0f); + touch_menu_up(&menu, 1, 10.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + CHECK(out.downs == 0); + touch_menu_down(&menu, 1, 2300.0f, 500.0f); + touch_menu_up(&menu, 1, 2300.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 0); + menu.settings.edge_left = 0.0f; + menu.settings.edge_right = 0.0f; + touch_menu_down(&menu, 1, 10.0f, 500.0f); + touch_menu_up(&menu, 1, 10.0f, 500.0f); + touch_menu_read(&menu, &out); + CHECK(out.clicks == 1); +} + +/* a zone is judged at the down only: a finger already down keeps its kind +when the zones change under it */ +static void test_zones_changed_under_a_finger_do_not_touch_it(void) +{ + struct touch_menu menu; + struct touch_menu_output out; + + setup_zones(&menu); + touch_menu_down(&menu, 1, 1170.0f, 540.0f); + menu.settings.edge_top = 600.0f; + menu.settings.edge_left = 2000.0f; + touch_menu_move(&menu, 1, 1170.0f, 100.0f); + touch_menu_up(&menu, 1, 1170.0f, 100.0f); + touch_menu_read(&menu, &out); + CHECK(out.wheel_steps == -2); + CHECK(out.clicks == 0); +} + static void test_reset_and_cancel_drop_a_zone_finger(void) { struct touch_menu menu; @@ -1108,6 +1178,9 @@ int main(void) test_without_zones_the_edges_work(); test_without_a_size_only_the_left_and_top_zones_exist(); test_each_side_has_its_own_inset(); + test_without_insets_the_far_edges_are_no_zones(); + test_zones_changed_between_fingers_apply_at_the_next_down(); + test_zones_changed_under_a_finger_do_not_touch_it(); test_reset_and_cancel_drop_a_zone_finger(); if (failures) printf("%d failures\n", failures); diff --git a/tools/mutate_touch_menu.py b/tools/mutate_touch_menu.py index dd8a990e..a446ebc4 100644 --- a/tools/mutate_touch_menu.py +++ b/tools/mutate_touch_menu.py @@ -158,17 +158,19 @@ MUTANTS = [ ("zone: left check removed", "return x < s->edge_left ||", "return 0 ||"), ("zone: left uses the right inset", "return x < s->edge_left ||", "return x < s->edge_right ||"), ("zone: right >= -> >", "x >= s->width - s->edge_right", "x > s->width - s->edge_right"), - ("zone: right check removed", " || (s->width > 0.0f && x >= s->width - s->edge_right)", ""), - ("zone: right without the width guard", "(s->width > 0.0f && x >= s->width - s->edge_right)", - "(x >= s->width - s->edge_right)"), + ("zone: right check removed", " || (s->edge_right > 0.0f && s->width > 0.0f && x >= s->width - s->edge_right)", ""), + ("zone: right without the width guard", "s->width > 0.0f && x >= s->width - s->edge_right)", + "x >= s->width - s->edge_right)"), + ("zone: right without the inset guard", "(s->edge_right > 0.0f && s->width", "(s->width"), ("zone: right uses the left inset", "x >= s->width - s->edge_right", "x >= s->width - s->edge_left"), ("zone: top < -> <=", "return y < s->edge_top ||", "return y <= s->edge_top ||"), ("zone: top check removed", "return y < s->edge_top ||", "return 0 ||"), ("zone: top uses the bottom inset", "return y < s->edge_top ||", "return y < s->edge_bottom ||"), ("zone: bottom >= -> >", "y >= s->height - s->edge_bottom", "y > s->height - s->edge_bottom"), - ("zone: bottom check removed", " || (s->height > 0.0f && y >= s->height - s->edge_bottom)", ""), - ("zone: bottom without the height guard", "(s->height > 0.0f && y >= s->height - s->edge_bottom)", - "(y >= s->height - s->edge_bottom)"), + ("zone: bottom check removed", " || (s->edge_bottom > 0.0f && s->height > 0.0f && y >= s->height - s->edge_bottom)", ""), + ("zone: bottom without the height guard", "s->height > 0.0f && y >= s->height - s->edge_bottom)", + "y >= s->height - s->edge_bottom)"), + ("zone: bottom without the inset guard", "(s->edge_bottom > 0.0f && s->height", "(s->height"), ("zone: bottom uses the top inset", "y >= s->height - s->edge_bottom", "y >= s->height - s->edge_top"), ("down: side zone finger accepted", "\tif (in_side_zone(menu, x))\n\t\treturn;\n", "\tif (0 && in_side_zone(menu, x))\n\t\treturn;\n"), @@ -185,6 +187,8 @@ MUTANTS = [ ("down: no_scroll always set", "menu->no_scroll = in_top_or_bottom_zone(menu, y);", "menu->no_scroll = 1 || in_top_or_bottom_zone(menu, y);"), ("down: no_scroll from x", "menu->no_scroll = in_top_or_bottom_zone(menu, y);", "menu->no_scroll = in_top_or_bottom_zone(menu, x);"), ("move: no-scroll finger scrolls", "\t\t\tmenu->void_tap = 1;\n\t\t\treturn;", "\t\t\tmenu->void_tap = 1;"), + ("move: zone judged again at the move", "\t\tif (menu->no_scroll)\n", + "\t\tif (in_top_or_bottom_zone(menu, menu->down_y))\n"), ("move: no_scroll test removed", "\t\tif (menu->no_scroll)\n", "\t\tif (0)\n"), ("move: no-scroll finger keeps its tap", "\t\t\tmenu->void_tap = 1;\n", ""), ("up: no-scroll finger clicks", "if (!menu->scrolling && !menu->void_tap)", "if (!menu->scrolling)"), From 8fda769765329efbcdf4de6e128d4ad6942d659f Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Fri, 2 Oct 2026 10:24:55 +0200 Subject: [PATCH 007/187] Let a tap on a setting row reach its value --- source/interface/ui_widget.c | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 12991c39..a9b7be5f 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5623,6 +5623,63 @@ static void ui_mouse_note_target( return; } +/** + * @brief Makes a setting row its value: a row (an item) whose child is a + * value stops being a target, and the value takes the row's height. The row + * is where the thin value box sits among a label and empty space, and a tap + * anywhere else on it would press A on the row, which on a setting screen + * is ACCEPT. The value keeps its width, and so its left half / right half + * rule. Only the value's parent is the row: that is how the controller and + * gametype screens nest them (op_* row, *_spinner child), and a nearer match + * than "any ancestor", which could take a whole panel for a row. + */ +static void ui_mouse_merge_setting_rows( + void) +{ + long index; + long other; + long kept = 0; + + for (index = 0; index < ui_mouse_target_count; index++) + { + struct ui_mouse_target *value = &ui_mouse_targets[index]; + + if (value->kind != _ui_mouse_target_value) + continue; + for (other = 0; other < ui_mouse_target_count; other++) + { + struct ui_mouse_target const *row = &ui_mouse_targets[other]; + + if (row->kind == _ui_mouse_target_item && row->widget == value->widget->parent) + { + value->bounds.y0 = row->bounds.y0; + value->bounds.y1 = row->bounds.y1; + } + } + } + for (index = 0; index < ui_mouse_target_count; index++) + { + struct ui_mouse_target *row = &ui_mouse_targets[index]; + + for (other = 0; other < ui_mouse_target_count && row->kind == _ui_mouse_target_item; other++) + { + struct ui_mouse_target const *value = &ui_mouse_targets[other]; + + if (value->kind == _ui_mouse_target_value && value->widget->parent == row->widget) + row->kind = NONE; + } + } + /* in place, in order: the last target noted wins an overlap */ + for (index = 0; index < ui_mouse_target_count; index++) + { + if (ui_mouse_targets[index].kind != NONE) + ui_mouse_targets[kept++] = ui_mouse_targets[index]; + } + ui_mouse_target_count = kept; + + return; +} + /** * @brief The right edge of a legend label's text as the game draws it, or * NONE when it cannot be measured (no text, no font, or icons in the string, @@ -6609,6 +6666,7 @@ void render_ui_widgets( } } } + ui_mouse_merge_setting_rows(); ui_mouse_fit_button_targets(); if (widget_globals.fade_to_black >= 0.0f && widget_globals.fade_to_black <= 1.0f) From c4cf55458e5fb4e495ed65d377ef481fbf89a8f1 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Fri, 2 Oct 2026 10:35:11 +0200 Subject: [PATCH 008/187] Merge a setting row only when it holds one value; fit legends first --- source/interface/ui_widget.c | 47 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index a9b7be5f..f3343630 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5624,14 +5624,19 @@ static void ui_mouse_note_target( } /** - * @brief Makes a setting row its value: a row (an item) whose child is a + * @brief Makes a setting row its value: an item whose only value child is a * value stops being a target, and the value takes the row's height. The row * is where the thin value box sits among a label and empty space, and a tap * anywhere else on it would press A on the row, which on a setting screen * is ACCEPT. The value keeps its width, and so its left half / right half * rule. Only the value's parent is the row: that is how the controller and - * gametype screens nest them (op_* row, *_spinner child), and a nearer match - * than "any ancestor", which could take a whole panel for a row. + * gametype screens nest them (op_* row, *_spinner child). An item holding + * several values (a panel, a list of spinners) is not a row: it and its + * values keep their own areas, since growing each value to the item's height + * would pile up boxes of which only the last noted could be tapped. + * Runs after ui_mouse_fit_button_targets so that the legends are fitted + * against the whole row, as before: a legend growing up into a row's label + * area would press ACCEPT there. */ static void ui_mouse_merge_setting_rows( void) @@ -5642,31 +5647,27 @@ static void ui_mouse_merge_setting_rows( for (index = 0; index < ui_mouse_target_count; index++) { - struct ui_mouse_target *value = &ui_mouse_targets[index]; + struct ui_mouse_target *row = &ui_mouse_targets[index]; + struct ui_mouse_target *only_value = NULL; + long value_count = 0; - if (value->kind != _ui_mouse_target_value) + if (row->kind != _ui_mouse_target_item) continue; for (other = 0; other < ui_mouse_target_count; other++) { - struct ui_mouse_target const *row = &ui_mouse_targets[other]; - - if (row->kind == _ui_mouse_target_item && row->widget == value->widget->parent) - { - value->bounds.y0 = row->bounds.y0; - value->bounds.y1 = row->bounds.y1; - } - } - } - for (index = 0; index < ui_mouse_target_count; index++) - { - struct ui_mouse_target *row = &ui_mouse_targets[index]; - - for (other = 0; other < ui_mouse_target_count && row->kind == _ui_mouse_target_item; other++) - { - struct ui_mouse_target const *value = &ui_mouse_targets[other]; + struct ui_mouse_target *value = &ui_mouse_targets[other]; if (value->kind == _ui_mouse_target_value && value->widget->parent == row->widget) - row->kind = NONE; + { + only_value = value; + value_count++; + } + } + if (value_count == 1) + { + only_value->bounds.y0 = row->bounds.y0; + only_value->bounds.y1 = row->bounds.y1; + row->kind = NONE; } } /* in place, in order: the last target noted wins an overlap */ @@ -6666,8 +6667,8 @@ void render_ui_widgets( } } } - ui_mouse_merge_setting_rows(); ui_mouse_fit_button_targets(); + ui_mouse_merge_setting_rows(); if (widget_globals.fade_to_black >= 0.0f && widget_globals.fade_to_black <= 1.0f) { From 2ee4fd2cc2e97063e26ae956ba23ce2c097cff60 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Fri, 2 Oct 2026 11:16:52 +0200 Subject: [PATCH 009/187] Double the width of each setting --- source/interface/ui_widget.c | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index f3343630..eacf59ff 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5681,6 +5681,55 @@ static void ui_mouse_merge_setting_rows( return; } +/** + * @brief Doubles the width of each value (a setting), around its centre: its + * arrows are small for a finger. Each half grows outward by half the box's + * width, so the previous / next split stays at the middle. A box is not held + * to its row (the last arrow of a row sits near the row's end), but stops at + * the edge of a non-button target beside it that it overlaps vertically, + * and at the menu's drawable area. Runs after ui_mouse_merge_setting_rows, + * once the rows have gone, and once a frame: the widths come from the boxes + * as noted. + */ +static void ui_mouse_widen_values( + void) +{ + long index; + long other; + short screen_width = (short)halo_screen_width(); + + for (index = 0; index < ui_mouse_target_count; index++) + { + struct ui_mouse_target *value = &ui_mouse_targets[index]; + short half = (short)((value->bounds.x1 - value->bounds.x0) / 2); + short left = (short)(value->bounds.x0 - half); + short right = (short)(value->bounds.x1 + half); + + if (value->kind != _ui_mouse_target_value) + continue; + left = MAX(left, (short)(-(screen_width - 640) / 2)); + right = MIN(right, (short)(640 + (screen_width - 640) / 2)); + for (other = 0; other < ui_mouse_target_count; other++) + { + struct ui_mouse_target const *beside = &ui_mouse_targets[other]; + + if (other == index || beside->kind == _ui_mouse_target_button || + beside->bounds.y0 >= value->bounds.y1 || beside->bounds.y1 <= value->bounds.y0) + { + continue; + } + if (beside->bounds.x1 <= value->bounds.x0) + left = MAX(left, beside->bounds.x1); + else if (beside->bounds.x0 >= value->bounds.x1) + right = MIN(right, beside->bounds.x0); + } + value->bounds.x0 = left; + value->bounds.x1 = right; + } + + return; +} + /** * @brief The right edge of a legend label's text as the game draws it, or * NONE when it cannot be measured (no text, no font, or icons in the string, @@ -6669,6 +6718,7 @@ void render_ui_widgets( } ui_mouse_fit_button_targets(); ui_mouse_merge_setting_rows(); + ui_mouse_widen_values(); if (widget_globals.fade_to_black >= 0.0f && widget_globals.fade_to_black <= 1.0f) { From 7ad54bb0123df23da69e8d67b008275f2b70b0c2 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Fri, 2 Oct 2026 12:33:02 +0200 Subject: [PATCH 010/187] Keep a widened setting's split at its centre; widen against the noted boxes --- source/interface/ui_widget.c | 54 ++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index eacf59ff..74fa302e 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5228,6 +5228,11 @@ struct ui_mouse_target rectangle2d bounds; short kind; short button_index; + /* where a value splits into previous / next: the middle of the box as + drawn, which ui_mouse_widen_values keeps when a neighbour or the screen's + edge holds the widened box to one side */ + short split_x; + short split_y; /* a button's parts, kept so its area can be settled once the whole frame's buttons are known (ui_mouse_fit_button_targets) */ rectangle2d icon_bounds; @@ -5616,6 +5621,8 @@ static void ui_mouse_note_target( target->bounds = bounds; target->kind = kind; target->button_index = button_index; + target->split_x = (bounds.x0 + bounds.x1) / 2; + target->split_y = (bounds.y0 + bounds.y1) / 2; target->icon_bounds = icon_bounds; target->label = target_label; target->label_bounds = target_label_bounds; @@ -5684,47 +5691,58 @@ static void ui_mouse_merge_setting_rows( /** * @brief Doubles the width of each value (a setting), around its centre: its * arrows are small for a finger. Each half grows outward by half the box's - * width, so the previous / next split stays at the middle. A box is not held - * to its row (the last arrow of a row sits near the row's end), but stops at - * the edge of a non-button target beside it that it overlaps vertically, - * and at the menu's drawable area. Runs after ui_mouse_merge_setting_rows, - * once the rows have gone, and once a frame: the widths come from the boxes - * as noted. + * width. A box is not held to its row (the last arrow of a row sits near + * the row's end), but stops at the edge of a non-button target beside it + * that it overlaps vertically, at the midpoint of the gap to another value + * facing it (so neither takes the other's room, whatever the order they + * were noted in: every measure is against the boxes as noted, not as already + * widened), and at the menu's drawable area. A box never shrinks. Where a + * limit holds one side back, the split between previous and next stays at + * the original centre, where the arrows are. Runs after + * ui_mouse_merge_setting_rows, once the rows have gone, and once a frame. */ static void ui_mouse_widen_values( void) { + rectangle2d noted[UI_MOUSE_MAXIMUM_TARGETS]; long index; long other; short screen_width = (short)halo_screen_width(); + for (index = 0; index < ui_mouse_target_count; index++) + noted[index] = ui_mouse_targets[index].bounds; for (index = 0; index < ui_mouse_target_count; index++) { struct ui_mouse_target *value = &ui_mouse_targets[index]; - short half = (short)((value->bounds.x1 - value->bounds.x0) / 2); - short left = (short)(value->bounds.x0 - half); - short right = (short)(value->bounds.x1 + half); + rectangle2d const *box = ¬ed[index]; + short half = (short)((box->x1 - box->x0) / 2); + short left = (short)(box->x0 - half); + short right = (short)(box->x1 + half); if (value->kind != _ui_mouse_target_value) continue; + value->split_x = (box->x0 + box->x1) / 2; + value->split_y = (box->y0 + box->y1) / 2; left = MAX(left, (short)(-(screen_width - 640) / 2)); right = MIN(right, (short)(640 + (screen_width - 640) / 2)); for (other = 0; other < ui_mouse_target_count; other++) { struct ui_mouse_target const *beside = &ui_mouse_targets[other]; + rectangle2d const *beside_box = ¬ed[other]; + boolean is_value = beside->kind == _ui_mouse_target_value; if (other == index || beside->kind == _ui_mouse_target_button || - beside->bounds.y0 >= value->bounds.y1 || beside->bounds.y1 <= value->bounds.y0) + beside_box->y0 >= box->y1 || beside_box->y1 <= box->y0) { continue; } - if (beside->bounds.x1 <= value->bounds.x0) - left = MAX(left, beside->bounds.x1); - else if (beside->bounds.x0 >= value->bounds.x1) - right = MIN(right, beside->bounds.x0); + if (beside_box->x1 <= box->x0) + left = MAX(left, is_value ? (short)((beside_box->x1 + box->x0) / 2) : beside_box->x1); + else if (beside_box->x0 >= box->x1) + right = MIN(right, is_value ? (short)((box->x1 + beside_box->x0) / 2) : beside_box->x0); } - value->bounds.x0 = left; - value->bounds.x1 = right; + value->bounds.x0 = MIN(left, box->x0); + value->bounds.x1 = MAX(right, box->x1); } return; @@ -6313,8 +6331,8 @@ static void ui_widgets_process_mouse( ui_mouse_give_focus(target->widget); ui_mouse_list_directions(target->widget, &back, &forward); first_half = back == _widget_event_dpad_left ? - ui_mouse_click_x < (target->bounds.x0 + target->bounds.x1) / 2 : - ui_mouse_click_y < (target->bounds.y0 + target->bounds.y1) / 2; + ui_mouse_click_x < target->split_x : + ui_mouse_click_y < target->split_y; ui_mouse_press(first_half ? back : forward); break; } From b8ac7b4b8d7f0f67be10f2f50094c138b7ff1cc0 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Sat, 3 Oct 2026 22:22:49 +0200 Subject: [PATCH 011/187] Settle tap targets once a frame; stop legend strips at targets below --- source/interface/ui_widget.c | 97 ++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 21 deletions(-) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 74fa302e..e1303fc1 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5228,9 +5228,11 @@ struct ui_mouse_target rectangle2d bounds; short kind; short button_index; - /* where a value splits into previous / next: the middle of the box as - drawn, which ui_mouse_widen_values keeps when a neighbour or the screen's - edge holds the widened box to one side */ + /* where a value splits into previous / next: the middle of its box + before ui_mouse_widen_values widens it, so across the middle of the value + as drawn, where its arrows are, and up and down the middle of its row once + ui_mouse_merge_setting_rows has given it the row's height. A widened box + that a neighbour or the screen's edge holds to one side keeps it */ short split_x; short split_y; /* a button's parts, kept so its area can be settled once the whole frame's @@ -5243,6 +5245,13 @@ struct ui_mouse_target static struct ui_mouse_target ui_mouse_targets[UI_MOUSE_MAXIMUM_TARGETS]; static long ui_mouse_target_count = 0; static boolean ui_mouse_noting_targets = FALSE; +/* the frame's targets are noted and settled (ui_mouse_fit_button_targets, +ui_mouse_merge_setting_rows, ui_mouse_widen_values) by one render only: +render_ui_widgets runs once per player's viewport, and once more for a +mirror (render.c), and a second render would note the same widgets again +and widen the values once more, which moves their split off the arrows. +Cleared with the targets, by ui_widgets_process_mouse */ +static boolean ui_mouse_targets_settled = FALSE; /* The presses the mouse makes, posted one a frame: the event queue keeps only the latest event posted between two frames (queue_event). What they @@ -5793,7 +5802,8 @@ static short ui_mouse_label_text_right( * cover the next legend), stops before the next legend's icon on its row, * and, for a touchscreen only, grows up to 8 units upward (not past an item, * value or list slot above it) and in the screen's bottom strip runs down to - * the screen's edge. Recomputed from the parts kept at noting, so running it + * the screen's edge (not past an item, value or list slot below it). + * Recomputed from the parts kept at noting, so running it * again changes nothing. */ static void ui_mouse_fit_button_targets( @@ -5850,7 +5860,26 @@ static void ui_mouse_fit_button_targets( if (target->kind != _ui_mouse_target_button || !ui_mouse_pointer_is_touch) continue; if (target->bounds.y1 >= 400) - target->bounds.y1 = 480; + { + short bottom = 480; + + /* running down to the screen's edge must not take the tap from an + item, value or list slot that a mod puts under the legend, as + growing upward must not above it: the legend is noted later, so it + would win the overlap */ + for (other = 0; other < ui_mouse_target_count; other++) + { + struct ui_mouse_target const *below = &ui_mouse_targets[other]; + + if (below->kind != _ui_mouse_target_button && + below->bounds.y0 >= target->bounds.y1 && + below->bounds.x0 < target->bounds.x1 && below->bounds.x1 > target->bounds.x0) + { + bottom = MIN(bottom, below->bounds.y0); + } + } + target->bounds.y1 = bottom; + } /* growing upward must not take the tap from an item, value or list slot above the legend: those keep their areas, and the legend is noted later, so it would win the overlap */ @@ -6135,7 +6164,8 @@ static boolean ui_mouse_menus_active( * slot's hover steps the list first), so it is resolved against the * targets of a later frame than the one the tap arrived in; the frames * between are logged so the target can be read against the screen that - * showed then. + * showed then. A value's line also gives the point where it splits into + * previous and next. * @param x horizontal position of the tap, in menu coordinates * @param y vertical position of the tap, in menu coordinates * @param target what the click acts on, or NULL for none @@ -6151,10 +6181,23 @@ static void ui_debug_log_click( if (target) { - platform_log("touch targets: tap at %d,%d hit %s %s [%d,%d,%d,%d] after %ld frames", x, y, - kinds[PIN(target->kind, 0, 3)], - tag_get_name(target->widget->definition_tag_index), - target->bounds.x0, target->bounds.y0, target->bounds.x1, target->bounds.y1, frames); + char const *kind = kinds[PIN(target->kind, 0, 3)]; + char const *name = tag_get_name(target->widget->definition_tag_index); + + /* a value steps by the side of its split the tap is on, and once + widened the split is not the middle of the logged box + (ui_mouse_widen_values) */ + if (target->kind == _ui_mouse_target_value) + { + platform_log("touch targets: tap at %d,%d hit %s %s [%d,%d,%d,%d] split %d,%d after %ld frames", x, y, + kind, name, target->bounds.x0, target->bounds.y0, target->bounds.x1, target->bounds.y1, + target->split_x, target->split_y, frames); + } + else + { + platform_log("touch targets: tap at %d,%d hit %s %s [%d,%d,%d,%d] after %ld frames", x, y, + kind, name, target->bounds.x0, target->bounds.y0, target->bounds.x1, target->bounds.y1, frames); + } } else { @@ -6199,10 +6242,14 @@ static void ui_debug_log_keyboard_tap( * into the first player's controller events. While the virtual keyboard is * up it gets the clicks and the menu behind gets nothing. A touch drag * stops at the list's ends and skips settings (ui_mouse_wheel_widget, - * ui_mouse_wheel_room); the desktop wheel wraps as the d-pad does. With - * debug.touch_targets on it also notes the finger-down and tap points for - * the debug view, and logs each tap: a menu click where it is resolved, a - * keyboard click where it is taken. + * ui_mouse_wheel_room); the desktop wheel wraps as the d-pad does. It + * records whether the pointer read is the touchscreen + * (ui_mouse_pointer_is_touch), because the legends' taller tap areas are + * for a finger only (ui_mouse_fit_button_targets). With debug.touch_targets + * on it also notes the finger-down and tap points for the debug view, and + * logs each tap: a menu click where it is resolved, a keyboard click where + * it is taken. It forgets the frame's targets at the end, so that the next + * frame's render notes and settles them anew. */ static void ui_widgets_process_mouse( void) @@ -6367,6 +6414,7 @@ static void ui_widgets_process_mouse( } } ui_mouse_target_count = 0; + ui_mouse_targets_settled = FALSE; return; } @@ -6631,7 +6679,8 @@ void render_ui_widgets_postgame( /** * @brief Renders the active widgets for one local player's viewport (or the - * whole screen), noting the first player's tap targets, and draws the + * whole screen), noting and settling the first player's tap targets in the + * frame's first render that can (ui_mouse_targets_settled), and draws the * debug view of the targets and the virtual keyboard when they are up. * @param local_player_index the viewport's player, or NONE for the whole screen * @param window_bounds the viewport's bounds on the screen @@ -6700,9 +6749,11 @@ void render_ui_widgets( bounds.y1 = window_bounds->y1 - window_bounds->y0; offset.x = 0; offset.y = 0; - /* the mouse drives the first player's menus */ - ui_mouse_noting_targets = widget->local_player_index == NONE || - widget->local_player_index == 0; + /* the mouse drives the first player's menus; a widget shown in + every viewport (a dialog for everyone) is noted in the first + player's render only, and once a frame */ + ui_mouse_noting_targets = first_players_render && !ui_mouse_targets_settled && + (widget->local_player_index == NONE || widget->local_player_index == 0); widget_instance_render_recursive( widget_globals.active_widgets[widget_index], &bounds, @@ -6734,9 +6785,13 @@ void render_ui_widgets( } } } - ui_mouse_fit_button_targets(); - ui_mouse_merge_setting_rows(); - ui_mouse_widen_values(); + if (first_players_render && !ui_mouse_targets_settled) + { + ui_mouse_fit_button_targets(); + ui_mouse_merge_setting_rows(); + ui_mouse_widen_values(); + ui_mouse_targets_settled = TRUE; + } if (widget_globals.fade_to_black >= 0.0f && widget_globals.fade_to_black <= 1.0f) { From 349c110d742510f1758a351331453d8decd777e6 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Sat, 3 Oct 2026 22:41:04 +0200 Subject: [PATCH 012/187] Step a full-screen list by the band beside its slots --- port/linux/README.md | 2 +- port/linux/src/port_config.c | 7 +- source/interface/ui_widget.c | 177 +++++++++++++++++++++++++++++++++-- 3 files changed, 172 insertions(+), 14 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index b8a9b65b..567861a4 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -185,7 +185,7 @@ the setting for one start of the game. It has priority over the file. | `debug.gpu_stats`, `debug.gpu_trace_frame`, `debug.gpu_trace_constants`, `debug.gpu_dump_shaders`, `debug.texture_dump_directory`, `debug.texture_log`, `debug.gl_debug`, `debug.texture_no_cache` | off | `HALO_GPU_STATS`, `HALO_GPU_TRACE`, `HALO_GPU_TRACE_CONSTANTS`, `HALO_GPU_DUMP_SHADERS`, `HALO_TEXTURE_DUMP`, `HALO_TEXTURE_LOG`, `HALO_GL_DEBUG`, `HALO_TEXTURE_NO_CACHE` | Tools to find problems in the graphics: counts for each frame, all the GL state of one frame, the GLSL code, the textures. | | `debug.gpu_skip_vertex_shaders`, `debug.gpu_debug_expression`, `debug.gpu_debug_flat`, `debug.gpu_debug_texture0` | off | `HALO_GPU_SKIP_VS`, `HALO_GPU_DEBUG_EXPR`, `HALO_GPU_DEBUG_FLAT`, `HALO_GPU_DEBUG_T0` | Tools to find problems in the graphics: skip the draws of a vertex shader, or replace the output of all pixel shaders with a GLSL expression (for example `t0.rgb`). | | `debug.network_test`, `debug.network_test_start`, `debug.network_test_kill`, `debug.network_test_score`, `debug.network_test_shoot`, `debug.network_test_vehicle`, `debug.network_test_pickup`, `debug.network_test_pickup_weapon`, `debug.test_input` | off | `HALO_NETWORK_TEST`, `HALO_NETWORK_TEST_START`, `HALO_NETWORK_TEST_KILL`, `HALO_NETWORK_TEST_SCORE`, `HALO_NETWORK_TEST_SHOOT`, `HALO_NETWORK_TEST_VEHICLE`, `HALO_NETWORK_TEST_PICKUP`, `HALO_NETWORK_TEST_PICKUP_WEAPON`, `HALO_TEST_INPUT` | Automatic tests of system link (`game/network_test.c`). Refer to `NETCODE.md`. | -| `debug.touch_targets` | `false` | `HALO_TOUCH_TARGETS` | Outlines the tap targets of the menus (item green, value blue, list slot yellow, legend button red, keys of the on-screen keyboard white), marks where the last finger went down and the last tap landed for 3 seconds, and logs each tap with the target that it hit: to judge the accuracy of touch. | +| `debug.touch_targets` | `false` | `HALO_TOUCH_TARGETS` | Outlines the tap targets of the menus (item green, value blue, list slot yellow, legend button red, the band beside the slots of a list orange, keys of the on-screen keyboard white), marks where the last finger went down and the last tap landed for 3 seconds, and logs each tap with the target that it hit (for a value, also where it splits into previous and next): to judge the accuracy of touch. | | `debug.network_latency`, `debug.network_loss` | `0` | `HALO_NETWORK_LATENCY`, `HALO_NETWORK_LOSS` | The game holds all the data that it receives for this number of milliseconds, and ignores this percentage of the datagrams. Use these settings to test the netcode as on the internet. | | `debug.telnet_console`, `debug.telnet_console_port` | `false`, `2323` | `HALO_TELNET_CONSOLE`, `HALO_TELNET_CONSOLE_PORT` | The game listens on 127.0.0.1, on this port, for a script console (connect with telnet). The console has no password, so only this computer can reach it. | diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index c5657e51..1d9a0f44 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -209,9 +209,10 @@ static const struct config_setting config_settings[] = "only the administrator can listen on." }, { "debug.touch_targets", _config_boolean, "false", "HALO_TOUCH_TARGETS", _environment_set_is_true, _platform_all, "Outline the menus' tap targets (item green, value blue, list slot yellow,\n" - "legend button red; the virtual keyboard's keys white), mark where the\n" - "last finger went down and the last tap landed for 3 seconds, and log\n" - "each tap with the target it hit; to judge touch accuracy." }, + "legend button red, the band beside a list's slots orange; the virtual\n" + "keyboard's keys white), mark where the last finger went down and the\n" + "last tap landed for 3 seconds, and log each tap with the target it hit\n" + "(and a value's split); to judge touch accuracy." }, { "debug.network_latency", _config_real, "0.0", "HALO_NETWORK_LATENCY", _environment_value, _platform_all, "Milliseconds everything received is held back (a round trip between two\n" "machines of twice it), to test the netcode as over the internet; 0 none." }, diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index e1303fc1..05e262b5 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5219,7 +5219,12 @@ enum ui_mouse_target_kind list steps to it, and a click then presses A */ _ui_mouse_target_list_slot, /* a button's icon and label in a screen's key: a click presses it */ - _ui_mouse_target_button + _ui_mouse_target_button, + /* the band of a list's slot rows left of its first slot (a list showing + several items): a click steps the list back by one */ + _ui_mouse_target_list_back, + /* the band right of its last slot: a click steps it forward by one */ + _ui_mouse_target_list_forward }; struct ui_mouse_target @@ -5394,10 +5399,12 @@ static void ui_debug_draw_targets( { for (index = 0; index < ui_mouse_target_count; index++) { - static pixel32 const colors[] = { 0xc000ff00, 0xc04080ff, 0xc0ffff00, 0xc0ff0000 }; + /* in the order of enum ui_mouse_target_kind; the band's two sides + share orange, which no other kind uses */ + static pixel32 const colors[] = { 0xc000ff00, 0xc04080ff, 0xc0ffff00, 0xc0ff0000, 0xc0ff8000, 0xc0ff8000 }; short kind = ui_mouse_targets[index].kind; - ui_debug_draw_outline(&ui_mouse_targets[index].bounds, colors[PIN(kind, 0, 3)]); + ui_debug_draw_outline(&ui_mouse_targets[index].bounds, colors[PIN(kind, 0, 5)]); } } ui_debug_draw_mark(&ui_debug_down_mark, 0xffff00ff); @@ -5451,6 +5458,11 @@ static long ui_mouse_child_index( return NONE; } +static void ui_mouse_list_directions( + struct widget_instance *widget, + short *back, + short *forward); + /* a list that shows several of its items at once */ static boolean ui_mouse_list_shows_several( struct widget_instance *widget) @@ -5531,10 +5543,123 @@ static boolean ui_mouse_widget_is_item( widget->type == _ui_widget_type_column_list; } +/** + * @brief Notes a target that steps a list by one, unless it is empty or the + * targets are full. + * @param widget the list the target steps + * @param bounds where the target is, render offset included + * @param kind _ui_mouse_target_list_back or _ui_mouse_target_list_forward + */ +static void ui_mouse_note_list_step( + struct widget_instance *widget, + rectangle2d const *bounds, + short kind) +{ + struct ui_mouse_target *target; + + if (ui_mouse_target_count >= UI_MOUSE_MAXIMUM_TARGETS || + bounds->x0 >= bounds->x1 || + bounds->y0 >= bounds->y1) + { + return; + } + target = &ui_mouse_targets[ui_mouse_target_count++]; + target->widget = widget; + target->bounds = *bounds; + target->kind = kind; + target->button_index = NONE; + /* as ui_mouse_note_target sets them for any target that is not a + button: only a value's split and a button's parts are read, and no stale + field of an earlier frame's target stays in the slot */ + target->split_x = (bounds->x0 + bounds->x1) / 2; + target->split_y = (bounds->y0 + bounds->y1) / 2; + target->icon_bounds = *bounds; + target->label = NULL; + target->label_bounds = *bounds; + + return; +} + +/** + * @brief Notes the band that steps a list showing several items side by + * side: the band of the slots' rows (from the top of the highest slot to + * the bottom of the lowest) left of the first slot, which steps back, and + * right of the last, which steps forward, out to the list's own bounds. + * + * A stock several-items list (the map list, the profile slots) covers the + * whole 640x480 screen, so a tap anywhere beside its slots cannot mean + * "step": the band is limited to the slots' rows, and a tap on the title, + * between slots or under them does nothing. The caller notes nothing for + * the list itself, and widget_instance_render_recursive notes the slots + * after this; ui_mouse_target_at takes the last target noted under a point, + * so a slot would win over the band, which never covers one anyway. A list + * the d-pad steps up and down has no side to tap and gets no band (no stock + * list does). + * + * @param widget the list + * @param definition its definition + * @param offset the render offset, the list's own offset included (as + * ui_mouse_note_target gets it) + */ +static void ui_mouse_note_slot_band( + struct widget_instance *widget, + struct ui_widget_definition const *definition, + point2d offset) +{ + struct widget_instance *child; + rectangle2d bounds; + rectangle2d slots; + short back, forward; + boolean found = FALSE; + + ui_mouse_list_directions(widget, &back, &forward); + if (back != _widget_event_dpad_left) + return; + for (child = widget->child; child; child = child->next) + { + rectangle2d slot; + + if (!child->visible) + continue; + /* where widget_instance_render_recursive draws the child: the + list's offset plus the child's own */ + slot = ui_widget_definition_get(child->definition_tag_index)->bounds; + slot.x0 += offset.x + child->horizontal_offset; + slot.x1 += offset.x + child->horizontal_offset; + slot.y0 += offset.y + child->vertical_offset; + slot.y1 += offset.y + child->vertical_offset; + if (!found) + { + slots = slot; + found = TRUE; + } + else + { + slots.x0 = MIN(slots.x0, slot.x0); + slots.x1 = MAX(slots.x1, slot.x1); + slots.y0 = MIN(slots.y0, slot.y0); + slots.y1 = MAX(slots.y1, slot.y1); + } + } + if (!found) + return; + bounds.y0 = slots.y0; + bounds.y1 = slots.y1; + bounds.x0 = definition->bounds.x0 + offset.x; + bounds.x1 = slots.x0; + ui_mouse_note_list_step(widget, &bounds, _ui_mouse_target_list_back); + bounds.x0 = slots.x1; + bounds.x1 = definition->bounds.x1 + offset.x; + ui_mouse_note_list_step(widget, &bounds, _ui_mouse_target_list_forward); + + return; +} + /** * @brief Notes a widget as a tap target of this frame, if it is one: an * item, a value, a list slot, or a legend button (kept in parts, which - * ui_mouse_fit_button_targets settles into the tap area). + * ui_mouse_fit_button_targets settles into the tap area). A list showing + * several items notes the band beside its slots (ui_mouse_note_slot_band). * @param widget the widget being rendered * @param definition the widget's definition * @param offset where the widget's parent is drawn, in menu coordinates @@ -5612,8 +5737,16 @@ static void ui_mouse_note_target( } else if (widget->type == _ui_widget_type_spinner_list) { - /* a list showing several items is picked through them */ - if (!parent || ui_mouse_list_shows_several(widget) || !widget_instance_can_receive_events(widget)) + if (!widget_instance_can_receive_events(widget)) + return; + /* a list showing several items is picked through them, and stepped + by the band beside them */ + if (ui_mouse_list_shows_several(widget)) + { + ui_mouse_note_slot_band(widget, definition, offset); + return; + } + if (!parent) return; kind = _ui_mouse_target_value; } @@ -6177,11 +6310,12 @@ static void ui_debug_log_click( struct ui_mouse_target const *target, long frames) { - static char const *const kinds[] = { "item", "value", "list slot", "button" }; + /* in the order of enum ui_mouse_target_kind */ + static char const *const kinds[] = { "item", "value", "list slot", "button", "list back", "list forward" }; if (target) { - char const *kind = kinds[PIN(target->kind, 0, 3)]; + char const *kind = kinds[PIN(target->kind, 0, 5)]; char const *name = tag_get_name(target->widget->definition_tag_index); /* a value steps by the side of its split the tap is on, and once @@ -6248,8 +6382,9 @@ static void ui_debug_log_keyboard_tap( * for a finger only (ui_mouse_fit_button_targets). With debug.touch_targets * on it also notes the finger-down and tap points for the debug view, and * logs each tap: a menu click where it is resolved, a keyboard click where - * it is taken. It forgets the frame's targets at the end, so that the next - * frame's render notes and settles them anew. + * it is taken. A click on the band beside a list's slots steps the list + * by one; hovering it does nothing. It forgets the frame's targets at the + * end, so that the next frame's render notes and settles them anew. */ static void ui_widgets_process_mouse( void) @@ -6353,6 +6488,13 @@ static void ui_widgets_process_mouse( case _ui_mouse_target_list_slot: ui_mouse_step_list_to_slot(target->widget); break; + /* stepping takes a click, and the click gives the focus itself: + a hover that gave it to a list off the focus path would clear + the slot the list had focused (widget_instance_give_focus_directly), + just for passing over its side */ + case _ui_mouse_target_list_back: + case _ui_mouse_target_list_forward: + break; } } } @@ -6390,6 +6532,21 @@ static void ui_widgets_process_mouse( case _ui_mouse_target_button: ui_mouse_press(target->button_index); break; + case _ui_mouse_target_list_back: + case _ui_mouse_target_list_forward: + { + short back, forward; + + ui_mouse_give_focus(target->widget); + /* as ui_mouse_step_list_to_slot: a list with nothing focused + (a mod's empty list) has no item to step from, and a d-pad + press would move the focus off the list instead */ + if (!target->widget->focused_child) + break; + ui_mouse_list_directions(target->widget, &back, &forward); + ui_mouse_press(target->kind == _ui_mouse_target_list_back ? back : forward); + break; + } } } else From f48031f15f5a13ea81121ced3eab43816264ae0b Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Sat, 3 Oct 2026 22:55:26 +0200 Subject: [PATCH 013/187] Correct two comments on the list band's hover and click --- source/interface/ui_widget.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 05e262b5..d17211fa 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -6488,10 +6488,9 @@ static void ui_widgets_process_mouse( case _ui_mouse_target_list_slot: ui_mouse_step_list_to_slot(target->widget); break; - /* stepping takes a click, and the click gives the focus itself: - a hover that gave it to a list off the focus path would clear - the slot the list had focused (widget_instance_give_focus_directly), - just for passing over its side */ + /* stepping is a click's action: hovering a band does nothing, so + that passing a finger over a list's side never moves its focus or + its items */ case _ui_mouse_target_list_back: case _ui_mouse_target_list_forward: break; @@ -6538,9 +6537,11 @@ static void ui_widgets_process_mouse( short back, forward; ui_mouse_give_focus(target->widget); - /* as ui_mouse_step_list_to_slot: a list with nothing focused - (a mod's empty list) has no item to step from, and a d-pad - press would move the focus off the list instead */ + /* as ui_mouse_step_list_to_slot: a list with no focused item + (empty, or one the focus left, which cleared its focused_child) + has no item to step from, so the first tap on its band only + gives it the focus, and a d-pad press would move the focus off + the list instead */ if (!target->widget->focused_child) break; ui_mouse_list_directions(target->widget, &back, &forward); From 31e54eba1b3e5cb486ae2caedc39aa7054881f93 Mon Sep 17 00:00:00 2001 From: saulo benigno Date: Sat, 3 Oct 2026 19:07:12 -0300 Subject: [PATCH 014/187] Add in-game settings menu --- port/assets/menus/README.md | 13 +++ port/assets/menus/ce/in_game.xml | 63 +++++++++++++ port/assets/menus/menus.json | 1 + port/linux/game/menu_functions.c | 69 ++++++++++++++ port/linux/game/menu_tags.c | 156 +++++++++++++++++++++++++++--- source/interface/ui_widget.c | 157 +++++++++++++++++++++++++++++++ tools/ce_menus.py | 3 +- tools/port_settings.py | 89 ++++++++++++++++++ 8 files changed, 536 insertions(+), 15 deletions(-) create mode 100644 port/assets/menus/ce/in_game.xml diff --git a/port/assets/menus/README.md b/port/assets/menus/README.md index c8ea5779..68fc64cd 100644 --- a/port/assets/menus/README.md +++ b/port/assets/menus/README.md @@ -22,6 +22,19 @@ the full screen, the frame rate, the volumes, internet play, the multiplayer HUD), the profile's controller settings, and the keyboard and mouse's controls. +In a game, the pause menus are the maps' own. The widgets named `in_game/...` +(`ce/in_game.xml`, also from `tools/port_settings.py`) are built in the game's +maps instead of `ui.map`, with only what they lead to (the screens they open, +and their bitmaps and strings): they are drawn with the pause menus' art, +which `ui.map` has not. `in_game/settings_button` is SETTINGS, which the full +screen pause menus (the campaign's, and a network game's with one player +here) get after RESUME GAME (`source/interface/ui_widget.c`), on the +desktop. Its screen opens Edit Profile's Controls Setup, Gamepads, Mouse +Setup, Audio Setup and Video Setup over the pause menu's dim. They save to +`config.toml` as they do from the main menu, but Gamepads, whose changes +Edit Profile saves there, saves only its controller settings to the +player's active profile, on OK (`port active profile edit begin` and `end`). + To change the menus without building the game, put files in a `menus` folder next to `config.toml`. A file with the same path as one here replaces it. On the desktop, any other `.xml` file there is added too. A PNG that a `` diff --git a/port/assets/menus/ce/in_game.xml b/port/assets/menus/ce/in_game.xml new file mode 100644 index 00000000..ed64de4f --- /dev/null +++ b/port/assets/menus/ce/in_game.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json index 6dc6e4e3..acbbd308 100644 --- a/port/assets/menus/menus.json +++ b/port/assets/menus/menus.json @@ -3,6 +3,7 @@ "files": [ "ce/bitmaps.xml", "ce/error.xml", + "ce/in_game.xml", "ce/main_menu.campaign_select.xml", "ce/main_menu.difficulty_select.xml", "ce/main_menu.gametype_select.xml", diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index e6388323..36d4b9fb 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -10,6 +10,9 @@ calls them from PC_MENU_FUNCTION_BASE on): - the PC version's "profile set edit begin" begins editing the first player profile, as its settings screens need, and fails with none (its handlers then open the screens that make one); +- in a game, "port active profile edit begin" and "port active profile edit + end" edit the player's active profile for GAMEPADS (in_game.xml) and keep + the controller settings it sets; - "gamespy screen init" hides the server browser's error and filter panels, and the title of the mode "mp type set mode" did not choose (Internet or LAN); @@ -480,6 +483,64 @@ boolean pc_menu_profile_edit_begin(void) return TRUE; } +/* in a game, GAMEPADS (port/assets/menus/ce/in_game.xml) sets the player's +active profile, which no Edit Profile screen is there to save: "port active +profile edit begin" (A on its row) edits the profile, and "port active +profile edit end" (its screen gone) puts the controller settings OK wrote +into the profile as it is stored and as the game has it, nothing else (the +game's copy can be older than the file: Edit Profile saves only the file), +then saves the one and applies the other, as the game's own pause menu does +with its inverted look (player0_look_invert_pitch). FALSE if the player has +no profile. */ +static long active_profile_edited = NONE; + +static boolean active_profile_edit_begin(short local_player) +{ + long index = player_ui_get_active_player_profile_index(local_player); + + active_profile_edited = NONE; + if (index == NONE) + return FALSE; + player_ui_begin_editing_profile(index); + if (!player_ui_get_edit_player_profile()) + return FALSE; + active_profile_edited = index; + return TRUE; +} + +static boolean active_profile_edit_end(short local_player) +{ + struct player_profile *edited = player_ui_get_edit_player_profile(); + long index = active_profile_edited; + + active_profile_edited = NONE; + if (index == NONE) + return TRUE; + if (edited && player_ui_edit_profile_is_dirty()) + { + struct player_profile profile; + + if (player_profile_get(index, &profile)) + { + profile.controller_settings = edited->controller_settings; + player_profile_save(index, &profile); + if (player_ui_get_active_player_profile_index(local_player) == index) + { + player_ui_get_active_player_profile(local_player, &profile); + profile.controller_settings = edited->controller_settings; + player_ui_set_active_player_profile(local_player, index, &profile); + } + platform_log("menus: the profile's controller settings saved"); + } + else + { + platform_log("menus: could not read the profile; its controller settings not saved"); + } + } + player_ui_end_editing_profile(); + return TRUE; +} + /* ---------- the campaign */ /* the rows of its lists (main_menu/new_select/list_item_0 to 10) */ @@ -3386,6 +3447,14 @@ boolean pc_menu_event_function_invoke( { return pc_menu_profile_edit_begin(); } + else if (!strcmp(name, "port active profile edit begin")) + { + return active_profile_edit_begin(controller_of(widget)); + } + else if (!strcmp(name, "port active profile edit end")) + { + return active_profile_edit_end(controller_of(widget)); + } else if (!strcmp(name, "mouse emit accept event")) { event_manager_post_button(controller_of(widget), BUTTON_A); diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index 74a4a29e..33414605 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -23,6 +23,11 @@ game keeps its own menus. The tag table cannot grow where it is (the tags' names follow it), so it is copied, with ours after it; every existing tag keeps its index. All of it is let go in scenario_tags_unload, before the next map's tags load. + +The game's maps get the in-game widgets (in_game/..., which ui.map has not: +they are drawn with the pause menus' art, which only the game's maps have), +and only what they lead to: the settings screens they open, with their +bitmaps and strings (ui_widget.c adds SETTINGS to the pause menus). */ #include "cseries.h" @@ -58,6 +63,7 @@ boolean pc_menu_tag(long tag_index); /* ---------- constants */ #define PC_MENU_TAG_PREFIX "pc\\" +#define IN_GAME_WIDGET_PREFIX "in_game/" #define UI_WIDGET_DEFINITION_TAG 'DeLa' #define UNICODE_STRING_LIST_TAG 'ustr' #define FONT_TAG 'font' @@ -334,6 +340,8 @@ static char const *const port_function_names[] = "mp profile init indicator opts", "mp profile set ctf rules", "mp profile set koth rules", "mp profile set slayer rules", "mp profile set oddball rules", "mp profile set racing rules", "mp profile set player options", "mp profile set item options", "mp profile set indicator opts", + /* (in a game: GAMEPADS on the player's active profile) */ + "port active profile edit begin", "port active profile edit end", }; /* the PC version's game data functions that the Xbox's have not, from @@ -365,6 +373,8 @@ static struct struct pc_menu_setting *settings; long setting_count; boolean loaded; + /* built in a game's map: the in-game widgets, not the main menu */ + boolean in_game; char root[300]; } menu_tags; @@ -382,6 +392,10 @@ static struct long *strings_tags; long *text_tags; long *spinner_tags; + /* which widgets, bitmaps and string lists are built */ + boolean *wanted_widgets; + boolean *wanted_bitmaps; + boolean *wanted_strings; boolean failed; } build; @@ -492,6 +506,70 @@ static long widget_named(char const *name) return find(name, build.menus->widget_count, widget_name); } +static boolean in_game_widget(char const *name) +{ + return !strncmp(name, IN_GAME_WIDGET_PREFIX, strlen(IN_GAME_WIDGET_PREFIX)); +} + +/* one of ours by its name (not the map's, with a backslash) wanted */ +static void want_named(char const *name, long count, char const *(*name_of)(long), boolean *wanted) +{ + long index = name && !strchr(name, '\\') ? find(name, count, name_of) : NONE; + + if (index != NONE) + wanted[index] = TRUE; +} + +static void want_widget(long index); + +static void want_widget_named(char const *name) +{ + long index = name && !strchr(name, '\\') ? widget_named(name) : NONE; + + if (index != NONE) + want_widget(index); +} + +/* a widget wanted, and what it names: its bitmaps and strings, its children, +its description, the widgets its handlers open and its conditional ones */ +static void want_widget(long index) +{ + struct halo_menus const *menus = build.menus; + struct halo_menu_widget const *widget = &menus->widgets[index]; + long child, handler, conditional; + + if (build.wanted_widgets[index]) + return; + build.wanted_widgets[index] = TRUE; + want_named(widget->bitmap, menus->bitmap_count, bitmap_name, build.wanted_bitmaps); + want_named(widget->header_bitmap, menus->bitmap_count, bitmap_name, build.wanted_bitmaps); + want_named(widget->footer_bitmap, menus->bitmap_count, bitmap_name, build.wanted_bitmaps); + want_named(widget->string_list, menus->string_list_count, strings_name, build.wanted_strings); + want_widget_named(widget->description); + for (child = widget->first_child; child != HALO_MENU_NONE; child = menus->children[child].next) + { + if (menus->children[child].nested != HALO_MENU_NONE) + want_widget(menus->children[child].nested); + else + want_widget_named(menus->children[child].widget); + } + for (handler = widget->first_handler; handler != HALO_MENU_NONE; handler = menus->handlers[handler].next) + { + struct halo_menu_handler const *on = &menus->handlers[handler]; + + want_widget_named(on->open); + want_widget_named(on->replace); + want_widget_named(on->focus); + want_widget_named(on->widget); + want_widget_named(on->otherwise); + } + for (conditional = widget->first_conditional; conditional != HALO_MENU_NONE; + conditional = menus->conditionals[conditional].next) + { + want_widget_named(menus->conditionals[conditional].widget); + } +} + static long map_tag(long group_tag, char const *name, char const *file, long line) { long index = tag_loaded(group_tag, name); @@ -1255,8 +1333,11 @@ void menu_tags_loaded( struct halo_menus const *menus; struct cache_file_tag_instance *instances; long widget_count, own_lists = 0, total, index; + long widgets_built = 0, strings_built = 0, bitmaps_built = 0; + /* (any map but ui.map is a game's) */ + boolean in_game = strcmp(map_name, "ui") != 0; - if (strcmp(map_name, "ui") || strcmp(config_string("display.menus"), "pc")) + if (strcmp(config_string("display.menus"), "pc")) return; menus = halo_menus_load(); if (!menus) @@ -1269,33 +1350,64 @@ void menu_tags_loaded( build.spinner_tags = malloc((widget_count + 1) * sizeof(long)); build.bitmap_tags = malloc((menus->bitmap_count + 1) * sizeof(long)); build.strings_tags = malloc((menus->string_list_count + 1) * sizeof(long)); + build.wanted_widgets = malloc((widget_count + 1) * sizeof(boolean)); + build.wanted_bitmaps = malloc((menus->bitmap_count + 1) * sizeof(boolean)); + build.wanted_strings = malloc((menus->string_list_count + 1) * sizeof(boolean)); + /* ui.map: all but the in-game widgets; a game's map: those, and what + they lead to */ + for (index = 0; index < widget_count; index++) + build.wanted_widgets[index] = !in_game && !in_game_widget(menus->widgets[index].name); + for (index = 0; index < menus->bitmap_count; index++) + build.wanted_bitmaps[index] = !in_game; + for (index = 0; index < menus->string_list_count; index++) + build.wanted_strings[index] = !in_game; + for (index = 0; in_game && index < widget_count; index++) + { + if (in_game_widget(menus->widgets[index].name)) + want_widget(index); + } for (index = 0; index < widget_count; index++) { - own_lists += (menus->widgets[index].text != NULL) + (menus->widgets[index].strings != NULL); + if (build.wanted_widgets[index]) + { + widgets_built++; + own_lists += (menus->widgets[index].text != NULL) + (menus->widgets[index].strings != NULL); + } /* (names are unique) */ if (widget_named(menus->widgets[index].name) != index) problem(menus->widgets[index].file, menus->widgets[index].line, "two widgets are named", menus->widgets[index].name); } - total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count; + for (index = 0; index < menus->string_list_count; index++) + strings_built += build.wanted_strings[index] != FALSE; + for (index = 0; index < menus->bitmap_count; index++) + bitmaps_built += build.wanted_bitmaps[index] != FALSE; + /* (a game's map with no in-game widgets: nothing to add) */ + if (!widgets_built && !build.failed) + goto done; + total = widgets_built + own_lists + strings_built + bitmaps_built; instances = build.failed ? NULL : instances_grow(total, &build.first_index, &build.first_salt); if (!instances) goto failed; /* each new tag's index */ for (index = 0; index < widget_count; index++) { - build.widget_tags[index] = next_tag(); - build.text_tags[index] = menus->widgets[index].text ? next_tag() : NONE; - build.spinner_tags[index] = menus->widgets[index].strings ? next_tag() : NONE; + boolean wanted = build.wanted_widgets[index]; + + build.widget_tags[index] = wanted ? next_tag() : NONE; + build.text_tags[index] = wanted && menus->widgets[index].text ? next_tag() : NONE; + build.spinner_tags[index] = wanted && menus->widgets[index].strings ? next_tag() : NONE; } for (index = 0; index < menus->string_list_count; index++) - build.strings_tags[index] = next_tag(); + build.strings_tags[index] = build.wanted_strings[index] ? next_tag() : NONE; for (index = 0; index < menus->bitmap_count; index++) - build.bitmap_tags[index] = next_tag(); + build.bitmap_tags[index] = build.wanted_bitmaps[index] ? next_tag() : NONE; /* the tags: the bitmaps and strings first, which the widgets name; the widgets' names in the table before any is built, which they find there */ for (index = 0; index < menus->bitmap_count && !build.failed; index++) { + if (!build.wanted_bitmaps[index]) + continue; instance_set(instances, BITMAP_GROUP_TAG, build.bitmap_tags[index], menus->bitmaps[index].name, "", bitmap_build(&menus->bitmaps[index], build.bitmap_tags[index])); } @@ -1303,6 +1415,8 @@ void menu_tags_loaded( { struct halo_menu_strings const *list = &menus->string_lists[index]; + if (!build.wanted_strings[index]) + continue; instance_set(instances, UNICODE_STRING_LIST_TAG, build.strings_tags[index], list->name, "", string_list_build(menus->strings + list->first, list->count)); } @@ -1310,6 +1424,8 @@ void menu_tags_loaded( { struct halo_menu_widget const *widget = &menus->widgets[index]; + if (!build.wanted_widgets[index]) + continue; if (widget->text) { instance_set(instances, UNICODE_STRING_LIST_TAG, build.text_tags[index], widget->name, " text", @@ -1329,10 +1445,17 @@ void menu_tags_loaded( goto failed; cache_files_set_tag_instances(instances, build.first_index + total); for (index = 0; index < widget_count && !build.failed; index++) - instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index); + { + if (build.wanted_widgets[index]) + instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index); + } if (build.failed) goto failed; - if (widget_named(menus->root) != NONE) + if (in_game) + { + /* (no main menu here) */ + } + else if (widget_named(menus->root) != NONE) { snprintf(menu_tags.root, sizeof(menu_tags.root), "%s%s", PC_MENU_TAG_PREFIX, menus->root); for (index = 0; menu_tags.root[index]; index++) @@ -1346,12 +1469,14 @@ void menu_tags_loaded( platform_log("menus: there is no widget named %s, the main menu", menus->root); } menu_tags.loaded = TRUE; + menu_tags.in_game = in_game; platform_log("menus: %ld widgets, %ld string lists and %ld bitmaps added to the map's %ld tags", - widget_count, own_lists + menus->string_list_count, menus->bitmap_count, build.first_index); + widgets_built, own_lists + strings_built, bitmaps_built, build.first_index); goto done; failed: - platform_log("menus: not added; using the game's own menus"); + platform_log(in_game ? "menus: not added; no SETTINGS in the pause menu" : + "menus: not added; using the game's own menus"); menu_tags_release(); done: @@ -1360,6 +1485,9 @@ done: free(build.spinner_tags); free(build.strings_tags); free(build.bitmap_tags); + free(build.wanted_widgets); + free(build.wanted_bitmaps); + free(build.wanted_strings); memset(&build, 0, sizeof(build)); } @@ -1377,7 +1505,7 @@ char const *pc_menus_root_name( settings' screens) */ char const *open = config_string("debug.menu_open"); - if (menu_tags.loaded && *open && build.menus == NULL) + if (menu_tags.loaded && !menu_tags.in_game && *open && build.menus == NULL) { static char name[300]; long index; @@ -1442,7 +1570,7 @@ char const *pc_menus_screen( }; short index; - if (!menu_tags.loaded) + if (!menu_tags.loaded || menu_tags.in_game) return name; for (index = 0; index < NUMBEROF(screens); index++) { diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index f5502acb..cf52aeb2 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -3694,6 +3694,16 @@ static void widget_instance_initialize( widget->render_regardless_of_controller_index = TEST_FLAG(definition->flags, _widget_render_regardless_of_controller_index_bit); widget->pause_game_time = TEST_FLAG(definition->flags, _widget_pause_game_time_bit); + /* port: the menus' screens (port/assets/menus) pause the game, as its + pause menu does, but not a network game, which its pause menu does not + pause (their SETTINGS: ui_pause_menu_loaded) */ + if (widget->pause_game_time && + pc_menu_tag(tag_index) && + !we_are_at_the_main_menu && + network_game_is_active()) + { + widget->pause_game_time = FALSE; + } widget->creation_time = widget_globals.current_system_milliseconds; widget->milliseconds_to_auto_close = MAX(definition->milliseconds_to_auto_close, 0); widget->auto_close_fade_time = MAX(definition->auto_close_fade_time, 0); @@ -3831,6 +3841,144 @@ void ui_widget_port_go_back( widget_instance_go_back_to_previous(widget); } +/* ---------- SETTINGS in the pause menus (port) + +The full screen pause menus (the campaign's, and a network game's with one +player here) get SETTINGS after RESUME GAME: the menus' own row for it +(port/assets/menus/ce/in_game.xml), which menu_tags.c builds in the game's +maps when the menus are there. It opens its screen as any widget opens +another, so B comes back here, to SETTINGS. The rows are found by their tags' +names, not their places, and spaced to fit the box (ui_pause_menu_rows_fit): +a row added after SETTINGS is spaced with the others. */ + +#define UI_PAUSE_MENU_SETTINGS_TAG "pc\\in_game\\settings_button" + +enum +{ + /* the body of the pause menus' box between the lines of its art + (ui\shell\bitmaps\pausebox2_*), and how far inside it the rows stay */ + UI_PAUSE_BOX_BODY_TOP = 2, + UI_PAUSE_BOX_BODY_BOTTOM = 130, + UI_PAUSE_BOX_ROWS_MARGIN = 4 +}; + +/* whether the widget is drawn from a tag whose name ends with the leaf */ +static boolean ui_pause_menu_tag_is( + struct widget_instance *widget, + char const *leaf) +{ + char const *name = tag_get_name(widget->definition_tag_index); + char const *last = name ? strrchr(name, '\\') : NULL; + + return last && !csstrcmp(last + 1, leaf); +} + +/* the rows spaced as the map spaces them, around the middle where it has +them, if they fit in the box's body; else the widest spacing that fits */ +static void ui_pause_menu_rows_fit( + struct widget_instance *list, + struct widget_instance *box) +{ + struct ui_widget_definition *definition = ui_widget_definition_get(list->definition_tag_index); + struct ui_widget_child_reference *references = + (struct ui_widget_child_reference *)definition->child_widgets.address; + struct ui_widget_definition *row_definition; + struct widget_instance *row; + short row_count = 0; + short row_height, pitch, middle_twice, body_top, body_bottom, span, top; + + for (row = list->child; row; row = row->next) + row_count++; + if (definition->child_widgets.count < 2 || row_count < 2) + return; + row_definition = ui_widget_definition_get(list->child->definition_tag_index); + row_height = row_definition->bounds.y1 - row_definition->bounds.y0; + pitch = references[1].vertical_offset - references[0].vertical_offset; + /* (on the screen, as the list's place and the box's are) */ + middle_twice = 2 * list->vertical_offset + references[0].vertical_offset + + references[definition->child_widgets.count - 1].vertical_offset + row_height; + body_top = box->vertical_offset + UI_PAUSE_BOX_BODY_TOP + UI_PAUSE_BOX_ROWS_MARGIN; + body_bottom = box->vertical_offset + UI_PAUSE_BOX_BODY_BOTTOM - UI_PAUSE_BOX_ROWS_MARGIN; + if ((row_count - 1) * pitch + row_height > body_bottom - body_top) + pitch = (body_bottom - body_top - row_height) / (row_count - 1); + span = (row_count - 1) * pitch + row_height; + top = PIN((middle_twice - span) / 2, body_top, body_bottom - span); + row_count = 0; + for (row = list->child; row; row = row->next) + row->vertical_offset = top - list->vertical_offset + row_count++ * pitch; + + return; +} + +static void ui_pause_menu_loaded( + struct widget_instance *root) +{ + static char const *const pause_menus[] = + { + "ui\\shell\\solo_game\\pause_game\\pause_game", + "ui\\shell\\multiplayer_game\\pause_game\\1p_pause_game", + }; + char const *name = tag_get_name(root->definition_tag_index); + long settings_tag_index; + struct widget_instance *box = NULL; + struct widget_instance *list = NULL; + struct widget_instance *resume = NULL; + struct widget_instance *child; + struct widget_instance *settings; + short index; + + for (index = 0; name && index < (short)NUMBEROF(pause_menus); index++) + { + if (!csstrcmp(name, pause_menus[index])) + break; + } + /* (and with one player here: the campaign's falls back on its full + screen pause menu for more, ui_check_for_pause_game) */ + if (!name || index == (short)NUMBEROF(pause_menus) || local_player_count() > 1) + return; + settings_tag_index = tag_loaded(UI_WIDGET_DEFINITION_TAG, UI_PAUSE_MENU_SETTINGS_TAG); + if (settings_tag_index == NONE) + return; + for (child = root->child; child; child = child->next) + { + if (ui_pause_menu_tag_is(child, "pause_dialog_bkd")) + { + box = child; + } + else if (child->type == _ui_widget_type_column_list && !resume) + { + for (resume = child->child; resume; resume = resume->next) + { + if (ui_pause_menu_tag_is(resume, "resume_game_button")) + break; + } + if (resume) + list = child; + } + } + if (!box || !list) + return; + settings = ui_widget_load_by_name_or_tag( + NULL, + settings_tag_index, + list, + list->local_player_index, + NONE, + NONE, + NONE); + if (!settings) + return; + settings->horizontal_offset = resume->horizontal_offset; + settings->previous = resume; + settings->next = resume->next; + if (resume->next) + resume->next->previous = settings; + resume->next = settings; + ui_pause_menu_rows_fit(list, box); + + return; +} + struct widget_instance *ui_widget_load_by_name_or_tag( char const *name, long tag_index, @@ -3925,6 +4073,10 @@ struct widget_instance *ui_widget_load_by_name_or_tag( tag_index, local_player_index, widget_stack); + /* port: SETTINGS in the pause menus, unless a created handler + closed the widget */ + if (!parent && widget_globals.active_widgets[widget_stack] == widget) + ui_pause_menu_loaded(widget); } else { @@ -7180,6 +7332,11 @@ static boolean ui_check_for_pause_game( else { ui_widget_delete(widget_globals.active_widgets[controller_index]); + /* port: and the screens B would have gone back to (the + pause menu, from its SETTINGS), as the campaign's START + closes them all (ui_widgets_close_all) */ + if (widget_globals.widget_stack[controller_index]) + dispose_widget_stack(&widget_globals.widget_stack[controller_index]); } } } diff --git a/tools/ce_menus.py b/tools/ce_menus.py index 728bb111..a31a6337 100644 --- a/tools/ce_menus.py +++ b/tools/ce_menus.py @@ -567,7 +567,8 @@ def main() -> None: lines += widget_xml(tag, widgets[tag], tags, functions, inputs, fonts, {}) lines.append("") (CE / f"{folder.replace('/', '.')}.xml").write_text("\n".join(lines) + "\n") - for name, lines in [*port_settings.settings_files().items(), *port_settings.multiplayer_files().items()]: + for name, lines in [*port_settings.settings_files().items(), *port_settings.multiplayer_files().items(), + *port_settings.in_game_files().items()]: (CE / name).write_text("\n".join(lines)) # their strings lines = ['', diff --git a/tools/port_settings.py b/tools/port_settings.py index 1068b120..bb1ed699 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -745,3 +745,92 @@ def multiplayer_files() -> dict: REPLACED_FOLDERS = [f"{MT}/server_settings"] + + +# ---------- in a game: SETTINGS in the pause menus + +# The game's pause menus are the maps' own, not these files', and ui.map has +# none of their art, so these widgets (in_game/...) are built in the game's +# maps instead, with the screens they open (port/linux/game/menu_tags.c); +# ui_widget.c adds SETTINGS to the pause menus after RESUME GAME. Its screen +# is the multiplayer pause menu's box and key, from the maps' own tags (every +# campaign and multiplayer map has them), with rows as the pause menus have +# them, named and ordered as Edit Profile has them. Each opens Edit Profile's +# screen over the pause menu's dim (behind them, the main menu has its dark +# backdrop, a game the game), and the screen saves config.toml's settings as +# it does there; GAMEPADS, on OK, only the controller settings of the +# player's active profile ("port active profile edit begin" and "end", +# port/linux/game/menu_functions.c). +# The desktop builds' only, for now. +IG = "in_game" +PAUSE_DIM = "ui\\shell\\bitmaps\\semi_transparent_grey" +PAUSE_ROW = [("type", "text"), ("width", 202), ("height", 27), ("bitmap", "ui\\shell\\bitmaps\\menu_bkds"), + ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("align", "center"), ("text_y", 3)] +DESKTOP = [("platform", "desktop")] +# Edit Profile's setups: their rows' names (its profile_edit_options), and +# the screens they open +IN_GAME_SETUPS = [("controls_setup", 1, f"{PE}/controls_setup/controls_settings_screen"), + ("gamepads", 2, f"{PE}/gamepad_setup/gamepad_setup_screen"), + ("mouse_setup", 3, f"{PE}/mouse_settings/mouse_settings_screen"), + ("audio_setup", 4, f"{PE}/audio_settings/audio_settings_screen"), + ("video_setup", 5, f"{PE}/video_settings/video_settings_screen")] +# the box where the multiplayer pause menu has it; the body of its art +# (pausebox2_*) between its rows 2 and 130, the key's band below; the rows +# that far inside it (ui_widget.c's UI_PAUSE_BOX_ROWS_MARGIN), spaced as the +# multiplayer pause menu's at most +BOX_Y, BOX_BODY_TOP, BOX_BODY_BOTTOM, ROWS_MARGIN, ROW_HEIGHT, ROW_SPACING = 164, 2, 130, 4, 27, 35 + + +def _pause_row(name: str, label: list, handlers: list) -> list: + return _widget(name, label + PAUSE_ROW + DESKTOP, handlers) + + +def _opens(screen: str, run: str = None) -> list: + action = (f'run="{run}" ' if run else "") + f'open="{screen}"' + return [f'', f''] + + +def _rows_place(count: int) -> tuple: + """the rows' top and spacing: in the middle of the box's body""" + body = BOX_BODY_BOTTOM - BOX_BODY_TOP + spacing = min(ROW_SPACING, (body - 2 * ROWS_MARGIN - ROW_HEIGHT) // (count - 1)) + span = (count - 1) * spacing + ROW_HEIGHT + return BOX_Y + BOX_BODY_TOP + (body - span) // 2, spacing + + +def in_game_files() -> dict: + """SETTINGS in the pause menus, its screen, and the setups' screens""" + top, spacing = _rows_place(len(IN_GAME_SETUPS)) + lines = _pause_row(f"{IG}/settings_button", [("text", "SETTINGS")], _opens(f"{IG}/settings_screen")) + lines += _widget(f"{IG}/settings_screen", + [("width", 640), ("height", 480), ("flags", "pass_unhandled_to_focused_child pause_game"), + ("bitmap", PAUSE_DIM)] + DESKTOP, + ['', '', + f'', + f'', + '']) + lines += _widget(f"{IG}/settings_box", [("width", 226), ("height", 154)] + DESKTOP, + ['', + '', + '']) + lines += _widget(f"{IG}/settings_list", + [("type", "column_list"), ("width", 202), + ("height", (len(IN_GAME_SETUPS) - 1) * spacing + ROW_HEIGHT), + ("flags", "pass_unhandled_to_focused_child up_down_tabs_items")] + DESKTOP, + [f'' + for index, (key, _, _) in enumerate(IN_GAME_SETUPS)]) + for key, string_index, screen in IN_GAME_SETUPS: + # (GAMEPADS sets the profile's controller settings: the player's + # active profile, being edited while its screen is up) + gamepads = key == "gamepads" + lines += _pause_row(f"{IG}/{key}_button", + [("string_list", f"{PE}/profile_edit_options"), ("string_index", string_index)], + _opens(f"{IG}/{key}_screen", "port active profile edit begin" if gamepads else None)) + lines += _widget(f"{IG}/{key}_screen", + [("width", 640), ("height", 480), ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", PAUSE_DIM)] + DESKTOP, + ([''] if gamepads else []) + + [f'']) + return {"in_game.xml": ['', + "", + "", *lines, "", ""]} From 124a3cc5846e5861cc65ae2e10ddf7bc08537403 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Sun, 4 Oct 2026 00:50:13 +0200 Subject: [PATCH 015/187] Stop a touch drag at a PC list's first and last usable row --- source/interface/ui_widget.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 44337a24..96d6fcee 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -6408,6 +6408,24 @@ static struct widget_instance *ui_mouse_wheel_widget( return result; } +/** + * @brief Whether the focus passes over a child of a list, which gives a + * press no place to land. The PC version's lists skip their labels and + * hidden rows (widget_instance_port_is_label) at both ends and wrap past + * them, so counting one would let a drag step past the last usable row. + * + * @param list the list the child is in + * @param child the child + * @return TRUE if the PC list's focus skips the child + */ +static boolean ui_mouse_wheel_skips_child( + struct widget_instance *list, + struct widget_instance *child) +{ + return list->type == _ui_widget_type_column_list && pc_menu_tag(list->definition_tag_index) && + widget_instance_port_is_label(child); +} + /** * @brief How many presses of a d-pad button can step the focus before it * would wrap around to the other end. @@ -6456,9 +6474,10 @@ static long ui_mouse_wheel_room( { struct ui_widget_definition *child_definition = ui_widget_definition_get(child->definition_tag_index); - if (child_definition->event_handlers.count > 0 || + if ((child_definition->event_handlers.count > 0 || TEST_FLAG(child_definition->flags, _widget_pass_unhandled_events_to_children_bit) || - list) + list) && + !ui_mouse_wheel_skips_child(widget, child)) { room++; } @@ -6489,7 +6508,7 @@ static long ui_mouse_wheel_room( child; child = forward ? child->next : child->previous) { - if (!child->disabled) + if (!child->disabled && !ui_mouse_wheel_skips_child(widget, child)) room++; } if (forward && widget->parameters.list.number_of_items > 0) From 38a85a9a1a675ea44c14d4007a5d98c23fa4b20c Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Sun, 4 Oct 2026 08:04:07 +0200 Subject: [PATCH 016/187] Let a touch drag leave a PC screen's button bar --- source/interface/ui_widget.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 96d6fcee..57e1f21b 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -6727,6 +6727,15 @@ static void ui_widgets_process_mouse( if (widget) ui_mouse_list_directions(widget, &back, &forward); + /* a PC screen's button bar steps sideways only, and the d-pad's up + and down pass over it to the list that holds it, so a touch drag + leaves the bar the same way */ + if (touch && widget && back == _widget_event_dpad_left && pc_menu_tag(widget->definition_tag_index) && + ui_mouse_wheel_room(ui_mouse_menu(), _widget_event_dpad_up, FALSE) != NONE) + { + back = _widget_event_dpad_up; + forward = _widget_event_dpad_down; + } button = going_forward ? forward : back; /* only a touch drag stops at the ends, and does nothing with no list to step; the desktop wheel wraps as the d-pad does, and falls back From d1da19316e91b6b877147d15403e8f09ee509890 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Sun, 4 Oct 2026 08:39:12 +0200 Subject: [PATCH 017/187] Trim comments to the repository's style --- .../java/com/halo/decomp/HaloActivity.java | 33 +- port/android/host/host_main.c | 27 +- port/linux/game/touch_game.c | 14 +- port/linux/include/halo_ui_pointer.h | 13 +- port/linux/src/d3d8_gl.c | 12 +- port/linux/src/sdl_platform.h | 32 +- port/linux/src/touch_input.c | 23 +- port/linux/src/touch_input.h | 30 +- port/linux/src/touch_menu.c | 18 +- port/linux/src/touch_menu.h | 60 +--- port/linux/src/xinput_sdl.c | 12 +- source/interface/ui_widget.c | 308 ++++++------------ source/interface/virtual_keyboard.h | 36 +- 13 files changed, 198 insertions(+), 420 deletions(-) diff --git a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java index b713ab61..4d907a47 100644 --- a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java +++ b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java @@ -20,9 +20,9 @@ public class HaloActivity extends SDLActivity { /** lets system link's broadcasts in over Wi-Fi while the game runs */ private WifiManager.MulticastLock multicastLock; /** - * The latest system gesture insets {left, top, right, bottom}, in pixels - * of the current orientation. Written on the UI thread, read by the - * game's thread: replaced as a whole, never changed in place. + * the latest system gesture insets {left, top, right, bottom}, in pixels; + * written on the UI thread, read by the game's thread: replaced as a + * whole, never changed in place */ private volatile int[] gestureInsets = new int[] { 0, 0, 0, 0 }; @@ -51,11 +51,11 @@ public class HaloActivity extends SDLActivity { } /** - * Keeps gestureInsets current. Android sends the insets again when the - * activity rotates, which happens after the game's native code has - * started on a phone launched from portrait, so a single read at startup - * would keep the portrait values. The listener hands the insets on, so - * that SDL's own handling (on its surface) still sees them. + * keeps gestureInsets current; Android sends the insets again when the + * activity rotates (after the game's native code has started on a phone + * launched from portrait), so a single read at startup would keep the + * portrait values; the listener hands the insets on so SDL's own + * handling still sees them */ private void trackGestureInsets() { getWindow().getDecorView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { @@ -81,15 +81,16 @@ public class HaloActivity extends SDLActivity { } /** - * The edges of the screen where Android keeps its gestures. In sticky + * the edges of the screen where Android keeps its gestures; in sticky * full screen the first swipe from an edge only shows the system bars, - * and Android hands that swipe to the game as an ordinary finger, so the - * game (port/linux/src/touch_input.c) must ignore touches that begin - * there. The game's thread calls this through JNI at every finger down. - * - * @return a copy of {left, top, right, bottom} in pixels; all 0 before - * Android 10, which has no such insets, and until the first - * insets arrive + * and Android hands that swipe to the game as an ordinary finger, so + * the game (port/linux/src/touch_input.c) must ignore touches that + * begin there; returns a copy of {left, top, right, bottom} in pixels: + * all 0 before Android 10 (which has no insets) and until the first + * insets arrive. The game's native code calls this through JNI by name + * and signature (host_main.c GetMethodID(..., + * "getSystemGestureInsetsPixels", "()[I")), at every finger down, so + * it must not be renamed, retyped or removed as unused */ public int[] getSystemGestureInsetsPixels() { return gestureInsets.clone(); diff --git a/port/android/host/host_main.c b/port/android/host/host_main.c index 593f238a..2985f989 100644 --- a/port/android/host/host_main.c +++ b/port/android/host/host_main.c @@ -136,16 +136,13 @@ struct environment int count; }; -/** - * @brief Asks the activity for Android's system gesture insets, in pixels - * (HaloActivity.getSystemGestureInsetsPixels). The guest calls it at every - * finger down: the insets change when the phone rotates. The method is - * found on the activity's own class: the game's native thread has the - * system's class loader, which does not know the app's classes. A change is - * logged, so that the log shows the value the game works with. - * @param insets receives left, top, right, bottom; all 0 on any failure (the - * touch controls then use the whole screen) - */ +/* asks the activity for Android's system gesture insets via JNI +(HaloActivity.getSystemGestureInsetsPixels); the guest calls it at every +finger down because the insets change when the phone rotates; the method is +found on the activity's own class because the game's native thread has the +system's class loader, which does not know the app's classes; a change is +logged so the log shows the value the game works with; all 0 on any failure +(the touch controls then use the whole screen) */ void host_gesture_insets(int *insets) { static int logged[4] = { -1, -1, -1, -1 }; @@ -283,13 +280,9 @@ static uint32_t make_boot(const struct environment *environment) #define MAIN_STACK_SIZE (16 * 1024 * 1024) -/** - * @brief The thread that runs the game: passes the display and its density, - * the time zone and the data and save folders to the guest through its - * environment, and runs the guest's main. - * @param unused the thread argument, not used - * @return never returns normally - */ +/* the thread that runs the game: passes the display and its density, the +time zone and the data and save folders to the guest through its +environment, and runs the guest's main; never returns normally */ static void *game_main(void *unused) { struct environment environment = { { 0 }, 0 }; diff --git a/port/linux/game/touch_game.c b/port/linux/game/touch_game.c index 18fa43bd..eaa8d336 100644 --- a/port/linux/game/touch_game.c +++ b/port/linux/game/touch_game.c @@ -10,15 +10,11 @@ game. The platform layer cannot see the game's types, so it asks here #include "game/game.h" #include "cutscene/cinematics.h" -/** - * @brief Asks whether a cinematic is playing that A would skip - * (player_control.c, player_control_action_test_check_reset_input_blob). - * The controller is read before the game exists (input_initialize, during - * shell_initialize), so cinematic_globals is tested first: game_initialize - * makes it after the game time globals that game_in_progress reads - * (game.c). - * @return nonzero if A would skip the cinematic now - */ +/* asks whether a cinematic is playing that A would skip +(player_control.c, player_control_action_test_check_reset_input_blob); tests +cinematic_globals first because the controller is read before the game exists +during input_initialize and shell_initialize, and game_initialize makes it +after the game time globals that game_in_progress reads (game.c) */ int touch_game_cinematic_skippable(void) { return cinematic_globals && game_in_progress() && cinematic_in_progress() && diff --git a/port/linux/include/halo_ui_pointer.h b/port/linux/include/halo_ui_pointer.h index 417cf3b1..209007a9 100644 --- a/port/linux/include/halo_ui_pointer.h +++ b/port/linux/include/halo_ui_pointer.h @@ -32,15 +32,10 @@ struct halo_ui_pointer short down_x, down_y; }; -/** - * @brief Gives the menus the pointer. Frees the mouse for the menus while - * menus_active and captures it again for aiming when not (the - * touchscreen: taps and drags go to the menus while menus_active). - * @param menus_active nonzero while a menu is up - * @param pointer receives what the pointer did since the last call, in the - * menus' 640x480 coordinates - * @return nonzero while menus are active and the pointer could be read - */ +/* gives the menus the pointer; frees the mouse for the menus while +menus_active and captures it for aiming when not (touchscreen: taps and +drags go to the menus while menus_active); returns nonzero while menus are +active and the pointer could be read */ int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer); #endif diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 82828c00..74668f0e 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -1151,14 +1151,10 @@ HRESULT WINAPI Direct3D_CreateDevice(UINT adapter, D3DDEVTYPE device_type, void /* ---------- the menus' pointer */ -/** - * @brief Turns a point in the window, as SDL reports it, into the menus' - * coordinates: the inverse of the letterboxed display blit at presentation, - * the screen's width and the menus' centering (halo_screen_ui_offset). - * @param window_x,window_y the point, in the window's units - * @param x,y receive the point in 640x480 menu units; -1 if there is no - * back buffer or window yet - */ +/* a point in the window, as SDL reports it, in the menus' coordinates: the +inverse of the letterboxed display blit at presentation, the screen's +width and the menus' centering (halo_screen_ui_offset); x and y are -1 if +there is no back buffer or window yet */ static void ui_point_from_window(float window_x, float window_y, short *x, short *y) { struct render_target_entry *back_buffer = render_target_get(&device.back_buffer); diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index 9ef9ec25..7ccc96a9 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -72,12 +72,9 @@ display.interpolation is false (port/linux/game/render_interpolation.c) */ int halo_interpolation_enabled(void); void platform_mouse_capture(BOOL capture); -/** - * @brief Handles the window's and the input devices' events. Main thread - * only, a no-op elsewhere. On Android this is where the finger events reach - * touch_input.c, which is why that module's state is only touched from the - * main thread (see touch_input.h). - */ +/* handles the window's and input devices' events; main thread only, a no-op +elsewhere; on Android this is where finger events reach touch_input.c, which +is why that module's state is only touched from the main thread */ void platform_pump_events(void); /* a snapshot of the input state; consume_motion resets the mouse deltas */ void platform_input_read(struct platform_input_state *state, BOOL consume_motion); @@ -98,26 +95,17 @@ struct platform_ui_pointer /* the pointer is the touchscreen (touch_input.c), not a mouse */ BOOL touch; }; -/** - * @brief Says whether a menu is up. The mouse is released for the menus - * (desktop), or the touchscreen goes to them (Android). A change of mode - * drops the gesture in progress. - * @param active nonzero while a menu is up - */ +/* says whether a menu is up; the mouse is released for the menus (desktop), +or the touchscreen goes to them (Android); a change of mode drops the gesture +in progress */ void platform_ui_pointer_set_active(BOOL active); -/** - * @brief What the pointer did since the last call. - * @param pointer receives the position, the clicks and the wheel steps - * @return nonzero while a menu is up (platform_ui_pointer_set_active) - */ +/* pointer receives what the pointer did since the last call; returns +nonzero while a menu is up (platform_ui_pointer_set_active) */ BOOL platform_ui_pointer_read(struct platform_ui_pointer *pointer); -/** - * @brief The window's size in the units that pointer positions come in, - * which differ from the drawable's pixels on displays that scale. - * @param width,height receive the size - */ +/* returns the window's size in the units that pointer positions come in, +which differ from the drawable's pixels on displays that scale */ void platform_video_window_size(int *width, int *height); BOOL platform_next_keystroke(struct platform_keystroke *keystroke); /* the multiplayer scoreboard (game_engine.c) open or not: while it is, the diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index 2129a2f0..657936b8 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -37,12 +37,8 @@ game must see the press */ static int skip_polls; #define TOUCH_PRESS_POLLS 2 -/** - * @brief The pixels in a dp: Android's densityDpi / 160, or else a guess - * from the height. - * @return pixels a dp; 0 while neither is known (the window does not exist - * yet) - */ +/* returns pixels per dp; 0 only while neither the density +(HALO_DISPLAY_DENSITY) nor the window's size is known */ static float pixels_per_dp(void) { const char *density = getenv("HALO_DISPLAY_DENSITY"); @@ -55,10 +51,7 @@ static float pixels_per_dp(void) return height > 0 ? (float)height / 360.0f : 0.0f; } -/** - * @brief Sets the gestures' sizes up, once they can be known; until then a - * dp is a pixel, there are no gesture zones and the next call tries again. - */ +/* sets up gesture sizes once they can be known; tries again until they are */ static void menu_setup(void) { if (!menu_ready) @@ -81,14 +74,8 @@ static void menu_setup(void) } } -/** - * @brief Brings the gesture zones and the window's size up to date. It runs - * at every finger down, not once at startup: the phone rotates after the app - * has started, and the insets of the portrait screen would stay. The zones - * matter only at a down, so changing them between two fingers is safe. - * Without Android's insets (desktop) there are no zones. - * @param width,height the window's size in pixels - */ +/* updates gesture zones at every finger down: the phone rotates after startup +and zones matter only at a down, so changing them between two fingers is safe */ static void refresh_zones(int width, int height) { #ifdef HALO_ANDROID diff --git a/port/linux/src/touch_input.h b/port/linux/src/touch_input.h index 92257507..2ce8efb2 100644 --- a/port/linux/src/touch_input.h +++ b/port/linux/src/touch_input.h @@ -17,36 +17,22 @@ on the game's main thread: a call from any other thread needs the lock. #include -/** - * @brief Feeds one finger event to the menus' pointer. - * @param type SDL_EVENT_FINGER_DOWN, _MOTION, _UP or _CANCELED - * @param finger the event; its x and y are 0..1 of the window - */ +/* feeds one finger event to the menus' pointer; x and y are 0..1 of the window */ void touch_input_event(unsigned int type, const SDL_TouchFingerEvent *finger); -/** - * @brief The window lost the focus: every touch ends without effect. - */ +/* the window lost the focus: every touch ends without effect */ void touch_input_cancel(void); -/** - * @brief Says whether the menus' pointer is used (a menu is up). Any - * touch in progress is dropped, so that it does not act in the other mode. - * @param active nonzero while a menu is up - */ +/* sets whether the menus' pointer is used (a menu is up); any touch in +progress is dropped so it does not act in the other mode */ void touch_input_menu_set_active(int active); -/** - * @brief What the fingers did in the menus since the last read. - * @param pointer receives the taps and the wheel steps; marked as touch - */ +/* reads what the fingers did in the menus since the last read; pointer is +marked as touch */ void touch_input_menu_read(struct platform_ui_pointer *pointer); -/** - * @brief Adds the touch controls to controller 1's state. Outside the menus - * a tap presses A for a few polls when a cinematic can be skipped. - * @param pad the state XInputGetState is about to return - */ +/* adds the touch controls to the gamepad state; outside the menus a tap +presses A for a few polls when a cinematic can be skipped */ void touch_input_gamepad(XINPUT_GAMEPAD *pad); #endif diff --git a/port/linux/src/touch_menu.c b/port/linux/src/touch_menu.c index a7598baf..fadac2ea 100644 --- a/port/linux/src/touch_menu.c +++ b/port/linux/src/touch_menu.c @@ -22,14 +22,8 @@ void touch_menu_reset(struct touch_menu *menu) touch_menu_init(menu, &settings); } -/** - * @brief Whether x is in the left or right gesture zone: [0, edge_left) and - * [width - edge_right, width). A zone with no inset does not exist (x at the - * window's edge is not in it), and neither does the right one without a - * window size: it would start at a negative x and cover the screen. - * @param menu the state (the zones are in its settings) - * @param x the finger's x, in pixels - */ +/* checks whether x is in the left or right gesture zone; a zone with no inset +does not exist, and the right zone needs a window size */ static int in_side_zone(const struct touch_menu *menu, float x) { const struct touch_menu_settings *s = &menu->settings; @@ -37,12 +31,8 @@ static int in_side_zone(const struct touch_menu *menu, float x) return x < s->edge_left || (s->edge_right > 0.0f && s->width > 0.0f && x >= s->width - s->edge_right); } -/** - * @brief Whether y is in the top or bottom gesture zone; as the side zones, - * the bottom one needs an inset and the window's height. - * @param menu the state (the zones are in its settings) - * @param y the finger's y, in pixels - */ +/* checks whether y is in the top or bottom gesture zone; like the side zones, +the bottom one needs an inset and the window's height */ static int in_top_or_bottom_zone(const struct touch_menu *menu, float y) { const struct touch_menu_settings *s = &menu->settings; diff --git a/port/linux/src/touch_menu.h b/port/linux/src/touch_menu.h index 29c9c245..6a8a3fc6 100644 --- a/port/linux/src/touch_menu.h +++ b/port/linux/src/touch_menu.h @@ -82,65 +82,31 @@ struct touch_menu struct touch_menu_output output; }; -/** - * @brief Prepares a menu pointer; the settings are copied. - * @param menu the state to set up (all of it is cleared) - * @param settings the gesture sizes, in pixels, and the steps a read - */ +/* prepares a menu pointer; the state is cleared and settings are copied */ void touch_menu_init(struct touch_menu *menu, const struct touch_menu_settings *settings); -/** - * @brief Forgets the finger, the pending steps and the unread taps but - * keeps the settings: what a finger began before a menu opened or closed - * must not act in the other one. - * @param menu the state to clear - */ +/* forgets the finger, pending steps and taps but keeps settings: what a +finger began before a menu opened or closed must not act in the other one */ void touch_menu_reset(struct touch_menu *menu); -/** - * @brief A finger went down. A second finger is ignored while one is - * down. The steps still waiting are dropped, so that a tap hits what the - * finger sees on a list still scrolling. The down is counted and its point - * kept for the next read (downs, down_x, down_y), even when the finger goes - * on to scroll. - * @param menu the state - * @param finger the finger's id - * @param x,y where it went down, in pixels - */ +/* a finger went down; a second finger is ignored, and pending steps are +dropped so a tap hits what the finger sees on a list still scrolling */ void touch_menu_down(struct touch_menu *menu, unsigned long long finger, float x, float y); -/** - * @brief The finger moved. Once it is further than the slop it scrolls; - * the steps count from the slop's edge, so that the slop is not a step - * and a fast first sample is not lost. - * @param menu the state - * @param finger the finger's id; other fingers are ignored - * @param x,y where it is, in pixels - */ +/* the finger moved; once further than the slop it scrolls, with steps +counted from the slop's edge so the slop is not a step and a fast first +sample is not lost */ void touch_menu_move(struct touch_menu *menu, unsigned long long finger, float x, float y); -/** - * @brief The finger went up: a tap if it never scrolled, at the place - * where it went down. - * @param menu the state - * @param finger the finger's id; other fingers are ignored - * @param x,y where it went up, in pixels (the last part of a drag counts) - */ +/* the finger went up; a tap if it never scrolled, at the place where it +went down (the last part of a drag counts) */ void touch_menu_up(struct touch_menu *menu, unsigned long long finger, float x, float y); -/** - * @brief The system took the touch away: the gesture neither taps nor - * scrolls. - * @param menu the state - */ +/* the system took the touch away: the gesture neither taps nor scrolls */ void touch_menu_cancel(struct touch_menu *menu); -/** - * @brief Hands out what happened since the last read. At most - * steps_per_read wheel steps come out; the rest waits for the next reads. - * @param menu the state - * @param output receives the pointer, the taps and the wheel steps - */ +/* hands out what happened since the last read; at most steps_per_read +wheel steps come out; the rest waits for the next read */ void touch_menu_read(struct touch_menu *menu, struct touch_menu_output *output); #endif diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index 1585e4e5..3274471b 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -787,14 +787,10 @@ static int controller_port(HANDLE device) return -1; } -/** - * @brief Reads a controller's state. For port 0 the keyboard, the mouse, - * the debug input and the touchscreen are merged into the first gamepad's. - * Runs on the game's main thread (touch_input_gamepad relies on it). - * @param device the controller's handle - * @param state receives the state (cleared first) - * @return ERROR_SUCCESS, or ERROR_DEVICE_NOT_CONNECTED for an unknown port - */ +/* reads a controller's state; for port 0 the keyboard, mouse, debug input +and touchscreen are merged into the first gamepad's; runs on the game's main +thread (touch_input_gamepad relies on it); returns ERROR_SUCCESS or +ERROR_DEVICE_NOT_CONNECTED for an unknown port */ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state) { int port = controller_port(device); diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 57e1f21b..eaeedf25 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5538,10 +5538,8 @@ static long ui_debug_frame, ui_debug_click_frame; void platform_log(char const *format, ...); int config_boolean(char const *name); -/** - * @brief Whether debug.touch_targets is on. Read once: the settings do not - * change while the game runs. - */ +/* whether debug.touch_targets is on; read once: settings do not change +while the game runs */ static boolean ui_debug_targets_enabled( void) { @@ -5553,13 +5551,10 @@ static boolean ui_debug_targets_enabled( return enabled != 0; } -/** - * @brief Outlines a rectangle, 2 menu pixels thick, with the game's own quad - * drawing, in the menus' coordinates (the caller has the menus' centering - * offset on). - * @param bounds the rectangle to outline, in menu coordinates - * @param color the outline's ARGB color - */ +/* outlines a rectangle, 2 menu pixels thick, in menu coordinates; the +caller has the menus' centering offset on (render.c sets +halo_screen_ui_offset(TRUE) around it; adding the offset inside would +double it) */ static void ui_debug_draw_outline( rectangle2d const *bounds, pixel32 color) @@ -5582,12 +5577,7 @@ static void ui_debug_draw_outline( return; } -/** - * @brief Draws a cross of about 8 menu pixels at a mark that is still - * within its 3 seconds. - * @param mark the remembered point and when it happened - * @param color the cross's ARGB color - */ +/* draws a cross of about 8 menu pixels at a mark that is still within its 3 seconds */ static void ui_debug_draw_mark( struct ui_debug_mark const *mark, pixel32 color) @@ -5613,14 +5603,10 @@ static void ui_debug_draw_mark( return; } -/** - * @brief Outlines the targets the pointer code collected this frame (or - * the virtual keyboard's keys while it is up), and the last finger-down and - * tap points. Only for the render that notes the targets (the first - * player's): a split-screen viewport that is not hit-tested shows nothing, - * or it would show outlines that no tap uses. - * @param first_players_render whether this render is the one that notes targets - */ +/* outlines the targets the pointer code collected this frame and the last +finger-down and tap points; only for the render that notes the targets +(the first player's): a split-screen viewport that is not hit-tested shows +nothing or it would show outlines that no tap uses */ static void ui_debug_draw_targets( boolean first_players_render) { @@ -5654,13 +5640,8 @@ static void ui_debug_draw_targets( return; } -/** - * @brief Remembers a finger-down or tap point for the debug view, to show - * it for 3 seconds. - * @param mark receives the point and the time - * @param x horizontal position, in menu coordinates - * @param y vertical position, in menu coordinates - */ +/* remembers a finger-down or tap point for the debug view, to show it +for 3 seconds */ static void ui_debug_set_mark( struct ui_debug_mark *mark, short x, @@ -5784,13 +5765,8 @@ static boolean ui_mouse_widget_is_item( widget->type == _ui_widget_type_column_list; } -/** - * @brief Notes a target that steps a list by one, unless it is empty or the - * targets are full. - * @param widget the list the target steps - * @param bounds where the target is, render offset included - * @param kind _ui_mouse_target_list_back or _ui_mouse_target_list_forward - */ +/* notes a target that steps a list by one, unless it is empty or the +targets are full */ static void ui_mouse_note_list_step( struct widget_instance *widget, rectangle2d const *bounds, @@ -5821,27 +5797,15 @@ static void ui_mouse_note_list_step( return; } -/** - * @brief Notes the band that steps a list showing several items side by - * side: the band of the slots' rows (from the top of the highest slot to - * the bottom of the lowest) left of the first slot, which steps back, and - * right of the last, which steps forward, out to the list's own bounds. - * - * A stock several-items list (the map list, the profile slots) covers the - * whole 640x480 screen, so a tap anywhere beside its slots cannot mean - * "step": the band is limited to the slots' rows, and a tap on the title, - * between slots or under them does nothing. The caller notes nothing for - * the list itself, and widget_instance_render_recursive notes the slots - * after this; ui_mouse_target_at takes the last target noted under a point, - * so a slot would win over the band, which never covers one anyway. A list - * the d-pad steps up and down has no side to tap and gets no band (no stock - * list does). - * - * @param widget the list - * @param definition its definition - * @param offset the render offset, the list's own offset included (as - * ui_mouse_note_target gets it) - */ +/* notes the band that steps a list showing several items side by side: +left of the first slot (steps back), right of the last (steps forward). A +stock several-items list covers the whole 640x480 screen, so a tap beside +its slots cannot mean "step"; the band is limited to the slots' rows, and a +tap on the title, between slots or under them does nothing. The caller notes +nothing for the list itself, and widget_instance_render_recursive notes the +slots after this; ui_mouse_target_at takes the last target noted under a +point, so a slot would win over the band. A list the d-pad steps up and down +has no side to tap and gets no band */ static void ui_mouse_note_slot_band( struct widget_instance *widget, struct ui_widget_definition const *definition, @@ -5896,15 +5860,10 @@ static void ui_mouse_note_slot_band( return; } -/** - * @brief Notes a widget as a tap target of this frame, if it is one: an - * item, a value, a list slot, or a legend button (kept in parts, which - * ui_mouse_fit_button_targets settles into the tap area). A list showing - * several items notes the band beside its slots (ui_mouse_note_slot_band). - * @param widget the widget being rendered - * @param definition the widget's definition - * @param offset where the widget's parent is drawn, in menu coordinates - */ +/* notes a widget as a tap target of this frame, if it is one: an item, +a value, a list slot, or a legend button (kept in parts that +ui_mouse_fit_button_targets settles into the tap area); a list showing +several items notes the band beside its slots */ static void ui_mouse_note_target( struct widget_instance *widget, struct ui_widget_definition const *definition, @@ -6013,21 +5972,16 @@ static void ui_mouse_note_target( return; } -/** - * @brief Makes a setting row its value: an item whose only value child is a - * value stops being a target, and the value takes the row's height. The row - * is where the thin value box sits among a label and empty space, and a tap - * anywhere else on it would press A on the row, which on a setting screen - * is ACCEPT. The value keeps its width, and so its left half / right half - * rule. Only the value's parent is the row: that is how the controller and - * gametype screens nest them (op_* row, *_spinner child). An item holding - * several values (a panel, a list of spinners) is not a row: it and its - * values keep their own areas, since growing each value to the item's height - * would pile up boxes of which only the last noted could be tapped. - * Runs after ui_mouse_fit_button_targets so that the legends are fitted - * against the whole row, as before: a legend growing up into a row's label - * area would press ACCEPT there. - */ +/* makes a setting row its value: an item whose only value child is a value +stops being a target, and the value takes the row's height. A tap anywhere +else on the row would press A, which on a setting screen is ACCEPT. The +value keeps its width and left/right half rule. Only the value's parent is +the row; an item holding several values (a panel, a list of spinners) is not +a row and keeps its own area. Only exactly one value merges because growing +several values to the item's height would pile up boxes of which only the +last noted could be tapped. Runs after ui_mouse_fit_button_targets so the +legends are fitted against the whole row: a legend growing up into a row's +label area would press ACCEPT there */ static void ui_mouse_merge_setting_rows( void) { @@ -6071,19 +6025,17 @@ static void ui_mouse_merge_setting_rows( return; } -/** - * @brief Doubles the width of each value (a setting), around its centre: its - * arrows are small for a finger. Each half grows outward by half the box's - * width. A box is not held to its row (the last arrow of a row sits near - * the row's end), but stops at the edge of a non-button target beside it - * that it overlaps vertically, at the midpoint of the gap to another value - * facing it (so neither takes the other's room, whatever the order they - * were noted in: every measure is against the boxes as noted, not as already - * widened), and at the menu's drawable area. A box never shrinks. Where a - * limit holds one side back, the split between previous and next stays at - * the original centre, where the arrows are. Runs after - * ui_mouse_merge_setting_rows, once the rows have gone, and once a frame. - */ +/* doubles the width of each value (a setting) around its centre: its arrows +are small for a finger. Each half grows outward by half the box's width. +The noted[] snapshot holds the boxes as noted; every measure is against them, +not as already widened, so two neighbours never take each other's room +whatever the order they were noted in. A box is not held to its row (the +last arrow of a row sits near the row's end) but stops at the edge of a +non-button target beside it that it overlaps vertically, at the midpoint of +the gap to another value facing it, and at the menu's drawable area. A box +never shrinks. Where a limit holds one side back, the split between previous +and next stays at the original centre where the arrows are. Runs after +ui_mouse_merge_setting_rows, once the rows have gone, and once a frame */ static void ui_mouse_widen_values( void) { @@ -6131,13 +6083,9 @@ static void ui_mouse_widen_values( return; } -/** - * @brief The right edge of a legend label's text as the game draws it, or - * NONE when it cannot be measured (no text, no font, or icons in the string, - * which the measuring does not account for). - * @param label the label's text box widget - * @param label_bounds the text box's bounds where it is drawn, in menu coordinates - */ +/* the right edge of a legend label's text as the game draws it, or NONE +when it cannot be measured (no text, no font, or icons in the string, which +the measuring does not account for) */ static short ui_mouse_label_text_right( struct widget_instance *label, rectangle2d const *label_bounds) @@ -6170,16 +6118,12 @@ static short ui_mouse_label_text_right( return text_bounds.x1 == SHORT_MIN ? NONE : text_bounds.x1; } -/** - * @brief Settles the areas of the frame's legend buttons: each reaches its - * label's drawn text (the text box can be far wider than the text, and would - * cover the next legend), stops before the next legend's icon on its row, - * and, for a touchscreen only, grows up to 8 units upward (not past an item, - * value or list slot above it) and in the screen's bottom strip runs down to - * the screen's edge (not past an item, value or list slot below it). - * Recomputed from the parts kept at noting, so running it - * again changes nothing. - */ +/* settles the areas of the frame's legend buttons: each reaches its label's +drawn text (the text box can be far wider), stops before the next legend's +icon on its row, and for a touchscreen only grows up to 8 units upward (not +past an item, value or list slot above it) and in the screen's bottom strip +runs down to the screen's edge (not past an item, value or list slot below +it); recomputed from the parts kept at noting, so running again changes nothing */ static void ui_mouse_fit_button_targets( void) { @@ -6370,19 +6314,11 @@ static void ui_mouse_step_list_to_slot( return; } -/** - * @brief The widget the wheel steps: the innermost on the focus's way that - * the d-pad steps through. - * - * A touch drag skips a list showing one value at a time (a setting): it - * moves between the rows, around the setting, so a drag never changes a - * value; that takes a tap on one of its halves. The desktop wheel keeps the - * game's own d-pad reach and steps a setting's value. - * - * @param root the menu - * @param skip_settings TRUE for a touch drag - * @return the widget, or NULL when none takes the d-pad - */ +/* the widget the wheel steps: the innermost on the focus's way that the +d-pad steps through; a touch drag skips a list showing one value at a time +(a setting), moving between the rows, so a drag never changes a value; that +takes a tap on one of its halves; the desktop wheel keeps the game's own +d-pad reach and steps a setting's value */ static struct widget_instance *ui_mouse_wheel_widget( struct widget_instance *root, boolean skip_settings) @@ -6408,16 +6344,10 @@ static struct widget_instance *ui_mouse_wheel_widget( return result; } -/** - * @brief Whether the focus passes over a child of a list, which gives a - * press no place to land. The PC version's lists skip their labels and - * hidden rows (widget_instance_port_is_label) at both ends and wrap past - * them, so counting one would let a drag step past the last usable row. - * - * @param list the list the child is in - * @param child the child - * @return TRUE if the PC list's focus skips the child - */ +/* whether the focus passes over a child of a list, which gives a press no +place to land; the PC version's lists skip their labels and hidden rows +(widget_instance_port_is_label) at both ends and wrap past them, so +counting one would let a drag step past the last usable row */ static boolean ui_mouse_wheel_skips_child( struct widget_instance *list, struct widget_instance *child) @@ -6426,25 +6356,13 @@ static boolean ui_mouse_wheel_skips_child( widget_instance_port_is_label(child); } -/** - * @brief How many presses of a d-pad button can step the focus before it - * would wrap around to the other end. - * - * The game's lists wrap (widget_event_function_list_widget_goto_next_item - * and _previous_item, and widget_instance_tab_to_next_valid_widget and - * _previous_), so a long touch drag would lap a short list and stop - * anywhere: the drag stops at the ends. The press goes down from the - * screen to the first widget on the focus's way that tabs in its direction - * (widget_instance_process_one_event_recursive), and the ends are that - * widget's. - * - * @param root the menu - * @param button the d-pad button that would be pressed - * @param forward TRUE to count toward the end after the focus, FALSE toward - * the start - * @return the presses that step before the end, 0 at the end or for a - * setting (changed by clicks only), NONE when no widget tabs that way - */ +/* how many presses of a d-pad button can step the focus before it would +wrap around to the other end; the game's lists wrap, so a long touch drag +would lap a short list: the drag stops at the ends; the press goes down from +the screen to the first widget on the focus's way that tabs in its direction +(widget_instance_process_one_event_recursive), and the ends are that +widget's; 0 at the end or for a setting (changed by clicks only), NONE when +no widget tabs that way */ static long ui_mouse_wheel_room( struct widget_instance *root, short button, @@ -6562,19 +6480,12 @@ static boolean ui_mouse_selection_row( (!strncmp(widget->name, "list_item_", 10) || !strncmp(widget->name, "server_item_", 12)); } -/** - * @brief Logs a tap the menus resolved: where it was and the target the - * click acts on. A click waits for the presses the mouse queued (a list - * slot's hover steps the list first), so it is resolved against the - * targets of a later frame than the one the tap arrived in; the frames - * between are logged so the target can be read against the screen that - * showed then. A value's line also gives the point where it splits into - * previous and next. - * @param x horizontal position of the tap, in menu coordinates - * @param y vertical position of the tap, in menu coordinates - * @param target what the click acts on, or NULL for none - * @param frames frames between the tap's arrival and its resolution - */ +/* logs a tap the menus resolved: where it was and the target the click acts +on; a click waits for the presses the mouse queued, so it is resolved +against the targets of a later frame than the one the tap arrived in; the +frames between are logged so the target can be read against the screen that +showed then; a value's line also gives the point where it splits into +previous and next */ static void ui_debug_log_click( short x, short y, @@ -6612,14 +6523,7 @@ static void ui_debug_log_click( return; } -/** - * @brief Logs a tap while the virtual keyboard is up: the key or legend - * that took it. - * @param x horizontal position of the tap, in menu coordinates - * @param y vertical position of the tap, in menu coordinates - * @param hit what virtual_keyboard_click matched (an index into - * virtual_keyboard_target_rectangles), or NONE - */ +/* logs a tap while the virtual keyboard is up: the key or legend that took it */ static void ui_debug_log_keyboard_tap( short x, short y, @@ -6642,23 +6546,18 @@ static void ui_debug_log_keyboard_tap( return; } -/** - * @brief Turns the pointer's motion, clicks and wheel since the last frame - * into the first player's controller events. While the virtual keyboard is - * up it gets the clicks and the menu behind gets nothing. A touch drag - * stops at the list's ends and skips settings (ui_mouse_wheel_widget, - * ui_mouse_wheel_room); the desktop wheel wraps as the d-pad does. It - * records whether the pointer read is the touchscreen - * (ui_mouse_pointer_is_touch), because the legends' taller tap areas are - * for a finger only (ui_mouse_fit_button_targets). With debug.touch_targets - * on it also notes the finger-down and tap points for the debug view, and - * logs each tap: a menu click where it is resolved, a keyboard click where - * it is taken. A click on the band beside a list's slots steps the list - * by one; hovering it does nothing. A PC list's row (ui_mouse_selection_row) - * is selected by its first click and used by a click on the selected row, - * a finger's as well as the mouse's. It forgets the frame's targets at the - * end, so that the next frame's render notes and settles them anew. - */ +/* turns the pointer's motion, clicks and wheel since the last frame into the +first player's controller events; while the virtual keyboard is up it gets +the clicks and the menu behind gets nothing; a touch drag stops at the list's +ends and skips settings; the desktop wheel wraps as the d-pad does; it +records whether the pointer read is the touchscreen because the legends' +taller tap areas are for a finger only; with debug.touch_targets on it also +notes the finger-down and tap points for the debug view, and logs each tap: a +menu click where it is resolved, a keyboard click where it is taken; a click +on the band beside a list's slots steps the list by one; a PC list's row is +selected by its first click and used by a click on the selected row; it +forgets the frame's targets at the end, so that the next frame's render notes +and settles them anew */ static void ui_widgets_process_mouse( void) { @@ -6727,9 +6626,9 @@ static void ui_widgets_process_mouse( if (widget) ui_mouse_list_directions(widget, &back, &forward); - /* a PC screen's button bar steps sideways only, and the d-pad's up - and down pass over it to the list that holds it, so a touch drag - leaves the bar the same way */ + /* a sideways PC widget (typically a screen's button bar) steps + sideways only, and the d-pad's up and down pass over it to a widget + above it on the focus's way, so a touch drag leaves it the same way */ if (touch && widget && back == _widget_event_dpad_left && pc_menu_tag(widget->definition_tag_index) && ui_mouse_wheel_room(ui_mouse_menu(), _widget_event_dpad_up, FALSE) != NONE) { @@ -7128,14 +7027,10 @@ void render_ui_widgets_postgame( return; } -/** - * @brief Renders the active widgets for one local player's viewport (or the - * whole screen), noting and settling the first player's tap targets in the - * frame's first render that can (ui_mouse_targets_settled), and draws the - * debug view of the targets and the virtual keyboard when they are up. - * @param local_player_index the viewport's player, or NONE for the whole screen - * @param window_bounds the viewport's bounds on the screen - */ +/* renders the active widgets for one local player's viewport (or the whole +screen), noting and settling the first player's tap targets in the frame's +first render that can, and draws the debug view of the targets and the +virtual keyboard when they are up */ void render_ui_widgets( short local_player_index, rectangle2d const *window_bounds) @@ -7238,6 +7133,9 @@ void render_ui_widgets( } if (first_players_render && !ui_mouse_targets_settled) { + /* fit the legends while the rows are still targets (merging + removes them); widen after merging (the merged values are + row-tall) */ ui_mouse_fit_button_targets(); ui_mouse_merge_setting_rows(); ui_mouse_widen_values(); diff --git a/source/interface/virtual_keyboard.h b/source/interface/virtual_keyboard.h index c551f29c..0241b81f 100644 --- a/source/interface/virtual_keyboard.h +++ b/source/interface/virtual_keyboard.h @@ -39,36 +39,22 @@ void virtual_keyboard_process( void virtual_keyboard_render( void); -/** - * @brief Applies a click or tap, in the menus' 640x480 coordinates that the - * keyboard draws in, to the keyboard. - * - * A key takes the focus and is pressed as A presses the focused key. BACK - * cancels as B does. ENTER goes to Done and presses it as Start does, not as - * A does: a touch has no focused key to confirm with. Keys that span several - * cells take the focus at their first. - * - * @param x horizontal position of the click - * @param y vertical position of the click - * @param hit receives which rectangle matched, as its index in - * virtual_keyboard_target_rectangles (the keys, then BACK, then ENTER), or - * NONE; may be NULL - * @return TRUE if the click was on a key or on the BACK or ENTER legend, - * which then acted; FALSE otherwise - */ +/* applies a click or tap to the keyboard; a key takes the focus and is +pressed as A presses the focused key; BACK cancels as B does; ENTER goes to +Done and presses it as Start does, not as A does (a touch has no focused key +to confirm with); keys that span several cells take the focus at their first; +hit (may be NULL) receives which rectangle matched as its index in +virtual_keyboard_target_rectangles (the keys, then BACK, then ENTER), or +NONE; the debug log relies on this order; returns TRUE if the click was on a +key or the BACK or ENTER legend, which then acted */ boolean virtual_keyboard_click( short x, short y, long *hit); -/** - * @brief Lists the rectangles that virtual_keyboard_click hit-tests, for - * the debug view of the touch targets (debug.touch_targets). - * @param rectangles receives the keys' rectangles, then the BACK and ENTER - * legends' - * @param maximum room in rectangles - * @return how many were written - */ +/* lists the rectangles that virtual_keyboard_click hit-tests, for the debug +view of the touch targets (debug.touch_targets); rectangles are the keys', +then BACK and ENTER legends', in that order */ long virtual_keyboard_target_rectangles( rectangle2d *rectangles, long maximum); From 1b585ba4996c17742053d69e90139123f025d678 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Sun, 4 Oct 2026 08:39:14 +0200 Subject: [PATCH 018/187] Keep the mutation script out of the PR --- tools/mutate_touch_menu.py | 350 ------------------------------------- 1 file changed, 350 deletions(-) delete mode 100644 tools/mutate_touch_menu.py diff --git a/tools/mutate_touch_menu.py b/tools/mutate_touch_menu.py deleted file mode 100644 index a446ebc4..00000000 --- a/tools/mutate_touch_menu.py +++ /dev/null @@ -1,350 +0,0 @@ -"""Mutation test of the menus' touch gestures (port/linux/src/touch_menu.c). - -Each mutant is a copy of touch_menu.c with ONE textual change, built with -touch_menu_test.c exactly as tools/test_touch_menu.py builds it. A mutant is -KILLED when the build or the test run fails, SURVIVED when the tests pass. -The tracked source is never touched. Exits non-zero if a mutant survives. - - python3 tools/mutate_touch_menu.py # the mutants - python3 tools/mutate_touch_menu.py --equivalent # also prove the equivalents survive -""" -import shutil -import subprocess -import sys -import tempfile -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -SOURCE = ROOT / "port" / "linux" / "src" / "touch_menu.c" -HEADER = ROOT / "port" / "linux" / "src" / "touch_menu.h" -TEST = ROOT / "port" / "linux" / "tests" / "touch_menu_test.c" - -GUARD = "if (!menu->finger_down || finger != menu->finger)\n\t\treturn;\n" -SLOP_X = "(dx > 0.0f ? menu->settings.slop : -menu->settings.slop)" -SLOP_Y = "(dy > 0.0f ? menu->settings.slop : -menu->settings.slop)" -CLICK_X = "menu->output.x = menu->output.click_x = menu->down_x;" -CLICK_Y = "menu->output.y = menu->output.click_y = menu->down_y;" - -# (name, text found exactly once in touch_menu.c, replacement) -MUTANTS = [ - # init / reset - ("init: settings not stored", "menu->settings = *settings;", "(void)settings;"), - ("init: no memset", "memset(menu, 0, sizeof(*menu));", ""), - ("reset: settings lost", "struct touch_menu_settings settings = menu->settings;", - "struct touch_menu_settings settings = {0};"), - # down - ("down: second finger accepted", "if (menu->finger_down)\n\t\treturn;\n\t/* no state", - "if (0)\n\t\treturn;\n\t/* no state"), - ("down: finger_down not set", "menu->finger_down = 1;", ""), - ("down: finger id not stored", "menu->finger = finger;", "(void)finger;"), - ("down: down_x not stored", "menu->down_x = menu->last_x = x;", "menu->last_x = x;"), - ("down: down_y not stored", "menu->down_y = menu->last_y = y;", "menu->last_y = y;"), - ("down: remainder kept", "\tmenu->remainder = 0.0f;\n\t/* a finger", "\t/* a finger"), - ("down: pending steps kept", "menu->pending_steps = 0;\n}\n\nvoid touch_menu_move", - "}\n\nvoid touch_menu_move"), - ("down: downs++ -> = 1", "menu->output.downs++;", "menu->output.downs = 1;"), - ("down: downs not counted", "menu->output.downs++;", ""), - ("down: downs++ -> --", "menu->output.downs++;", "menu->output.downs--;"), - ("down: down_x not reported", "menu->output.down_x = x;", ""), - ("down: down_y not reported", "menu->output.down_y = y;", ""), - ("down: reported x is y", "menu->output.down_x = x;", "menu->output.down_x = y;"), - ("down: reported y is x", "menu->output.down_y = y;", "menu->output.down_y = x;"), - ("cancel: downs kept", "\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}\n\nvoid touch_menu_read", - "\tmenu->pending_steps = 0;\n}\n\nvoid touch_menu_read"), - ("read: downs kept", "\tmenu->output.clicks = 0;\n\tmenu->output.downs = 0;", "\tmenu->output.clicks = 0;"), - # move guards - ("move: guard || -> &&", GUARD + "\tif (!menu->scrolling)", - GUARD.replace("||", "&&") + "\tif (!menu->scrolling)"), - ("move: finger id ignored", GUARD + "\tif (!menu->scrolling)", - "if (!menu->finger_down)\n\t\treturn;\n\t(void)finger;\n\tif (!menu->scrolling)"), - ("move: finger_down ignored", GUARD + "\tif (!menu->scrolling)", - "if (finger != menu->finger)\n\t\treturn;\n\tif (!menu->scrolling)"), - ("move: != -> ==", GUARD + "\tif (!menu->scrolling)", - GUARD.replace("!=", "==") + "\tif (!menu->scrolling)"), - ("move: slop test <= -> <", "dy * dy <= menu->settings.slop", "dy * dy < menu->settings.slop"), - ("move: slop test <= -> >", "dy * dy <= menu->settings.slop", "dy * dy > menu->settings.slop"), - ("move: slop not squared", "<= menu->settings.slop * menu->settings.slop", "<= menu->settings.slop"), - ("move: distance dx*dx - dy*dy", "dx * dx + dy * dy", "dx * dx - dy * dy"), - ("move: distance ignores dy", "dx * dx + dy * dy", "dx * dx"), - ("move: distance ignores dx", "dx * dx + dy * dy", "dy * dy"), - ("move: dx sign", "float dx = x - menu->down_x", "float dx = menu->down_x - x"), - ("move: dy sign", "dy = y - menu->down_y", "dy = menu->down_y - y"), - ("move: scrolling not set", "menu->scrolling = 1;\n\t\tmenu->axis", "menu->axis"), - # axis choice - ("axis: > -> >= (tie horizontal)", "fabsf(dx) > fabsf(dy)", "fabsf(dx) >= fabsf(dy)"), - ("axis: > -> <", "fabsf(dx) > fabsf(dy)", "fabsf(dx) < fabsf(dy)"), - ("axis: always vertical", "menu->axis = fabsf(dx) > fabsf(dy);", "menu->axis = 0;"), - ("axis: always horizontal", "menu->axis = fabsf(dx) > fabsf(dy);", "menu->axis = 1;"), - ("axis: no fabsf on dx", "fabsf(dx) > fabsf(dy)", "dx > fabsf(dy)"), - ("axis: no fabsf on dy", "fabsf(dx) > fabsf(dy)", "fabsf(dx) > dy"), - ("axis: edge branch inverted", "if (menu->axis)\n\t\t\tmenu->last_x", "if (!menu->axis)\n\t\t\tmenu->last_x"), - # slop-edge reset of the start point - ("edge: x not reset", "\t\t\tmenu->last_x = menu->down_x + " + SLOP_X + ";\n", - "\t\t\tmenu->last_x = menu->down_x;\n"), - ("edge: y not reset", "\t\t\tmenu->last_y = menu->down_y + " + SLOP_Y + ";\n", - "\t\t\tmenu->last_y = menu->down_y;\n"), - ("edge: x sign of slop swapped", SLOP_X, "(dx > 0.0f ? -menu->settings.slop : menu->settings.slop)"), - ("edge: y sign of slop swapped", SLOP_Y, "(dy > 0.0f ? -menu->settings.slop : menu->settings.slop)"), - ("edge: x + -> -", "menu->down_x + (dx", "menu->down_x - (dx"), - ("edge: y + -> -", "menu->down_y + (dy", "menu->down_y - (dy"), - ("edge: x test dx > -> <", "(dx > 0.0f ?", "(dx < 0.0f ?"), - ("edge: y test dy > -> <", "(dy > 0.0f ?", "(dy < 0.0f ?"), - ("edge: x from down_y", "menu->last_x = menu->down_x + (", "menu->last_x = menu->down_y + ("), - ("edge: x sign from dy", "menu->last_x = menu->down_x + (dx", "menu->last_x = menu->down_x + (dy"), - ("edge: y sign from dx", "menu->last_y = menu->down_y + (dy", "menu->last_y = menu->down_y + (dx"), - ("edge: half a slop", "(dx > 0.0f ? menu->settings.slop :", "(dx > 0.0f ? menu->settings.slop / 2.0f :"), - # along, remainder, steps - ("along: axis swapped", "along = menu->axis ? x - menu->last_x : y - menu->last_y;", - "along = menu->axis ? y - menu->last_y : x - menu->last_x;"), - ("along: x sign", "menu->axis ? x - menu->last_x", "menu->axis ? menu->last_x - x"), - ("along: y sign", ": y - menu->last_y;", ": menu->last_y - y;"), - ("along: x against down", "menu->axis ? x - menu->last_x", "menu->axis ? x - menu->down_x"), - ("along: y against down", ": y - menu->last_y;", ": y - menu->down_y;"), - ("last_x not updated", "menu->last_x = x;\n\tmenu->last_y = y;", "menu->last_y = y;"), - ("last_y not updated", "menu->last_x = x;\n\tmenu->last_y = y;", "menu->last_x = x;"), - ("remainder = instead of +=", "menu->remainder += along;", "menu->remainder = along;"), - ("remainder -= along", "menu->remainder += along;", "menu->remainder -= along;"), - ("remainder not added", "menu->remainder += along;", "(void)along;"), - ("back loop: >= -> >", "menu->remainder >= menu->settings.step", "menu->remainder > menu->settings.step"), - ("back loop: >= -> <=", "menu->remainder >= menu->settings.step", "menu->remainder <= menu->settings.step"), - ("back loop: step doubled", "menu->remainder >= menu->settings.step", "menu->remainder >= menu->settings.step * 2.0f"), - ("back loop: ++ -> --", "menu->pending_steps++;", "menu->pending_steps--;"), - ("back loop: -= -> +=", "menu->remainder -= menu->settings.step;", "menu->remainder += menu->settings.step;"), - ("back loop: subtraction dropped", - "\t\tmenu->pending_steps++;\n\t\tmenu->remainder -= menu->settings.step;\n\t}\n\twhile", - "\t\tmenu->pending_steps++;\n\t\tmenu->remainder = 0.0f;\n\t}\n\twhile"), - ("back loop: while -> if", "while (menu->remainder >= menu->settings.step)", "if (menu->remainder >= menu->settings.step)"), - ("forward loop: <= -> <", "menu->remainder <= -menu->settings.step", "menu->remainder < -menu->settings.step"), - ("forward loop: <= -> >=", "menu->remainder <= -menu->settings.step", "menu->remainder >= -menu->settings.step"), - ("forward loop: no minus", "menu->remainder <= -menu->settings.step", "menu->remainder <= menu->settings.step"), - ("forward loop: -- -> ++", "menu->pending_steps--;", "menu->pending_steps++;"), - ("forward loop: += -> -=", "menu->remainder += menu->settings.step;\n\t}\n}", "menu->remainder -= menu->settings.step;\n\t}\n}"), - ("forward loop: subtraction dropped", - "\t\tmenu->pending_steps--;\n\t\tmenu->remainder += menu->settings.step;", - "\t\tmenu->pending_steps--;\n\t\tmenu->remainder = 0.0f;"), - ("forward loop: while -> if", "while (menu->remainder <= -menu->settings.step)", "if (menu->remainder <= -menu->settings.step)"), - # up - ("up: guard || -> &&", GUARD + "\ttouch_menu_move", GUARD.replace("||", "&&") + "\ttouch_menu_move"), - ("up: finger id ignored", GUARD + "\ttouch_menu_move", "if (!menu->finger_down)\n\t\treturn;\n\ttouch_menu_move"), - ("up: finger_down ignored", GUARD + "\ttouch_menu_move", "if (finger != menu->finger)\n\t\treturn;\n\ttouch_menu_move"), - ("up: != -> ==", GUARD + "\ttouch_menu_move", GUARD.replace("!=", "==") + "\ttouch_menu_move"), - ("up: final move dropped", "\ttouch_menu_move(menu, finger, x, y);\n\tif (!menu->scrolling &&", "\t(void)x;\n\t(void)y;\n\tif (!menu->scrolling &&"), - ("up: tap test inverted", "\tif (!menu->scrolling && !menu->void_tap)\n\t{\n\t\tmenu->output.moved", "\tif (menu->scrolling)\n\t{\n\t\tmenu->output.moved"), - ("up: tap always", "\tif (!menu->scrolling && !menu->void_tap)\n\t{\n\t\tmenu->output.moved", "\tif (1)\n\t{\n\t\tmenu->output.moved"), - ("up: moved not set", "menu->output.moved = 1;", ""), - ("up: moved = 0", "menu->output.moved = 1;", "menu->output.moved = 0;"), - ("up: x from the lift", CLICK_X, CLICK_X.replace("menu->down_x", "x")), - ("up: y from the lift", CLICK_Y, CLICK_Y.replace("menu->down_y", "y")), - ("up: x from down_y", CLICK_X, CLICK_X.replace("down_x", "down_y")), - ("up: y from down_x", CLICK_Y, CLICK_Y.replace("down_y", "down_x")), - ("up: pointer x not set", CLICK_X, "menu->output.click_x = menu->down_x;"), - ("up: click x not set", CLICK_X, "menu->output.x = menu->down_x;"), - ("up: pointer y not set", CLICK_Y, "menu->output.click_y = menu->down_y;"), - ("up: click y not set", CLICK_Y, "menu->output.y = menu->down_y;"), - ("up: clicks++ -> = 1", "menu->output.clicks++;", "menu->output.clicks = 1;"), - ("up: clicks not counted", "menu->output.clicks++;", ""), - ("up: clicks++ -> --", "menu->output.clicks++;", "menu->output.clicks--;"), - ("up: finger stays down", "\tmenu->finger_down = 0;\n\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel", - "\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel"), - # cancel - ("cancel: finger stays down", "void touch_menu_cancel(struct touch_menu *menu)\n{\n\tmenu->finger_down = 0;\n", - "void touch_menu_cancel(struct touch_menu *menu)\n{\n"), - ("cancel: pending steps kept", "\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", - "\tmenu->remainder = 0.0f;\n\tmenu->output.downs = 0;\n}"), - ("cancel: unread tap dropped too", "menu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}\n\nvoid touch_menu_read", - "menu->pending_steps = 0;\n\tmenu->output.downs = 0;\n\tmenu->output.clicks = 0;\n\tmenu->output.moved = 0;\n}\n\nvoid touch_menu_read"), - # gesture zones - ("zone: left < -> <=", "return x < s->edge_left ||", "return x <= s->edge_left ||"), - ("zone: left check removed", "return x < s->edge_left ||", "return 0 ||"), - ("zone: left uses the right inset", "return x < s->edge_left ||", "return x < s->edge_right ||"), - ("zone: right >= -> >", "x >= s->width - s->edge_right", "x > s->width - s->edge_right"), - ("zone: right check removed", " || (s->edge_right > 0.0f && s->width > 0.0f && x >= s->width - s->edge_right)", ""), - ("zone: right without the width guard", "s->width > 0.0f && x >= s->width - s->edge_right)", - "x >= s->width - s->edge_right)"), - ("zone: right without the inset guard", "(s->edge_right > 0.0f && s->width", "(s->width"), - ("zone: right uses the left inset", "x >= s->width - s->edge_right", "x >= s->width - s->edge_left"), - ("zone: top < -> <=", "return y < s->edge_top ||", "return y <= s->edge_top ||"), - ("zone: top check removed", "return y < s->edge_top ||", "return 0 ||"), - ("zone: top uses the bottom inset", "return y < s->edge_top ||", "return y < s->edge_bottom ||"), - ("zone: bottom >= -> >", "y >= s->height - s->edge_bottom", "y > s->height - s->edge_bottom"), - ("zone: bottom check removed", " || (s->edge_bottom > 0.0f && s->height > 0.0f && y >= s->height - s->edge_bottom)", ""), - ("zone: bottom without the height guard", "s->height > 0.0f && y >= s->height - s->edge_bottom)", - "y >= s->height - s->edge_bottom)"), - ("zone: bottom without the inset guard", "(s->edge_bottom > 0.0f && s->height", "(s->height"), - ("zone: bottom uses the top inset", "y >= s->height - s->edge_bottom", "y >= s->height - s->edge_top"), - ("down: side zone finger accepted", "\tif (in_side_zone(menu, x))\n\t\treturn;\n", - "\tif (0 && in_side_zone(menu, x))\n\t\treturn;\n"), - ("down: side zone finger blocks the next", - "\tif (in_side_zone(menu, x))\n\t\treturn;\n", - "\tif (in_side_zone(menu, x))\n\t{\n\t\tmenu->finger_down = 1;\n\t\tmenu->finger = finger;\n\t\treturn;\n\t}\n"), - ("down: side zone finger counts as a down", - "\tif (in_side_zone(menu, x))\n\t\treturn;\n", - "\tif (in_side_zone(menu, x))\n\t{\n\t\tmenu->output.downs++;\n\t\treturn;\n\t}\n"), - ("down: side zone finger drops the pending steps", - "\tif (in_side_zone(menu, x))\n\t\treturn;\n", - "\tif (in_side_zone(menu, x))\n\t{\n\t\tmenu->pending_steps = 0;\n\t\treturn;\n\t}\n"), - ("down: no_scroll never set", "menu->no_scroll = in_top_or_bottom_zone(menu, y);", "menu->no_scroll = 0 && in_top_or_bottom_zone(menu, y);"), - ("down: no_scroll always set", "menu->no_scroll = in_top_or_bottom_zone(menu, y);", "menu->no_scroll = 1 || in_top_or_bottom_zone(menu, y);"), - ("down: no_scroll from x", "menu->no_scroll = in_top_or_bottom_zone(menu, y);", "menu->no_scroll = in_top_or_bottom_zone(menu, x);"), - ("move: no-scroll finger scrolls", "\t\t\tmenu->void_tap = 1;\n\t\t\treturn;", "\t\t\tmenu->void_tap = 1;"), - ("move: zone judged again at the move", "\t\tif (menu->no_scroll)\n", - "\t\tif (in_top_or_bottom_zone(menu, menu->down_y))\n"), - ("move: no_scroll test removed", "\t\tif (menu->no_scroll)\n", "\t\tif (0)\n"), - ("move: no-scroll finger keeps its tap", "\t\t\tmenu->void_tap = 1;\n", ""), - ("up: no-scroll finger clicks", "if (!menu->scrolling && !menu->void_tap)", "if (!menu->scrolling)"), - ("up: void test inverted", "if (!menu->scrolling && !menu->void_tap)", "if (!menu->scrolling && menu->void_tap)"), - # read - ("read: cap > -> <", "if (steps > limit)", "if (steps < limit)"), - ("read: upper cap dropped", "\tif (steps > limit)\n\t\tsteps = limit;\n", ""), - ("read: upper cap wrong value", "\t\tsteps = limit;", "\t\tsteps = limit + 1;"), - ("read: upper cap sign", "\t\tsteps = limit;", "\t\tsteps = -limit;"), - ("read: lower cap < -> >", "if (steps < -limit)", "if (steps > -limit)"), - ("read: lower cap dropped", "\tif (steps < -limit)\n\t\tsteps = -limit;\n", ""), - ("read: lower cap wrong value", "\t\tsteps = -limit;", "\t\tsteps = -limit - 1;"), - ("read: lower cap sign", "\t\tsteps = -limit;", "\t\tsteps = limit;"), - ("read: limit is 1", "int limit = menu->settings.steps_per_read;", "int limit = 1;"), - ("read: limit + 1", "int limit = menu->settings.steps_per_read;", "int limit = menu->settings.steps_per_read + 1;"), - ("read: limit - 1", "int limit = menu->settings.steps_per_read;", "int limit = menu->settings.steps_per_read - 1;"), - ("read: steps from nothing", "int steps = menu->pending_steps;", "int steps = 0;"), - ("read: output not copied", "\t*output = menu->output;\n", ""), - ("read: wheel steps not set", "output->wheel_steps = steps;", ""), - ("read: wheel steps negated", "output->wheel_steps = steps;", "output->wheel_steps = -steps;"), - ("read: pending not consumed", "menu->pending_steps -= steps;", ""), - ("read: pending -= -> +=", "menu->pending_steps -= steps;", "menu->pending_steps += steps;"), - ("read: pending cleared", "menu->pending_steps -= steps;", "menu->pending_steps = 0;"), - ("read: moved kept", "menu->output.moved = 0;\n\tmenu->output.clicks = 0;", "menu->output.clicks = 0;"), - ("read: clicks kept", "menu->output.moved = 0;\n\tmenu->output.clicks = 0;", "menu->output.moved = 0;"), -] - -# Mutants that cannot change behaviour, and why. Run only with --equivalent, -# to prove that each one really survives. -EQUIVALENT = [ - ("down: last_x/last_y not initialised", - "menu->down_x = menu->last_x = x;\n\tmenu->down_y = menu->last_y = y;", - "menu->down_x = x;\n\tmenu->down_y = y;", - "the first move past the slop rewrites last on the scroll axis before it is read, " - "and the other axis's last is never read; a move inside the slop reads neither"), - ("down: scrolling not cleared", - "menu->scrolling = 0;\n\tmenu->no_scroll", - "menu->no_scroll", - "scrolling is already 0 whenever finger_down is 0: init, up and cancel clear it, and down " - "runs only when finger_down is 0"), - ("up: scrolling not cleared", - "\tmenu->finger_down = 0;\n\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel", - "\tmenu->finger_down = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel", - "scrolling is read only while finger_down is set, and down clears it before the next finger"), - ("up: void_tap not cleared", - "\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n}\n\nvoid touch_menu_cancel", - "\tmenu->scrolling = 0;\n}\n\nvoid touch_menu_cancel", - "void_tap is read only while finger_down is set (up), and down clears it before the next finger"), - ("cancel: scrolling not cleared", - "menu->finger_down = 0;\n\tmenu->scrolling = 0;\n\tmenu->void_tap = 0;\n\tmenu->remainder = 0.0f;", - "menu->finger_down = 0;\n\tmenu->void_tap = 0;\n\tmenu->remainder = 0.0f;", - "same as up: scrolling is read only while finger_down is set, and down clears it"), - ("cancel: void_tap not cleared", - "menu->scrolling = 0;\n\tmenu->void_tap = 0;\n\tmenu->remainder = 0.0f;", - "menu->scrolling = 0;\n\tmenu->remainder = 0.0f;", - "same as up: void_tap is read only while finger_down is set, and down clears it"), - ("down: void_tap not cleared", - "menu->void_tap = 0;\n\tmenu->remainder = 0.0f;\n\t/* a finger", - "menu->remainder = 0.0f;\n\t/* a finger", - "up and cancel already clear void_tap, and it is set only while a finger is down"), - ("cancel: remainder not cleared", - "menu->void_tap = 0;\n\tmenu->remainder = 0.0f;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", - "menu->void_tap = 0;\n\tmenu->pending_steps = 0;\n\tmenu->output.downs = 0;\n}", - "remainder is read only by move, which needs finger_down, and down zeroes it"), - ("edge: dx >= 0 for the sign of the x edge", "(dx > 0.0f ?", "(dx >= 0.0f ?", - "the x edge is computed only when |dx| > |dy| >= 0, so dx is never 0 there"), - ("edge: dy >= 0 for the sign of the y edge", "(dy > 0.0f ?", "(dy >= 0.0f ?", - "on the vertical axis |dy| >= |dx| and dx*dx+dy*dy > slop*slop > 0, so dy is never 0"), - ("move: dx measured from last_x", "float dx = x - menu->down_x", "float dx = x - menu->last_x", - "dx is computed only before scrolling starts, and until then last_x still equals down_x"), - ("move: dy measured from last_y", "dy = y - menu->down_y", "dy = y - menu->last_y", - "same as dx: last_y equals down_y until scrolling starts"), - ("up: tap x taken from last_x", "menu->output.x = menu->output.click_x = menu->down_x;", - "menu->output.x = menu->output.click_x = menu->last_x;", - "a tap is a gesture that never scrolled, so last_x was never moved off down_x"), - ("up: tap y taken from last_y", "menu->output.y = menu->output.click_y = menu->down_y;", - "menu->output.y = menu->output.click_y = menu->last_y;", - "a tap is a gesture that never scrolled, so last_y was never moved off down_y"), - ("read: upper cap >= (assigns limit to limit)", "if (steps > limit)", "if (steps >= limit)", - "at steps == limit the assignment changes nothing"), - ("read: lower cap <= (assigns -limit to -limit)", "if (steps < -limit)", "if (steps <= -limit)", - "at steps == -limit the assignment changes nothing"), -] - - -def compiler(): - """Find the build machine's C compiler. - - Returns the path of clang, cc or gcc, whichever exists first; exits if none. - """ - for name in ("clang", "cc", "gcc"): - path = shutil.which(name) - if path: - return path - sys.exit("no C compiler") - - -def run_mutant(cc, work, text): - """Build touch_menu_test.c against one mutant and run it. - - cc: the compiler path. work: a temporary directory that holds the mutant - and a copy of touch_menu.h. text: the mutant's source. - Returns ("KILLED", reason) when the tests fail or hang, ("SURVIVED", "") - when they pass. A build failure exits the script: it is a broken mutation, - not a kill. - """ - source = work / "touch_menu.c" - source.write_text(text, newline="\n") - binary = work / "touch_menu_test" - build = subprocess.run( - [cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-O1", str(TEST), str(source), - "-lm", "-o", str(binary)], - capture_output=True, text=True) - if build.returncode != 0: - # a mutant that does not compile proves nothing about the tests - sys.exit(f"script error: the mutant does not build:\n{build.stderr}") - try: - result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10) - except subprocess.TimeoutExpired: - return "KILLED", "timeout" - if result.returncode == 0: - return "SURVIVED", "" - return "KILLED", "test: " + (result.stdout.splitlines() or ["?"])[0] - - -def main(): - """Run every mutant, and with --equivalent the equivalent ones too. - - Returns the exit code: 1 if a mutant survived or an equivalent one was - killed, else 0. - """ - cc = compiler() - original = SOURCE.read_text().replace("\r\n", "\n") - failed = False - with tempfile.TemporaryDirectory() as tmp: - work = Path(tmp) - # the mutant includes "touch_menu.h" from its own directory - shutil.copy(HEADER, work / "touch_menu.h") - killed = 0 - for name, old, new in MUTANTS: - if original.count(old) != 1: - sys.exit(f"mutation '{name}': {original.count(old)} matches, need exactly 1") - verdict, how = run_mutant(cc, work, original.replace(old, new)) - print(f"{verdict:8} {name} {how}".rstrip()) - killed += verdict == "KILLED" - print(f"killed {killed}/{len(MUTANTS)}") - failed = killed != len(MUTANTS) - if "--equivalent" in sys.argv: - for name, old, new, why in EQUIVALENT: - if original.count(old) != 1: - sys.exit(f"equivalent '{name}': {original.count(old)} matches, need exactly 1") - verdict, _ = run_mutant(cc, work, original.replace(old, new)) - print(f"equivalent {'ok' if verdict == 'SURVIVED' else 'KILLED'}: {name} ({why})") - failed = failed or verdict != "SURVIVED" - return 1 if failed else 0 - - -if __name__ == "__main__": - sys.exit(main()) From f2aec7bee3aff23581025b7c37dc4796fbcfc6be Mon Sep 17 00:00:00 2001 From: Fadi Derouiche Date: Sun, 4 Oct 2026 21:32:34 +0100 Subject: [PATCH 019/187] Draw the reflection at the display's pixel density The Xbox draws the mirror into a 320x240 target (RASTERIZER_TARGET_RENDER_SECONDARY_WIDTH/HEIGHT), a quarter of the picture's linear resolution at 1080p, and gives its shaders that size: that is the softness the reflection has always had. The game's size stays - its viewport, its projection, and the 320.0f/240.0f it puts in c[-84] for the mirror's projective texture coordinate are untouched, so nothing about the reflection's mapping moves - and the texture it is drawn into gets the display's own pixels, the viewport and the clear following it (target_pixel). A colour and its depth must be the same size in GL, and the game pairs the reflection's target with the screen's depth (the reflection's target has no Z of its own, so rasterizer_set_target uses the primary's), so draw into a depth of the colour's own size: the game clears the depth of every target it draws the scene into, so one of its own behaves as the shared one did. The mirror's mapping was checked over a play session - the surface's plane, the mirrored camera it gives, and the surface's own screen position all come out exact - so the reflection is where it was, in more pixels. Co-Authored-By: Claude Opus 5 (1M context) --- port/linux/src/d3d8_gl.c | 83 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 43c924d2..f3328091 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -272,6 +272,52 @@ struct framebuffer_entry static struct render_target_entry *render_targets; static struct framebuffer_entry *framebuffers; +/* A depth texture of a size, for a color target whose own depth is another +size: the game pairs the reflection's target with the screen's depth +(rasterizer_set_target), which Direct3D allowed, but GL will not attach a +depth of one size to a color of another. */ +struct depth_texture_entry +{ + struct depth_texture_entry *next; + unsigned long width, height; + GLuint texture; +}; + +static struct depth_texture_entry *depth_textures; + +static GLuint depth_texture_for_size(unsigned long width, unsigned long height) +{ + struct depth_texture_entry *entry; + + for (entry = depth_textures; entry; entry = entry->next) + { + if (entry->width == width && entry->height == height) + return entry->texture; + } + entry = calloc(1, sizeof(*entry)); + if (!entry) + return 0; + entry->width = width; + entry->height = height; + glGenTextures(1, &entry->texture); + glBindTexture(GL_TEXTURE_2D, entry->texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_DEPTH24_STENCIL8, + (GLsizei)width, + (GLsizei)height, + 0, + GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, + NULL); + xgpu_gl_state_invalidate(); + entry->next = depth_textures; + depth_textures = entry; + return entry->texture; +} + /* ---------- the device */ #ifdef HALO_ANDROID @@ -755,6 +801,16 @@ static void surface_dimensions(const D3DSurface *surface, unsigned long *width, format == D3DFMT_LIN_D24S8 || format == D3DFMT_LIN_F24S8 || format == D3DFMT_LIN_D16 || format == D3DFMT_LIN_F16; } +/* The Xbox's reflection target (rasterizer_xbox.c's RASTERIZER_TARGET_RENDER_ +SECONDARY_WIDTH/HEIGHT): the game draws the mirror into it at this size, and +gives its shaders this size, so it stays the game's; render_target_get makes +the texture it is drawn into the display's size. */ +enum +{ + mirror_target_width = 320, + mirror_target_height = 240 +}; + static struct render_target_entry *render_target_get(const D3DSurface *surface) { struct render_target_entry *entry; @@ -766,8 +822,15 @@ static struct render_target_entry *render_target_get(const D3DSurface *surface) float scale[2] = { 1.0f, 1.0f }; surface_dimensions(surface, &width, &height, &depth); - /* the screen's targets are drawn at the screen's scale */ - if (width == (unsigned long)halo_screen_width() && height == SCREEN_HEIGHT) + /* The screen's targets are drawn at the screen's scale, and the scene the + reflection draws is drawn at it too: the game's reflection target is + mirror_target_width x mirror_target_height, a quarter of the picture's + linear resolution at 1080p, which is the low quality the reflection has + had. Its size stays the game's, so the constants the game gives its + reflection shaders (rasterizer_xbox_environment.c) keep matching it; only + the texture it is drawn into is bigger. */ + if ((width == (unsigned long)halo_screen_width() && height == SCREEN_HEIGHT) || + (width == mirror_target_width && height == mirror_target_height)) { scale[0] = screen_scale[0]; scale[1] = screen_scale[1]; @@ -862,6 +925,7 @@ static BOOL bind_targets(BOOL *has_depth) { struct render_target_entry *color = render_target_get(device.render_target); struct render_target_entry *depth = render_target_get(device.depth_stencil); + GLuint depth_texture = 0; if (depth && !depth->target.depth) depth = NULL; @@ -869,10 +933,23 @@ static BOOL bind_targets(BOOL *has_depth) return FALSE; if (color) color->last_rendered = device.frame + 1; + /* A depth of another size than the color it is drawn with (the screen's, + which the game pairs the reflection's target with) cannot be attached in + GL: draw into a depth of the color's own size instead. The game clears + the depth of every target it draws the scene into (rasterizer_set_target), + so a depth of its own behaves as the shared one did. */ + if (depth) + { + depth_texture = depth->target.texture; + if (color && + (color->target.gl_width != depth->target.gl_width || + color->target.gl_height != depth->target.gl_height)) + depth_texture = depth_texture_for_size(color->target.gl_width, color->target.gl_height); + } /* viewports and clears are in the targets' units (render_target_get) */ target_scale[0] = color ? color->target.scale[0] : depth->target.scale[0]; target_scale[1] = color ? color->target.scale[1] : depth->target.scale[1]; - state_framebuffer(framebuffer_get(color ? color->target.texture : 0, depth ? depth->target.texture : 0)); + state_framebuffer(framebuffer_get(color ? color->target.texture : 0, depth_texture)); *has_depth = depth != NULL; return TRUE; } From 58a6bd2f5ec946eda587a6980260c8c2631a0f0b Mon Sep 17 00:00:00 2001 From: eltanschauung <158000645+eltanschauung@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:39:42 -0400 Subject: [PATCH 020/187] Add rebindable PNG Screenshot action, default F10 --- port/linux/src/d3d8_gl.c | 109 ++++++++++++++++++++++++++++++++++ port/linux/src/port_config.c | 2 + port/linux/src/sdl_platform.c | 20 +++++++ port/linux/src/sdl_platform.h | 4 ++ port/linux/src/xinput_sdl.c | 38 +++++++++--- 5 files changed, 165 insertions(+), 8 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 41f11475..dab2d0eb 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -25,6 +25,9 @@ Conventions carried over from the Xbox: #include "sdl_platform.h" #include "halo_ui_pointer.h" #include "port_config.h" +#include "main/console.h" + +#include #include #include @@ -3618,6 +3621,110 @@ void WINAPI D3DDevice_Clear(DWORD count, CONST D3DRECT *rectangles, DWORD flags, /* ---------- presentation */ +static void write_key_screenshot(struct render_target_entry *target) +{ + char directory[512], path[640], timestamp[32], filename[64]; + SDL_Time now; + SDL_DateTime date; + SDL_Surface *surface = NULL; + unsigned int collision; + unsigned long width = target->target.gl_width, height = target->target.gl_height; + GLint framebuffer, draw_framebuffer, pack_buffer, alignment, row_length, skip_rows, skip_pixels; + GLenum error; + unsigned long row, column; + + if (snprintf(directory, sizeof(directory), "%s/screenshots", platform_data_root()) >= (int)sizeof(directory)) + { + SDL_SetError("Screenshot directory path is too long"); + goto failed; + } + if (!SDL_CreateDirectory(directory) || !SDL_GetCurrentTime(&now) || !SDL_TimeToDateTime(now, &date, true)) + goto failed; + snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d_%02d.%02d.%02d", + date.year, date.month, date.day, date.hour, date.minute, date.second); + for (collision = 0; collision < 1000; collision++) + { + if (collision) + snprintf(filename, sizeof(filename), "%s_%u.png", timestamp, collision + 1); + else + snprintf(filename, sizeof(filename), "%s.png", timestamp); + snprintf(path, sizeof(path), "%s/%s", directory, filename); + if (!SDL_GetPathInfo(path, NULL)) + break; + } + if (collision == 1000) + { + SDL_SetError("Too many screenshots with the same timestamp"); + goto failed; + } + if (!width || !height || width > INT_MAX / 4 || height > INT_MAX) + { + SDL_SetError("Invalid screenshot dimensions"); + goto failed; + } +#ifdef HALO_ANDROID + surface = SDL_CreateSurface((int)width, (int)height, SDL_PIXELFORMAT_RGBA32); +#else + surface = SDL_CreateSurface((int)width, (int)height, SDL_PIXELFORMAT_BGRA32); +#endif + if (!surface) + goto failed; + + /* Read the render target before the display blit's vertical flip. Its + row zero is the top of the image. Preserve the caller's readback state. */ + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &framebuffer); + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw_framebuffer); + glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &pack_buffer); + glGetIntegerv(GL_PACK_ALIGNMENT, &alignment); + glGetIntegerv(GL_PACK_ROW_LENGTH, &row_length); + glGetIntegerv(GL_PACK_SKIP_ROWS, &skip_rows); + glGetIntegerv(GL_PACK_SKIP_PIXELS, &skip_pixels); + glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer_get(target->target.texture, 0)); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glPixelStorei(GL_PACK_ROW_LENGTH, surface->pitch / 4); + glPixelStorei(GL_PACK_SKIP_ROWS, 0); + glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + glReadPixels(0, 0, (GLsizei)width, (GLsizei)height, +#ifdef HALO_ANDROID + GL_RGBA, +#else + GL_BGRA, +#endif + GL_UNSIGNED_BYTE, surface->pixels); + error = glGetError(); + glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw_framebuffer); + glBindBuffer(GL_PIXEL_PACK_BUFFER, pack_buffer); + glPixelStorei(GL_PACK_ALIGNMENT, alignment); + glPixelStorei(GL_PACK_ROW_LENGTH, row_length); + glPixelStorei(GL_PACK_SKIP_ROWS, skip_rows); + glPixelStorei(GL_PACK_SKIP_PIXELS, skip_pixels); + if (error != GL_NO_ERROR) + { + SDL_SetError("OpenGL screenshot readback failed (%04x)", (unsigned)error); + goto failed; + } + /* Destination alpha is scratch data in the game, not transparency. */ + for (row = 0; row < height; row++) + { + unsigned char *pixels = (unsigned char *)surface->pixels + (size_t)row * surface->pitch; + + for (column = 0; column < width; column++) + pixels[column * 4 + 3] = 0xff; + } + if (!SDL_SavePNG(surface, path)) + goto failed; + SDL_DestroySurface(surface); + console_printf(FALSE, "Saved screenshot as %s", filename); + return; + +failed: + console_printf(FALSE, "Screenshot failed: %.180s", SDL_GetError()); + if (surface) + SDL_DestroySurface(surface); +} + static void write_screenshot(struct render_target_entry *target) { const char *directory = *config_string("debug.screenshot_directory") ? @@ -3689,6 +3796,8 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat back_buffer->target.texture); if (screenshot_every > 0 && device.frame % (unsigned long)screenshot_every == 0) write_screenshot(back_buffer); + if (platform_screenshot_take_request()) + write_key_screenshot(back_buffer); platform_video_drawable_size(&window_width, &window_height); /* letterbox to the back buffer's aspect ratio */ diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index c7554eac..67d95dbc 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -203,6 +203,8 @@ static const struct config_setting config_settings[] = "Showing the scores (the controller's Back)." }, { "controls.pause", _config_string, "\"Escape\"", "HALO_KEY_PAUSE", _environment_value, _platform_all, "The pause menu (the controller's Start)." }, + { "controls.screenshot", _config_string, "\"F10\"", "HALO_KEY_SCREENSHOT", _environment_value, _platform_all, + "Save a PNG screenshot beside maps/ (press once per capture)." }, { "game.console_log", _config_string, "\"important\"", "HALO_CONSOLE_LOG", _environment_value, _platform_all, "What the game's console shows on screen of what it logs: \"important\"\n" diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 58130ee4..6bdc8e78 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -33,6 +33,8 @@ static struct platform_input_state input_state; /* keys pressed since the last read, so a press and release between two reads still counts as a press (input injected on Android, or a slow frame) */ static unsigned char keys_pressed[SDL_SCANCODE_COUNT]; +/* The bound Screenshot action requests capture at the next presentation. */ +static BOOL screenshot_requested; /* likewise the mouse buttons pressed since the last read, so that a click quicker than a frame still counts */ static unsigned char mouse_buttons_pressed[PLATFORM_MOUSE_BUTTON_COUNT]; @@ -1174,6 +1176,24 @@ void platform_scoreboard_scroll(int open, long *notches, long *pages) pthread_mutex_unlock(&input_lock); } +void platform_screenshot_request(void) +{ + pthread_mutex_lock(&input_lock); + screenshot_requested = TRUE; + pthread_mutex_unlock(&input_lock); +} + +BOOL platform_screenshot_take_request(void) +{ + BOOL requested; + + pthread_mutex_lock(&input_lock); + requested = screenshot_requested; + screenshot_requested = FALSE; + pthread_mutex_unlock(&input_lock); + return requested; +} + void platform_pump_events(void) { /* debug.exit_after (seconds) ends the game that long after the window diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index 1c4978be..2f0d3df1 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -83,6 +83,10 @@ void platform_mouse_capture(BOOL capture); /* main thread only; a no-op elsewhere */ void platform_pump_events(void); +/* request a capture from the bound Screenshot action */ +void platform_screenshot_request(void); +/* consume the request; call on the render thread with a completed frame */ +BOOL platform_screenshot_take_request(void); /* a snapshot of the input state; consume_motion resets the mouse deltas */ void platform_input_read(struct platform_input_state *state, BOOL consume_motion); #ifndef HALO_ANDROID diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index a2e87267..480c2bf0 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -22,8 +22,9 @@ In the menus the keys drive the controller, to move about them: on-screen keyboard takes what is typed, and the mouse is free and drives a pointer (port/linux/include/halo_ui_pointer.h, source/interface/ui_widget.c). -F11 switches between fullscreen and the window, and F12 releases or -recaptures the mouse, always. +Screenshot is a normal bound action (default F10), also available in the +menus. F11 switches between fullscreen and the window, and F12 releases +or recaptures the mouse, always. Mouse aim does not go through the right stick: the game's look code asks halo_linux_mouse_look for the motion since its last call and adds it to the @@ -331,7 +332,7 @@ static const char *const binding_settings[NUMBER_OF_HALO_KEYBOARD_ACTIONS] = "controls.move_forward", "controls.move_backward", "controls.strafe_left", "controls.strafe_right", "controls.jump", "controls.crouch", "controls.fire", "controls.throw_grenade", "controls.melee", "controls.reload", "controls.zoom", "controls.switch_weapon", "controls.switch_grenade", "controls.action", - "controls.flashlight", "controls.scoreboard", "controls.pause", + "controls.flashlight", "controls.scoreboard", "controls.pause", "controls.screenshot", }; static const struct @@ -460,9 +461,8 @@ static BOOL input_held(const struct platform_input_state *input, int code) return wheel && wheel_direction == (code == INPUT_WHEEL_UP ? 1 : -1); } -/* in the game: the actions held, and the controller's Start and Back for -the pause menu and the scoreboard */ -static void keyboard_controls(const struct platform_input_state *input, XINPUT_GAMEPAD *pad) +/* Shared binding lookup for gameplay and Screenshot, including in menus. */ +static unsigned long keyboard_bound_actions(const struct platform_input_state *input) { unsigned long held = 0; int action, slot; @@ -476,6 +476,23 @@ static void keyboard_controls(const struct platform_input_state *input, XINPUT_G held |= 1UL << action; } } + return held; +} + +/* One capture per press, regardless of how long the binding is held. */ +static void keyboard_screenshot(unsigned long held) +{ + static BOOL was_down; + BOOL down = (held & (1UL << HALO_KEYBOARD_SCREENSHOT)) != 0; + + if (down && !was_down) + platform_screenshot_request(); + was_down = down; +} + +/* in the game: the actions held, and Start/Back for pause/scores */ +static void keyboard_controls(unsigned long held, XINPUT_GAMEPAD *pad) +{ if (held & (1UL << HALO_KEYBOARD_PAUSE)) pad->wButtons |= XINPUT_GAMEPAD_START; if (held & (1UL << HALO_KEYBOARD_SCOREBOARD)) @@ -846,18 +863,23 @@ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state) if (port == 0) { struct platform_input_state input; + unsigned long held; + BOOL console_active; platform_input_read(&input, TRUE); mouse_poll(&input); wheel_update(); keyboard_actions_held = 0; keys_held_over_switch(&input); - if (!console_is_active()) + console_active = console_is_active(); + held = console_active ? 0 : keyboard_bound_actions(&input); + keyboard_screenshot(held); + if (!console_active) { if (input.menus) keyboard_gamepad(&input, &state->Gamepad); else - keyboard_controls(&input, &state->Gamepad); + keyboard_controls(held, &state->Gamepad); } if (port_gamepad(gamepads, count, 0)) sdl_gamepad_state(gamepads[0], &state->Gamepad); From c6959530d5c87f488b5879819433e4d180b536db Mon Sep 17 00:00:00 2001 From: eltanschauung <158000645+eltanschauung@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:40:23 -0400 Subject: [PATCH 021/187] Expose Screenshot below Pause Menu in Controls Setup --- port/linux/game/menu_functions.c | 1 + 1 file changed, 1 insertion(+) diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 7d098faa..3663e53a 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -1741,6 +1741,7 @@ static struct { "controls.flashlight", L"FLASHLIGHT", 2 }, { "controls.scoreboard", L"SHOW SCORES", 2 }, { "controls.pause", L"PAUSE MENU", 2 }, + { "controls.screenshot", L"SCREENSHOT", 2 }, }; static struct From 15566ec76b31c3ae107508f18a11578996132706 Mon Sep 17 00:00:00 2001 From: eltanschauung <158000645+eltanschauung@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:41:08 -0400 Subject: [PATCH 022/187] Define the rebindable Screenshot action --- port/linux/include/halo_keyboard.h | 1 + 1 file changed, 1 insertion(+) diff --git a/port/linux/include/halo_keyboard.h b/port/linux/include/halo_keyboard.h index e8a66d12..825ba81b 100644 --- a/port/linux/include/halo_keyboard.h +++ b/port/linux/include/halo_keyboard.h @@ -34,6 +34,7 @@ enum halo_keyboard_action read from it) */ HALO_KEYBOARD_SCOREBOARD, HALO_KEYBOARD_PAUSE, + HALO_KEYBOARD_SCREENSHOT, NUMBER_OF_HALO_KEYBOARD_ACTIONS }; From 8d2d81179e8ca7d8d93f97ddd68c4fb53356a64b Mon Sep 17 00:00:00 2001 From: eltanschauung <158000645+eltanschauung@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:41:35 -0400 Subject: [PATCH 023/187] Document Screenshot controls and timestamped PNGs --- port/linux/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index bfc82b4b..b455719c 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -120,9 +120,14 @@ gamepads' only. | zoom | Z, middle mouse button | | show the scores (hold) | tab | | pause menu | escape | +| screenshot | F10 | Always: \` opens the developer console, F12 releases or captures the mouse, -F11 changes between fullscreen and window. +F11 changes between fullscreen and window. Screenshot (default F10, +rebindable under Controls Setup > Actions, below Pause Menu) saves a PNG of the completed +frame to `screenshots/` beside `maps/`, named `YYYY-MM-DD_HH.MM.SS.png` in +local time, and prints the filename in the console. Captures in the same +second get a numeric suffix so previous screenshots are preserved. One movement of the mouse wheel changes the weapon one time. A second movement after a short pause changes it again. @@ -247,7 +252,7 @@ the setting for one start of the game. It has priority over the file. | `input.mouse_vertical_sensitivity` | `0.0` | `HALO_MOUSE_VERTICAL_SENSITIVITY` | The multiplier for the vertical mouse aim. `0`: the same as `input.mouse_sensitivity`. | | `input.invert_mouse` | `false` | `HALO_MOUSE_INVERT=1` sets `true` | `true`: the vertical mouse aim is inverted. | | `input.mouse_aim_assist` | `false` | `HALO_MOUSE_AIM_ASSIST` | `true`: the magnetism of the controller also operates for the mouse. `false`: when the mouse moved after the right stick, the view is not slowed or dragged by a target. The autoaim of the bullets operates in both cases. | -| `controls.` | (the table in "Controls") | `HALO_KEY_` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. | +| `controls.` | (the table in "Controls") | `HALO_KEY_` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`, `screenshot`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. | | `game.console_log` | `"important"` | `HALO_CONSOLE_LOG` | What the console shows on the screen. `"important"`: bans, players that the host drops for cheating, the reasons that the game refuses a command, and the asserts that stop the game. `"all"`: all the lines. `"none"`: only the asserts that stop the game. The output of a command always shows. `debug.txt` gets all the lines. | | `game.language` | `""` | `HALO_LANGUAGE` | The language of the menus: `ja`, `de`, `fr`, `es` or `it`. Empty: English. | | `paths.data` | `""` | `HALO_DATA_ROOT` | The data root. Refer to "Start the game". | From 0db37662aa15edfb2113656b6278e065ae601e39 Mon Sep 17 00:00:00 2001 From: eltanschauung <158000645+eltanschauung@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:42:15 -0400 Subject: [PATCH 024/187] Test rebindable screenshot input and real GL-to-PNG capture --- tools/test_screenshot.py | 257 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 tools/test_screenshot.py diff --git a/tools/test_screenshot.py b/tools/test_screenshot.py new file mode 100644 index 00000000..c4b7d601 --- /dev/null +++ b/tools/test_screenshot.py @@ -0,0 +1,257 @@ +"""Check rebindable screenshot actions and real PNG readback in a hidden GL context.""" +import argparse +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def block(source, marker): + start = source.index(marker) + end = source.index('{', start) + 1 + depth = 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +PRELUDE = r''' +#include +#include +#include +#include +#include +#include +#include "gl.h" +typedef int BOOL; +#define TRUE 1 +#define FALSE 0 +#define CHECK(c) do { if (!(c)) { fprintf(stderr, "FAIL line %d: %s (%s)\n", __LINE__, #c, SDL_GetError()); exit(1); } } while (0) +#include "../include/halo_keyboard.h" +#undef INPUT_MOUSE +#include "sdl_platform.h" +#define GL_DEFINE(name) __typeof__(name) name; +GL_FUNCTIONS(GL_DEFINE) +#undef GL_DEFINE +struct render_target_entry { struct {unsigned long gl_width, gl_height; GLuint texture;} target; }; +static GLuint capture_framebuffer; +static char data_root[512], message[256]; +static BOOL fail_save, screenshot_requested; +static int input_lock, locks; +static Uint64 wheel_press_until_ms; +static int wheel_direction; +static unsigned long config_revision; +static const char *screenshot_binding = "F10", *fire_binding = "Mouse Left"; +static unsigned long config_changes(void) {return config_revision;} +static const char *config_string(const char *name) { + if (!strcmp(name, "controls.screenshot")) return screenshot_binding; + if (!strcmp(name, "controls.fire")) return fire_binding; + if (!strcmp(name, "controls.move_forward")) return "W"; + if (!strcmp(name, "controls.pause")) return "Escape"; + if (!strcmp(name, "controls.scoreboard")) return "Tab"; + return ""; +} +static void platform_log(const char *format, ...) {(void)format; CHECK(FALSE);} +typedef struct {unsigned short wButtons;} XINPUT_GAMEPAD; +#define XINPUT_GAMEPAD_START 0x10 +#define XINPUT_GAMEPAD_BACK 0x20 +enum {_binding_capture_waiting, _binding_capture_taken}; +static int binding_capture, binding_capture_result, binding_captured_input; +static Uint64 binding_taken_ms; +static void pthread_mutex_lock(int *lock) { (void)lock; CHECK(!locks); locks++; } +static void pthread_mutex_unlock(int *lock) { (void)lock; CHECK(locks == 1); locks--; } +static const char *platform_data_root(void) {return data_root;} +static GLuint framebuffer_get(GLuint color, GLuint depth) { + (void)color; (void)depth; + /* A cache miss binds both framebuffer targets in the real renderer. */ + glBindFramebuffer(GL_FRAMEBUFFER, capture_framebuffer); + return capture_framebuffer; +} +static void console_printf(unsigned char clear, const char *format, ...) { + va_list args; (void)clear; + va_start(args, format); vsnprintf(message, sizeof(message), format, args); va_end(args); +} +static bool fixed_current_time(SDL_Time *time) {*time = 0; return true;} +static bool fixed_date(SDL_Time time, SDL_DateTime *date, bool local) { + (void)time; CHECK(local); + *date = (SDL_DateTime){.year=2026, .month=8, .day=25, .hour=14, .minute=3, .second=27}; + return true; +} +static bool save_png(SDL_Surface *surface, const char *path) { + if (fail_save) return SDL_SetError("Simulated disk write failure"); + return SDL_SavePNG(surface, path); +} +#define SDL_GetCurrentTime fixed_current_time +#define SDL_TimeToDateTime fixed_date +#define SDL_SavePNG save_png +''' + +TESTS = r''' +static BOOL poll_screenshot(const struct platform_input_state *input) { + keyboard_screenshot(keyboard_bound_actions(input)); + return platform_screenshot_take_request(); +} +static void check_bindings(void) { + struct platform_input_state input = {0}; + XINPUT_GAMEPAD pad = {0}; + SDL_Event event = {0}; + input.keys[SDL_SCANCODE_F2] = 1; CHECK(!poll_screenshot(&input)); + input.keys[SDL_SCANCODE_F10] = 1; CHECK(poll_screenshot(&input)); + CHECK(!poll_screenshot(&input) && !platform_screenshot_take_request() && !locks); + input.keys[SDL_SCANCODE_F10] = 0; CHECK(!poll_screenshot(&input)); + input.keys[SDL_SCANCODE_F10] = 1; CHECK(poll_screenshot(&input)); + /* Rebinding removes the old default; the two slots use the normal parser. */ + screenshot_binding = "F2, Mouse 4"; config_revision++; + input.keys[SDL_SCANCODE_F2] = 0; CHECK(!poll_screenshot(&input)); + input.keys[SDL_SCANCODE_F2] = 1; CHECK(poll_screenshot(&input)); + input.keys[SDL_SCANCODE_F2] = 0; CHECK(!poll_screenshot(&input)); + input.mouse_buttons[SDL_BUTTON_X1] = 1; CHECK(poll_screenshot(&input)); + CHECK(!poll_screenshot(&input)); + input.mouse_buttons[SDL_BUTTON_X1] = 0; CHECK(!poll_screenshot(&input)); + /* The default is free for other actions. No hidden screenshot protection. */ + fire_binding = "F10"; config_revision++; + CHECK(keyboard_bound_actions(&input) & (1UL << HALO_KEYBOARD_FIRE)); + CHECK(!poll_screenshot(&input)); + screenshot_binding = ""; config_revision++; + input.keys[SDL_SCANCODE_F2] = 1; CHECK(!poll_screenshot(&input)); + screenshot_binding = "Wheel Down"; config_revision++; + wheel_direction = -1; wheel_press_until_ms = SDL_GetTicks() + 1000; + CHECK(poll_screenshot(&input)); CHECK(!poll_screenshot(&input)); + wheel_press_until_ms = 0; CHECK(!poll_screenshot(&input)); + screenshot_binding = "F10"; config_revision++; + input.menus = TRUE; CHECK(poll_screenshot(&input)); + input.keys[SDL_SCANCODE_ESCAPE] = input.keys[SDL_SCANCODE_TAB] = input.keys[SDL_SCANCODE_W] = 1; + unsigned long held = keyboard_bound_actions(&input); + keyboard_controls(held, &pad); + CHECK(pad.wButtons == (XINPUT_GAMEPAD_START | XINPUT_GAMEPAD_BACK)); + CHECK(keyboard_actions_held == held && (held & (1UL << HALO_KEYBOARD_MOVE_FORWARD))); + keyboard_screenshot(0); CHECK(!platform_screenshot_take_request()); + /* Normal binding capture accepts both F2 and F10, with no capture fired. */ + event.key.down = true; + for (int i = 0; i < 2; i++) { + binding_capture = _binding_capture_waiting; + event.key.scancode = i ? SDL_SCANCODE_F10 : SDL_SCANCODE_F2; + capture_key(event); + CHECK(binding_capture == _binding_capture_taken && binding_capture_result == 1); + CHECK(binding_captured_input == event.key.scancode && !platform_screenshot_take_request()); + } + binding_capture = _binding_capture_waiting; + event.key.repeat = true; capture_key(event); + CHECK(binding_capture == _binding_capture_waiting); +} +static void check_state(GLuint pack_buffer) { + GLint value; + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &value); CHECK(value == 0); + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &value); CHECK(value == 0); + glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &value); CHECK(value == (GLint)pack_buffer); + glGetIntegerv(GL_PACK_ALIGNMENT, &value); CHECK(value == 8); + glGetIntegerv(GL_PACK_ROW_LENGTH, &value); CHECK(value == 8); + glGetIntegerv(GL_PACK_SKIP_ROWS, &value); CHECK(value == 1); + glGetIntegerv(GL_PACK_SKIP_PIXELS, &value); CHECK(value == 2); + CHECK(glGetError() == GL_NO_ERROR); +} +static void check_png(const char *filename, const Uint8 *colors) { + char path[640]; Uint8 r, g, b, a; + snprintf(path, sizeof(path), "%s/screenshots/%s", data_root, filename); + SDL_Surface *image = SDL_LoadPNG(path); + CHECK(image && image->w == 3 && image->h == 2); + for (int y = 0; y < 2; y++) for (int x = 0; x < 3; x++) { + const Uint8 *expected = colors + (y * 3 + x) * 4; + CHECK(SDL_ReadSurfacePixel(image, x, y, &r, &g, &b, &a)); + CHECK(r == expected[0] && g == expected[1] && b == expected[2] && a == 255); + } + SDL_DestroySurface(image); +} +int main(int argc, char **argv) { + CHECK(argc == 2); snprintf(data_root, sizeof(data_root), "%s", argv[1]); + check_bindings(); + + CHECK(SDL_Init(SDL_INIT_VIDEO)); + CHECK(SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4)); + CHECK(SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5)); + CHECK(SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE)); + SDL_Window *window = SDL_CreateWindow("Screenshot regression", 32, 32, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); + CHECK(window); + SDL_GLContext context = SDL_GL_CreateContext(window); CHECK(context); +#define HALO_TEST_LOAD_GL(name) name = (__typeof__(name))SDL_GL_GetProcAddress(#name); CHECK(name); + GL_FUNCTIONS(HALO_TEST_LOAD_GL) +#undef HALO_TEST_LOAD_GL + struct render_target_entry target = {.target = {.gl_width = 3, .gl_height = 2}}; + const Uint8 colors[24] = {255,0,0,1, 0,255,0,2, 0,0,255,3, 255,255,255,4, 13,29,47,5, 127,63,31,6}; + glGenTextures(1, &target.target.texture); glBindTexture(GL_TEXTURE_2D, target.target.texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 3, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, colors); + glGenFramebuffers(1, &capture_framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, capture_framebuffer); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.target.texture, 0); + CHECK(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + GLuint pack_buffer; glGenBuffers(1, &pack_buffer); glBindBuffer(GL_PIXEL_PACK_BUFFER, pack_buffer); + glBufferData(GL_PIXEL_PACK_BUFFER, 32, NULL, GL_STREAM_READ); + glPixelStorei(GL_PACK_ALIGNMENT, 8); glPixelStorei(GL_PACK_ROW_LENGTH, 8); + glPixelStorei(GL_PACK_SKIP_ROWS, 1); glPixelStorei(GL_PACK_SKIP_PIXELS, 2); + CHECK(glGetError() == GL_NO_ERROR); + write_key_screenshot(&target); + CHECK(!strcmp(message, "Saved screenshot as 2026-08-25_14.03.27.png")); + check_state(pack_buffer); check_png("2026-08-25_14.03.27.png", colors); + write_key_screenshot(&target); + CHECK(!strcmp(message, "Saved screenshot as 2026-08-25_14.03.27_2.png")); + check_state(pack_buffer); check_png("2026-08-25_14.03.27_2.png", colors); + check_png("2026-08-25_14.03.27.png", colors); + fail_save = TRUE; write_key_screenshot(&target); + CHECK(!strncmp(message, "Screenshot failed:", 18)); check_state(pack_buffer); + fail_save = FALSE; + target.target.gl_width = 0; write_key_screenshot(&target); + CHECK(!strncmp(message, "Screenshot failed:", 18)); check_state(pack_buffer); + glDeleteBuffers(1, &pack_buffer); glDeleteFramebuffers(1, &capture_framebuffer); + glDeleteTextures(1, &target.target.texture); + SDL_GL_DestroyContext(context); SDL_DestroyWindow(window); SDL_Quit(); + puts("PASS: F10 default; normal rebind/clear/two slots/mouse/wheel; F2/F10 binding capture; gameplay unchanged; real GL-to-PNG pixels, orientation/opaque alpha; timestamps/collisions; GL state and failures"); + return 0; +} +''' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cc', default='clang') + args = parser.parse_args() + render = (ROOT / 'port/linux/src/d3d8_gl.c').read_text() + events = (ROOT / 'port/linux/src/sdl_platform.c').read_text() + keyboard = (ROOT / 'port/linux/src/xinput_sdl.c').read_text() + menu = (ROOT / 'port/linux/game/menu_functions.c').read_text() + config = (ROOT / 'port/linux/src/port_config.c').read_text() + assert '{ "controls.pause", L"PAUSE MENU", 2 },\n\t{ "controls.screenshot", L"SCREENSHOT", 2 },' in menu + assert r'"controls.screenshot", _config_string, "\"F10\""' in config + assert 'SDL_SCANCODE_F2' not in events and 'SDL_SCANCODE_F10' not in events + unit = (PRELUDE + block(events, 'void platform_screenshot_request(void)') + + block(events, 'BOOL platform_screenshot_take_request(void)') + + '\n' + keyboard[keyboard.index('#define MAXIMUM_BINDINGS'):keyboard.index('unsigned long halo_keyboard_actions(')] + + '\nstatic void capture_key(SDL_Event event) { do {\n' + + block(events, 'if (binding_capture == _binding_capture_waiting && event.key.down') + '\n} while (0); }\n' + + block(render, 'static void write_key_screenshot(') + TESTS) + compiler = [args.cc, '-std=gnu11', '-O2', '-fuse-ld=lld', '-I' + str(ROOT / 'port/linux/src')] + env = dict(os.environ) + if sys.platform == 'win32': + sdl = next((ROOT / 'build/windows/third_party').glob('SDL3-*/include')).parent + compiler += ['--target=i686-pc-windows-msvc', '-I' + str(sdl / 'include')] + libraries = [str(sdl / 'lib/x86/SDL3.lib')] + env['PATH'] = str(sdl / 'lib/x86') + os.pathsep + env['PATH'] + else: + compiler += subprocess.check_output(['pkg-config', '--cflags', 'sdl3'], text=True).split() + libraries = subprocess.check_output(['pkg-config', '--libs', 'sdl3'], text=True).split() + with tempfile.TemporaryDirectory(prefix='halo-screenshot-test-') as directory: + path = Path(directory) + (path / 'screenshot.c').write_text(unit) + subprocess.run([*compiler, str(path / 'screenshot.c'), *libraries, '-o', str(path / 'screenshot.exe')], check=True) + subprocess.run([str(path / 'screenshot.exe'), str(path)], env=env, check=True, timeout=30) + files = sorted((path / 'screenshots').glob('*.png')) + assert len(files) == 2 + for file in files: + assert file.read_bytes().startswith(b'\x89PNG\r\n\x1a\n') + + +if __name__ == '__main__': + main() From 102e0934365aef4aaa7d905e11c64e9663aa57c8 Mon Sep 17 00:00:00 2001 From: RockBooch Date: Mon, 5 Oct 2026 22:20:57 -0400 Subject: [PATCH 025/187] Update units.c Keep legs moving during grenade throws --- source/units/units.c | 107 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/source/units/units.c b/source/units/units.c index 5c4099b7..a530d65a 100644 --- a/source/units/units.c +++ b/source/units/units.c @@ -10551,6 +10551,112 @@ static boolean unit_animation_set_state( done: return result; } +/* port: a unit throwing a grenade on the move keeps its lower body (the +pelvis and the legs) in the movement animation; the throw keeps the spine +and everything above it. The throw is a base animation, which sets every +node, so the legs of the original game stop while the unit slides. */ +static boolean unit_node_is_at_or_below( + struct model *model, + short node_index, + short ancestor_node_index) +{ + while (node_index != NONE) + { + if (node_index == ancestor_node_index) + { + return TRUE; + } + node_index = TAG_BLOCK_GET_ELEMENT(&model->nodes, node_index, struct model_node)->parent_node_index; + } + + return FALSE; +} + +static void unit_grenade_throw_keep_legs_moving( + long unit_index, + struct real_orientation *node_orientations) +{ + struct unit_datum *unit = unit_get(unit_index); + struct unit_definition *unit_definition = unit_definition_get(unit->definition_index); + struct animation_graph *animation_graph; + struct animation_graph_unit_seat *unit_seat; + struct animation_graph_weapon_class *weapon_class; + struct animation *animation; + struct model *model; + struct real_orientation move_orientations[MAXIMUM_NODES_PER_MODEL]; + short slot; + short animation_index; + short spine; + short node_index; + + if (unit->unit.animation.state != _unit_state_throw_grenade || + unit->object.parent_object_index != NONE || + unit->unit.animation.seat_index == NONE || + magnitude3d(&unit->unit.throttle) < 0.1f) + { + return; + } + + /* the movement animation for the direction the unit is pushed */ + if (fabs(unit->unit.throttle.i) >= fabs(unit->unit.throttle.j)) + { + slot = unit->unit.throttle.i > 0.f ? + _unit_weapon_class_animation_moving_front : + _unit_weapon_class_animation_moving_back; + } + else + { + slot = unit->unit.throttle.j > 0.f ? + _unit_weapon_class_animation_moving_left : + _unit_weapon_class_animation_moving_right; + } + + animation_graph = animation_graph_definition_get(unit_definition->object.animation_graph.index); + unit_seat = TAG_BLOCK_GET_ELEMENT(&animation_graph->unit_seats, unit->unit.animation.seat_index, struct animation_graph_unit_seat); + weapon_class = TAG_BLOCK_GET_ELEMENT(&unit_seat->weapon_classes, unit->unit.animation.weapon_index, struct animation_graph_weapon_class); + + if (slot >= weapon_class->animations.count) + { + return; + } + animation_index = animation_graph_animation_index_get(&weapon_class->animations)[slot].animation_index; + if (animation_index == NONE) + { + return; + } + + animation = TAG_BLOCK_GET_ELEMENT(&animation_graph->animations, animation_index, struct animation); + model = model_definition_get(unit_definition->object.model.index); + if (animation->type != _animation_base || + animation->frame_count < 1 || + animation->node_count != model->nodes.count) + { + return; + } + + /* the throw keeps the spine and everything above it */ + spine = model_find_node(unit_definition->object.model.index, "bip01 spine"); + if (spine == NONE) + { + return; + } + + animation_get_node_orientations( + model, + animation, + (short)(game_time_get() % animation->frame_count), + move_orientations); + + for (node_index = 0; node_index < model->nodes.count; node_index++) + { + if (!unit_node_is_at_or_below(model, node_index, spine)) + { + node_orientations[node_index] = move_orientations[node_index]; + } + } + + return; +} void unit_preprocess_node_orientations( long unit_index, struct real_orientation *node_orientations) @@ -10565,6 +10671,7 @@ void unit_preprocess_node_orientations( unit_definition = unit_definition_get(unit->definition_index); animation_graph = animation_graph_definition_get( unit_definition->object.animation_graph.index); + unit_grenade_throw_keep_legs_moving(unit_index, node_orientations); if (unit->unit.animation.action_animation.index != NONE) { From fd7f2d30b6ac9b5868c627776d10b10ef8997519 Mon Sep 17 00:00:00 2001 From: RockBooch Date: Tue, 6 Oct 2026 02:27:29 -0400 Subject: [PATCH 026/187] Update units.c Blend leg directions and ease the stop during grenade throws --- source/units/units.c | 255 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 213 insertions(+), 42 deletions(-) diff --git a/source/units/units.c b/source/units/units.c index 9e8ca0e5..9d91cc67 100644 --- a/source/units/units.c +++ b/source/units/units.c @@ -10713,6 +10713,94 @@ static boolean unit_node_is_at_or_below( return FALSE; } +/* the speed, in world units per second, at and above which the legs move +fully; below it they blend toward the rest pose */ +#define GRENADE_THROW_FULL_MOVE_SPEED 0.75f +/* the ticks the legs take to blend fully in or out, as the engine's own +transitions between animation states */ +#define GRENADE_THROW_BLEND_TICKS 6 +#define GRENADE_THROW_MAXIMUM_TRACKED_UNITS 2048 + +static real grenade_throw_leg_weights[GRENADE_THROW_MAXIMUM_TRACKED_UNITS]; +static long grenade_throw_leg_weight_times[GRENADE_THROW_MAXIMUM_TRACKED_UNITS]; + +/* one of the weapon class's base animations, NULL where the graph has none +that fits the model */ +static struct animation *unit_weapon_class_base_animation( + struct model *model, + struct animation_graph *animation_graph, + struct animation_graph_weapon_class *weapon_class, + short slot) +{ + short animation_index; + struct animation *animation; + + if (slot < 0 || slot >= weapon_class->animations.count) + { + return NULL; + } + animation_index = animation_graph_animation_index_get(&weapon_class->animations)[slot].animation_index; + if (animation_index == NONE) + { + return NULL; + } + + animation = TAG_BLOCK_GET_ELEMENT(&animation_graph->animations, animation_index, struct animation); + if (animation->type != _animation_base || + animation->frame_count < 1 || + animation->node_count != model->nodes.count) + { + return NULL; + } + + return animation; +} + +/* the animation's pose at a point (0 to 1) through its cycle */ +static void unit_animation_pose_at_phase( + struct model *model, + struct animation *animation, + real phase, + struct real_orientation *orientations) +{ + long frame_index = (long)(phase * animation->frame_count); + + frame_index = PIN(frame_index, 0, animation->frame_count - 1); + animation_get_node_orientations(model, animation, (short)frame_index, orientations); + + return; +} + +static void unit_node_orientations_blend( + short node_count, + struct real_orientation const *from, + struct real_orientation const *to, + real fraction, + struct real_orientation *result) +{ + short node_index; + + for (node_index = 0; node_index < node_count; node_index++) + { + quaternions_interpolate_and_normalize( + &from[node_index].rotation, + &to[node_index].rotation, + fraction, + &result[node_index].rotation); + points_interpolate( + &from[node_index].translation, + &to[node_index].translation, + fraction, + &result[node_index].translation); + scalars_interpolate( + from[node_index].scale, + to[node_index].scale, + fraction, + &result[node_index].scale); + } + + return; +} static void unit_grenade_throw_keep_legs_moving( long unit_index, @@ -10723,58 +10811,41 @@ static void unit_grenade_throw_keep_legs_moving( struct animation_graph *animation_graph; struct animation_graph_unit_seat *unit_seat; struct animation_graph_weapon_class *weapon_class; - struct animation *animation; struct model *model; + struct animation *idle_animation; + struct animation *front_animation; + struct animation *forward_animation; + struct animation *side_animation; + struct real_orientation rest_orientations[MAXIMUM_NODES_PER_MODEL]; + struct real_orientation forward_orientations[MAXIMUM_NODES_PER_MODEL]; + struct real_orientation side_orientations[MAXIMUM_NODES_PER_MODEL]; struct real_orientation move_orientations[MAXIMUM_NODES_PER_MODEL]; - short slot; - short animation_index; + struct real_orientation legs_orientations[MAXIMUM_NODES_PER_MODEL]; + real_vector3d left; + real forward_speed; + real left_speed; + real target_weight; + real move_weight; + real side_weight; + real phase; + long absolute_index; + long now; + boolean crouching; + boolean moving; short spine; short node_index; if (unit->unit.animation.state != _unit_state_throw_grenade || unit->object.parent_object_index != NONE || - unit->unit.animation.seat_index == NONE || - magnitude3d(&unit->unit.throttle) < 0.1f) + unit->unit.animation.seat_index == NONE) { return; } - /* the movement animation for the direction the unit is pushed */ - if (fabs(unit->unit.throttle.i) >= fabs(unit->unit.throttle.j)) - { - slot = unit->unit.throttle.i > 0.f ? - _unit_weapon_class_animation_moving_front : - _unit_weapon_class_animation_moving_back; - } - else - { - slot = unit->unit.throttle.j > 0.f ? - _unit_weapon_class_animation_moving_left : - _unit_weapon_class_animation_moving_right; - } - animation_graph = animation_graph_definition_get(unit_definition->object.animation_graph.index); unit_seat = TAG_BLOCK_GET_ELEMENT(&animation_graph->unit_seats, unit->unit.animation.seat_index, struct animation_graph_unit_seat); weapon_class = TAG_BLOCK_GET_ELEMENT(&unit_seat->weapon_classes, unit->unit.animation.weapon_index, struct animation_graph_weapon_class); - - if (slot >= weapon_class->animations.count) - { - return; - } - animation_index = animation_graph_animation_index_get(&weapon_class->animations)[slot].animation_index; - if (animation_index == NONE) - { - return; - } - - animation = TAG_BLOCK_GET_ELEMENT(&animation_graph->animations, animation_index, struct animation); model = model_definition_get(unit_definition->object.model.index); - if (animation->type != _animation_base || - animation->frame_count < 1 || - animation->node_count != model->nodes.count) - { - return; - } /* the throw keeps the spine and everything above it */ spine = model_find_node(unit_definition->object.model.index, "bip01 spine"); @@ -10783,17 +10854,117 @@ static void unit_grenade_throw_keep_legs_moving( return; } - animation_get_node_orientations( + /* the unit's real speed along its facing and to its left, per second */ + cross_product3d(&unit->object.up, &unit->object.forward, &left); + forward_speed = dot_product3d(&unit->object.translational_velocity, &unit->object.forward) * TICKS_PER_SECOND; + left_speed = dot_product3d(&unit->object.translational_velocity, &left) * TICKS_PER_SECOND; + target_weight = square_root(forward_speed * forward_speed + left_speed * left_speed) / GRENADE_THROW_FULL_MOVE_SPEED; + target_weight = PIN(target_weight, 0.f, 1.f); + + /* the legs ease toward the target over a few ticks, so a sudden stop + still blends out; a new throw starts at the target */ + move_weight = target_weight; + now = game_time_get(); + absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_index); + if (absolute_index >= 0 && absolute_index < GRENADE_THROW_MAXIMUM_TRACKED_UNITS) + { + if (grenade_throw_leg_weight_times[absolute_index] == now) + { + move_weight = grenade_throw_leg_weights[absolute_index]; + } + else + { + if (grenade_throw_leg_weight_times[absolute_index] == now - 1) + { + real step = 1.f / GRENADE_THROW_BLEND_TICKS; + real previous_weight = grenade_throw_leg_weights[absolute_index]; + + move_weight = previous_weight + PIN(target_weight - previous_weight, -step, step); + } + grenade_throw_leg_weights[absolute_index] = move_weight; + grenade_throw_leg_weight_times[absolute_index] = now; + } + } + + /* the rest pose: the crouching idle when crouched, else the throw's own legs */ + crouching = unit->unit.animation.base_seat_index == _unit_base_seat_crouch; + idle_animation = crouching ? + unit_weapon_class_base_animation(model, animation_graph, weapon_class, _unit_weapon_class_animation_idle) : + NULL; + if (idle_animation) + { + unit_animation_pose_at_phase( + model, + idle_animation, + (real)(now % idle_animation->frame_count) / (real)idle_animation->frame_count, + rest_orientations); + } + else + { + if (move_weight <= 0.f) + { + return; + } + for (node_index = 0; node_index < model->nodes.count; node_index++) + { + rest_orientations[node_index] = node_orientations[node_index]; + } + } + + /* the movement pose: the forward or back walk blended with the left or + right walk by how much of the movement is sideways, all at one point in + the walk cycle so the feet stay in step */ + front_animation = unit_weapon_class_base_animation(model, animation_graph, weapon_class, _unit_weapon_class_animation_moving_front); + phase = front_animation ? + (real)(now % front_animation->frame_count) / (real)front_animation->frame_count : + 0.f; + forward_animation = unit_weapon_class_base_animation( model, - animation, - (short)(game_time_get() % animation->frame_count), - move_orientations); + animation_graph, + weapon_class, + forward_speed >= 0.f ? _unit_weapon_class_animation_moving_front : _unit_weapon_class_animation_moving_back); + side_animation = unit_weapon_class_base_animation( + model, + animation_graph, + weapon_class, + left_speed > 0.f ? _unit_weapon_class_animation_moving_left : _unit_weapon_class_animation_moving_right); + side_weight = (real)(fabs(forward_speed) + fabs(left_speed)); + side_weight = side_weight > 0.f ? (real)fabs(left_speed) / side_weight : 0.f; + + moving = FALSE; + if (move_weight > 0.f) + { + if (forward_animation && side_animation) + { + unit_animation_pose_at_phase(model, forward_animation, phase, forward_orientations); + unit_animation_pose_at_phase(model, side_animation, phase, side_orientations); + unit_node_orientations_blend(model->nodes.count, forward_orientations, side_orientations, side_weight, move_orientations); + moving = TRUE; + } + else if (forward_animation) + { + unit_animation_pose_at_phase(model, forward_animation, phase, move_orientations); + moving = TRUE; + } + else if (side_animation) + { + unit_animation_pose_at_phase(model, side_animation, phase, move_orientations); + moving = TRUE; + } + } + + if (moving) + { + unit_node_orientations_blend(model->nodes.count, rest_orientations, move_orientations, move_weight, legs_orientations); + } for (node_index = 0; node_index < model->nodes.count; node_index++) { if (!unit_node_is_at_or_below(model, node_index, spine)) { - node_orientations[node_index] = move_orientations[node_index]; + node_orientations[node_index] = moving ? + legs_orientations[node_index] : + rest_orientations[node_index]; } } From abae0d09282fda5d0cac35fa34bed5ebba9cdc0b Mon Sep 17 00:00:00 2001 From: RockBooch Date: Tue, 6 Oct 2026 02:47:45 -0400 Subject: [PATCH 027/187] Update bipeds.c Turn around while throwing grenade no longer locks until animation is over --- source/units/bipeds.c | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/source/units/bipeds.c b/source/units/bipeds.c index a5fb7608..d71814c9 100644 --- a/source/units/bipeds.c +++ b/source/units/bipeds.c @@ -3291,14 +3291,20 @@ static void biped_update_turning( biped_snap_facing(biped_index); biped_verify_object_vectors(biped_index, "post-fly-turn"); } - else if (biped->unit.animation.base_seat_index!=_unit_base_seat_asleep) - { - boolean flaming = - biped->unit.animation.base_seat_index==_unit_base_seat_flaming; - real_vector3d turn_axis; - real turn_error; - real facing_alignment; - boolean turn_right; + else if (biped->unit.animation.base_seat_index!=_unit_base_seat_asleep) + { + boolean flaming = + biped->unit.animation.base_seat_index==_unit_base_seat_flaming; + /* port: a player throwing a grenade turns with the aim, as one + meleeing does: the melee plays over the moving or idle state, the + throw is a state of its own, which the original never turned in */ + boolean throwing_turn = + biped->unit.player_index!=NONE && + biped->unit.animation.state==_unit_state_throw_grenade; + real_vector3d turn_axis; + real turn_error; + real facing_alignment; + boolean turn_right; if (TEST_FLAG(flags, _biped_climbs_anything_bit)) { @@ -3358,7 +3364,8 @@ static void biped_update_turning( } if (biped->biped.state==biped_state_moving || - TEST_FLAG(flags, _biped_turns_without_animating_bit)) + TEST_FLAG(flags, _biped_turns_without_animating_bit) || + throwing_turn) { if (!TEST_FLAG(biped->unit.control_flags, _unit_control_look_dont_turn_bit)) { From 1fe6836960150cf40305fc76fae71d2a97ae8edf Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Tue, 6 Oct 2026 18:38:32 -0400 Subject: [PATCH 028/187] Add finer mouse sensitivity steps --- ...select.player_setup.player_profile_edit.mouse_settings.xml | 4 ++-- tools/port_settings.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml index a26889d4..a3590681 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml @@ -30,7 +30,7 @@ - + @@ -39,7 +39,7 @@ - + diff --git a/tools/port_settings.py b/tools/port_settings.py index 95c226c9..a9e38832 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -18,7 +18,7 @@ from xml.sax.saxutils import quoteattr PE = "main_menu/settings_select/player_setup/player_profile_edit" YES_NO = [("YES", "true"), ("NO", "false")] ON_OFF = [("ON", "true"), ("OFF", "false")] -SENSITIVITIES = [(f"{value:g}", f"{value:g}") for value in (0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4)] +SENSITIVITIES = [(f"{value:g}", f"{value:g}") for value in (0.1, 0.15, 0.25, 0.4, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4)] VOLUMES = [(str(step), f"{step / 10:g}") for step in range(11)] # each screen: its folder below PE, its screen's widget (the name the profile From 7e8c8b1d565ca44a8fe5a4313dcfcce528bcf8c6 Mon Sep 17 00:00:00 2001 From: Kirk Lazarus <7864917+kirklandsig@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:14:15 -0400 Subject: [PATCH 029/187] Menus: no save file read each frame The server and LAN browsers name each visible row's map every frame (map_display_name), and the lobby shows its map every frame (lobby_map_show). Both went through ui_widget_port_multiplayer_maps, which also reads the map used last from z:\lastmpmp.txt. Until a game has been hosted there is no such file, and each failed read also writes two lines to debug.txt. The map used last is now looked up only when the caller asks for it (last_used not NULL), as the menus that open a map list do. profile_name_show read the last-used profile at most once a second when the read worked, but read one that could not be read again every frame. That one is now tried again a second later too. Co-Authored-By: Claude --- port/linux/game/menu_functions.c | 21 ++++++++++++------- .../ui_widget_event_handler_functions.c | 6 +++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 0c85e5e6..e4616354 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -813,6 +813,7 @@ static void profile_name_show(struct widget_instance *description) static struct player_profile profile; static long read_index = NONE; static unsigned long read_time; + static boolean read_good; long index = player_ui_get_active_player_profile_index(0); if (!description) @@ -824,13 +825,17 @@ static void profile_name_show(struct widget_instance *description) } else if ((index = player_ui_get_player1_last_used_profile_index()) == NONE) return; - else if (index != read_index || system_milliseconds() - read_time > 1000) + else { - read_index = NONE; - if (!player_profile_get(index, &profile)) + /* (one that cannot be read too: tried again a second later) */ + if (index != read_index || system_milliseconds() - read_time > 1000) + { + read_index = index; + read_good = player_profile_get(index, &profile); + read_time = system_milliseconds(); + } + if (!read_good) return; - read_index = index; - read_time = system_milliseconds(); } text_set(named(description, "current_profile_name", 0), profile.player_name); } @@ -3103,8 +3108,8 @@ scenario's path or name */ static void map_display_name(char const *map_name, wchar_t *text) { char const *const *names; - short last, index; - short count = ui_widget_port_multiplayer_maps(&names, &last); + short index; + short count = ui_widget_port_multiplayer_maps(&names, NULL); for (index = 0; index < count; index++) { @@ -4037,7 +4042,7 @@ static void lobby_row_text(short row, wchar_t *text) static void lobby_map_show(struct widget_instance *description, char const *map_name) { char const *const *names; - short last, count = ui_widget_port_multiplayer_maps(&names, &last), map = 19, index; + short count = ui_widget_port_multiplayer_maps(&names, NULL), map = 19, index; short level = campaign_level_of(map_name); struct widget_instance *widget; diff --git a/source/interface/ui_widget_event_handler_functions.c b/source/interface/ui_widget_event_handler_functions.c index 45147d92..2d2dca43 100644 --- a/source/interface/ui_widget_event_handler_functions.c +++ b/source/interface/ui_widget_event_handler_functions.c @@ -5881,7 +5881,9 @@ static boolean solo_level_initialize_list_single_player( /* port: the PC version's multiplayer menus (port/linux/game/menu_functions.c), on our lists rather than the Xbox's spinners: */ -/* the multiplayer maps (the Xbox's 13), and the one used last (else 0) */ +/* the multiplayer maps (the Xbox's 13), and the one used last (else 0), +unless last_used is NULL: it is read from a file of the save root, which the +menus that name maps each frame need not do */ short ui_widget_port_multiplayer_maps( char const *const **names, short *last_used) @@ -5890,6 +5892,8 @@ short ui_widget_port_multiplayer_maps( short level_index; *names = (char const *const *)event_handler_functions.multiplayer_levels; + if (!last_used) + return 13; *last_used = 0; if (saved_game_file_retrieve_last_used_multiplayer_map(map_name)) { From a2166da3521832655b7004715665950275ddd226 Mon Sep 17 00:00:00 2001 From: RockBooch Date: Tue, 6 Oct 2026 22:53:03 -0400 Subject: [PATCH 030/187] Update units.c Improve grenade throw and vehicle rider animations Keep the legs moving during grenade throws, blended by speed and direction, with smooth stops Stay crouched when throwing while crouched Use the floating jump pose when throwing in mid-air Keep Warthog and Scorpion riders seated when throwing Release the Scorpion grip IK during throws and reloads Borrow the standing reload for Scorpion seats that have none Let players turn their body while throwing (bipeds.c) --- source/units/units.c | 234 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 197 insertions(+), 37 deletions(-) diff --git a/source/units/units.c b/source/units/units.c index 9d91cc67..07055d39 100644 --- a/source/units/units.c +++ b/source/units/units.c @@ -3289,7 +3289,50 @@ void unit_impulse( return; } +/* port: a seat whose graph has no reload of its own (a Scorpion rider's) +reloads with the standing seat's, the same weapon's: a reload is a +replacement animation, which moves only the arms and the weapon */ +static short unit_standing_weapon_type_animation( + struct animation_graph *animation_graph, + struct animation_graph_weapon_class *weapon_class, + struct animation_graph_weapon_type *weapon_type, + short weapon_type_animation_index) +{ + short seat_index; + for (seat_index = 0; seat_index < animation_graph->unit_seats.count; seat_index++) + { + struct animation_graph_unit_seat *seat = TAG_BLOCK_GET_ELEMENT(&animation_graph->unit_seats, seat_index, struct animation_graph_unit_seat); + short class_index; + + if (_stricmp(seat->label, "stand")) + { + continue; + } + for (class_index = 0; class_index < seat->weapon_classes.count; class_index++) + { + struct animation_graph_weapon_class *stand_class = TAG_BLOCK_GET_ELEMENT(&seat->weapon_classes, class_index, struct animation_graph_weapon_class); + short type_index; + + if (_stricmp(stand_class->label, weapon_class->label)) + { + continue; + } + for (type_index = 0; type_index < stand_class->weapon_types.count; type_index++) + { + struct animation_graph_weapon_type *stand_type = TAG_BLOCK_GET_ELEMENT(&stand_class->weapon_types, type_index, struct animation_graph_weapon_type); + + if (!_stricmp(stand_type->label, weapon_type->label) && + weapon_type_animation_index < stand_type->animations.count) + { + return animation_graph_animation_index_get(&stand_type->animations)[weapon_type_animation_index].animation_index; + } + } + } + } + + return NONE; +} void unit_animation_start_action( long unit_index, short action) @@ -3368,7 +3411,16 @@ void unit_animation_start_action( animation_index = NONE; } } - + /* port: a seat with no reload borrows the standing one's */ + if (animation_index == NONE && + (weapon_type_animation_index == 0 || weapon_type_animation_index == 1)) + { + animation_index = unit_standing_weapon_type_animation( + animation_graph, + weapon_class, + weapon_type, + weapon_type_animation_index); + } interpolation_frame_count = action_index==7 ? 0 : 6; if (animation_index!=NONE) @@ -10723,6 +10775,76 @@ transitions between animation states */ static real grenade_throw_leg_weights[GRENADE_THROW_MAXIMUM_TRACKED_UNITS]; static long grenade_throw_leg_weight_times[GRENADE_THROW_MAXIMUM_TRACKED_UNITS]; +static real grenade_throw_air_weights[GRENADE_THROW_MAXIMUM_TRACKED_UNITS]; +static long grenade_throw_air_weight_times[GRENADE_THROW_MAXIMUM_TRACKED_UNITS]; +static long grenade_throw_ik_release_times[GRENADE_THROW_MAXIMUM_TRACKED_UNITS]; + +/* a weight eased toward its target by a step a tick; a new throw starts +at the target */ +static real unit_grenade_throw_ease( + real *weights, + long *times, + long unit_index, + real target) +{ + long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_index); + long now = game_time_get(); + real weight = target; + + if (absolute_index >= 0 && absolute_index < GRENADE_THROW_MAXIMUM_TRACKED_UNITS) + { + if (times[absolute_index] == now) + { + weight = weights[absolute_index]; + } + else + { + if (times[absolute_index] == now - 1) + { + real step = 1.f / GRENADE_THROW_BLEND_TICKS; + + weight = weights[absolute_index] + PIN(target - weights[absolute_index], -step, step); + } + weights[absolute_index] = weight; + times[absolute_index] = now; + } + } + + return weight; +} + +/* port: a rider's hands leave the vehicle's grips while throwing a grenade +or playing an action (a reload, readying or putting away a weapon, a +melee), and through the interpolation back to the seat after it, which the +grips would otherwise snap */ +static boolean unit_grenade_throw_releases_vehicle_ik( + long unit_index) +{ + struct unit_datum *unit = unit_get(unit_index); + long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_index); + long now = game_time_get(); + long elapsed; + boolean releasing = + unit->unit.animation.state == _unit_state_throw_grenade || + unit->unit.animation.action != 0; + + if (absolute_index < 0 || absolute_index >= GRENADE_THROW_MAXIMUM_TRACKED_UNITS) + { + return releasing; + } + if (releasing) + { + grenade_throw_ik_release_times[absolute_index] = now + 1; + return TRUE; + } + if (grenade_throw_ik_release_times[absolute_index] == 0) + { + return FALSE; + } + elapsed = now + 1 - grenade_throw_ik_release_times[absolute_index]; + + return elapsed >= 0 && elapsed <= GRENADE_THROW_BLEND_TICKS; +} /* one of the weapon class's base animations, NULL where the graph has none that fits the model */ @@ -10813,6 +10935,7 @@ static void unit_grenade_throw_keep_legs_moving( struct animation_graph_weapon_class *weapon_class; struct model *model; struct animation *idle_animation; + struct animation *air_animation; struct animation *front_animation; struct animation *forward_animation; struct animation *side_animation; @@ -10821,22 +10944,24 @@ static void unit_grenade_throw_keep_legs_moving( struct real_orientation side_orientations[MAXIMUM_NODES_PER_MODEL]; struct real_orientation move_orientations[MAXIMUM_NODES_PER_MODEL]; struct real_orientation legs_orientations[MAXIMUM_NODES_PER_MODEL]; + struct real_orientation air_orientations[MAXIMUM_NODES_PER_MODEL]; + struct real_orientation *legs; real_vector3d left; real forward_speed; real left_speed; real target_weight; real move_weight; + real air_weight; real side_weight; real phase; - long absolute_index; long now; + boolean airborne; boolean crouching; boolean moving; short spine; short node_index; if (unit->unit.animation.state != _unit_state_throw_grenade || - unit->object.parent_object_index != NONE || unit->unit.animation.seat_index == NONE) { return; @@ -10846,6 +10971,7 @@ static void unit_grenade_throw_keep_legs_moving( unit_seat = TAG_BLOCK_GET_ELEMENT(&animation_graph->unit_seats, unit->unit.animation.seat_index, struct animation_graph_unit_seat); weapon_class = TAG_BLOCK_GET_ELEMENT(&unit_seat->weapon_classes, unit->unit.animation.weapon_index, struct animation_graph_weapon_class); model = model_definition_get(unit_definition->object.model.index); + now = game_time_get(); /* the throw keeps the spine and everything above it */ spine = model_find_node(unit_definition->object.model.index, "bip01 spine"); @@ -10854,43 +10980,65 @@ static void unit_grenade_throw_keep_legs_moving( return; } + /* port: a rider throwing from a vehicle's seat stays seated: the lower + body keeps the seat's own idle pose, as the throw is the standing one */ + if (unit->object.parent_object_index != NONE) + { + struct animation *seated_animation = unit_weapon_class_base_animation( + model, + animation_graph, + weapon_class, + _unit_weapon_class_animation_idle); + + if (seated_animation) + { + unit_animation_pose_at_phase( + model, + seated_animation, + (real)(now % seated_animation->frame_count) / (real)seated_animation->frame_count, + rest_orientations); + for (node_index = 0; node_index < model->nodes.count; node_index++) + { + if (!unit_node_is_at_or_below(model, node_index, spine)) + { + node_orientations[node_index] = rest_orientations[node_index]; + } + } + } + + return; + } + + /* in the air the legs float as in a jump, and do not run; both ease in + and out over a few ticks */ + airborne = unit_flying_through_air(unit_index); + air_weight = unit_grenade_throw_ease( + grenade_throw_air_weights, + grenade_throw_air_weight_times, + unit_index, + airborne ? 1.f : 0.f); + /* the unit's real speed along its facing and to its left, per second */ cross_product3d(&unit->object.up, &unit->object.forward, &left); forward_speed = dot_product3d(&unit->object.translational_velocity, &unit->object.forward) * TICKS_PER_SECOND; left_speed = dot_product3d(&unit->object.translational_velocity, &left) * TICKS_PER_SECOND; target_weight = square_root(forward_speed * forward_speed + left_speed * left_speed) / GRENADE_THROW_FULL_MOVE_SPEED; - target_weight = PIN(target_weight, 0.f, 1.f); - - /* the legs ease toward the target over a few ticks, so a sudden stop - still blends out; a new throw starts at the target */ - move_weight = target_weight; - now = game_time_get(); - absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_index); - if (absolute_index >= 0 && absolute_index < GRENADE_THROW_MAXIMUM_TRACKED_UNITS) - { - if (grenade_throw_leg_weight_times[absolute_index] == now) - { - move_weight = grenade_throw_leg_weights[absolute_index]; - } - else - { - if (grenade_throw_leg_weight_times[absolute_index] == now - 1) - { - real step = 1.f / GRENADE_THROW_BLEND_TICKS; - real previous_weight = grenade_throw_leg_weights[absolute_index]; - - move_weight = previous_weight + PIN(target_weight - previous_weight, -step, step); - } - grenade_throw_leg_weights[absolute_index] = move_weight; - grenade_throw_leg_weight_times[absolute_index] = now; - } - } + target_weight = airborne ? 0.f : PIN(target_weight, 0.f, 1.f); + move_weight = unit_grenade_throw_ease( + grenade_throw_leg_weights, + grenade_throw_leg_weight_times, + unit_index, + target_weight); /* the rest pose: the crouching idle when crouched, else the throw's own legs */ crouching = unit->unit.animation.base_seat_index == _unit_base_seat_crouch; idle_animation = crouching ? unit_weapon_class_base_animation(model, animation_graph, weapon_class, _unit_weapon_class_animation_idle) : NULL; + if (!idle_animation && move_weight <= 0.f && air_weight <= 0.f) + { + return; + } if (idle_animation) { unit_animation_pose_at_phase( @@ -10901,10 +11049,6 @@ static void unit_grenade_throw_keep_legs_moving( } else { - if (move_weight <= 0.f) - { - return; - } for (node_index = 0; node_index < model->nodes.count; node_index++) { rest_orientations[node_index] = node_orientations[node_index]; @@ -10953,18 +11097,33 @@ static void unit_grenade_throw_keep_legs_moving( } } + legs = rest_orientations; if (moving) { unit_node_orientations_blend(model->nodes.count, rest_orientations, move_orientations, move_weight, legs_orientations); + legs = legs_orientations; + } + + /* the floating legs of a jump over all of it */ + air_animation = air_weight > 0.f ? + unit_weapon_class_base_animation(model, animation_graph, weapon_class, _unit_weapon_class_animation_airborne) : + NULL; + if (air_animation) + { + unit_animation_pose_at_phase( + model, + air_animation, + (real)(now % air_animation->frame_count) / (real)air_animation->frame_count, + air_orientations); + unit_node_orientations_blend(model->nodes.count, legs, air_orientations, air_weight, forward_orientations); + legs = forward_orientations; } for (node_index = 0; node_index < model->nodes.count; node_index++) { if (!unit_node_is_at_or_below(model, node_index, spine)) { - node_orientations[node_index] = moving ? - legs_orientations[node_index] : - rest_orientations[node_index]; + node_orientations[node_index] = legs[node_index]; } } @@ -11320,7 +11479,8 @@ void unit_postprocess_node_matrices( struct animation_graph_weapon_class); if (unit->object.parent_object_index!=NONE && - unit_animation_vehicle_ik(&unit->unit.animation)) + unit_animation_vehicle_ik(&unit->unit.animation) && + !unit_grenade_throw_releases_vehicle_ik(unit_index)) { ik_points = &unit_seat->ik_points; ik_point_index = 0; From 1860a94813fe255b0b4d588b8a2501e1bce16823 Mon Sep 17 00:00:00 2001 From: RockBooch Date: Wed, 7 Oct 2026 02:16:40 -0400 Subject: [PATCH 031/187] All animation fixes only applied to cyborg and cyborg_mp All animation fixes only applied to cyborg and cyborg_mp --- source/units/bipeds.c | 9 ++++++--- source/units/units.c | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/source/units/bipeds.c b/source/units/bipeds.c index 1e1ed697..0588ecc4 100644 --- a/source/units/bipeds.c +++ b/source/units/bipeds.c @@ -518,6 +518,8 @@ static void biped_update_jumping( struct unit_animation_update_data *animation); static void biped_update_physics( struct biped_physics *physics); +/* units.c's: whether the unit gets the animation enhancements */ +boolean unit_animation_enhanced(long unit_index); static void biped_snap_facing( long biped_index); @@ -3336,9 +3338,10 @@ static void biped_update_turning( /* port: a player throwing a grenade turns with the aim, as one meleeing does: the melee plays over the moving or idle state, the throw is a state of its own, which the original never turned in */ - boolean throwing_turn = - biped->unit.player_index!=NONE && - biped->unit.animation.state==_unit_state_throw_grenade; + boolean throwing_turn = + biped->unit.player_index!=NONE && + biped->unit.animation.state==_unit_state_throw_grenade && + unit_animation_enhanced(biped_index); real_vector3d turn_axis; real turn_error; real facing_alignment; diff --git a/source/units/units.c b/source/units/units.c index 2130683c..78f2d16c 100644 --- a/source/units/units.c +++ b/source/units/units.c @@ -3313,6 +3313,32 @@ void unit_impulse( return; } +/* port: the animation enhancements are for the player bipeds (cyborg and +cyborg_mp), whose animations they were made with; every other unit keeps +the original animations */ +static char const *const enhanced_animation_units[] = +{ + "cyborg", + "cyborg_mp", +}; + +boolean unit_animation_enhanced( + long unit_index) +{ + struct unit_datum *unit = unit_get(unit_index); + char const *name = tag_name_strip_path(tag_get_name(unit->definition_index)); + long name_index; + + for (name_index = 0; name_index < NUMBEROF(enhanced_animation_units); name_index++) + { + if (!_stricmp(name, enhanced_animation_units[name_index])) + { + return TRUE; + } + } + + return FALSE; +} /* port: a seat whose graph has no reload of its own (a Scorpion rider's) reloads with the standing seat's, the same weapon's: a reload is a replacement animation, which moves only the arms and the weapon */ @@ -3437,7 +3463,8 @@ void unit_animation_start_action( } /* port: a seat with no reload borrows the standing one's */ if (animation_index == NONE && - (weapon_type_animation_index == 0 || weapon_type_animation_index == 1)) + (weapon_type_animation_index == 0 || weapon_type_animation_index == 1) && + unit_animation_enhanced(unit_index)) { animation_index = unit_standing_weapon_type_animation( animation_graph, @@ -10882,6 +10909,10 @@ static boolean unit_grenade_throw_releases_vehicle_ik( unit->unit.animation.state == _unit_state_throw_grenade || unit->unit.animation.action != 0; + if (!unit_animation_enhanced(unit_index)) + { + return FALSE; + } if (absolute_index < 0 || absolute_index >= GRENADE_THROW_MAXIMUM_TRACKED_UNITS) { return releasing; @@ -11069,7 +11100,8 @@ static void unit_grenade_throw_keep_legs_moving( short node_index; if (unit->unit.animation.state != _unit_state_throw_grenade || - unit->unit.animation.seat_index == NONE) + unit->unit.animation.seat_index == NONE || + !unit_animation_enhanced(unit_index)) { return; } From 343eef15ccc10e211059ebd1f9a26995b0904317 Mon Sep 17 00:00:00 2001 From: Kirk Lazarus <7864917+kirklandsig@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:16:14 -0400 Subject: [PATCH 032/187] Android renderer: no unsynchronized mirror write while a buffer copy is pending mirror_refresh writes a page uploaded for the first time through an unsynchronized mapping (no queued draw can read it) and a page uploaded again with glBufferSubData. On Mali, a glBufferSubData into a buffer that queued draws read gives the buffer new storage at once and copies the old contents into it on the GPU's timeline. An unsynchronized write made in the meantime goes into the new storage and is then overwritten by that copy. After a level change the next level's load does both in the same segments: it uploads some pages again (the earlier level's) and some for the first time, and some of the new level's vertices were drawn as the earlier level's. A level loaded first uploads no page again, so it never showed there. Each segment now records device.frame at its latest glBufferSubData. A first-time page goes in unsynchronized only once STREAM_BUFFER_RING frames have passed since then (Present waits for each frame's fence when it reuses that frame's stream buffer), and with glBufferSubData otherwise. The frames are compared by unsigned subtraction, which keeps a short age right across the counter's rollover; only an age of a whole counter period or more could alias, and only towards glBufferSubData. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_015jhszivGTnJoXcpPBKX3E1 --- port/linux/src/d3d8_gl.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index b3343d34..10ce8065 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -3761,6 +3761,10 @@ static struct /* the page's memory_watch generation when it was uploaded */ unsigned long generation[MIRROR_PAGE_COUNT]; unsigned long rewritten_frame[MIRROR_PAGE_COUNT]; +#ifdef HALO_ANDROID + /* device.frame at the segment's latest glBufferSubData */ + unsigned long subdata_frame[MIRROR_SEGMENT_COUNT]; +#endif } mirror; /* uploads the pages of [first, last) that are absent or stale; FALSE if one @@ -3844,19 +3848,28 @@ static BOOL mirror_refresh(unsigned long first, unsigned long last) glGenBuffers(1, &mirror.buffers[segment]); glBindBuffer(GL_COPY_WRITE_BUFFER, mirror.buffers[segment]); glBufferData(GL_COPY_WRITE_BUFFER, MIRROR_SEGMENT_SIZE, NULL, GL_DYNAMIC_DRAW); +#ifdef HALO_ANDROID + /* (no glBufferSubData yet: as if long enough ago) */ + mirror.subdata_frame[segment] = device.frame - STREAM_BUFFER_RING; +#endif } glBindBuffer(GL_COPY_WRITE_BUFFER, mirror.buffers[segment]); #ifdef HALO_ANDROID /* Mali copies the whole buffer for a glBufferSubData that queued draws might read (see STREAM_BUFFER_RING); unused pages can be - written without waiting for them */ - if (unused) + written without waiting for them. Not while such a copy may be + pending: it gives the buffer new storage at once but fills it on + the GPU's timeline, over what was written into it meanwhile (a level + loaded after another drew some of the earlier level's vertices). + The GPU is done with a frame's work STREAM_BUFFER_RING frames on */ + if (unused && device.frame - mirror.subdata_frame[segment] >= STREAM_BUFFER_RING) { host_gl_buffer_write(GL_COPY_WRITE_BUFFER, (unsigned int)(address - PLATFORM_CONTIGUOUS_BASE - segment * MIRROR_SEGMENT_SIZE), (unsigned int)size, (const void *)address); continue; } + mirror.subdata_frame[segment] = device.frame; #else (void)unused; #endif From 17b63c8ac301ca6e74c322cd9fd04fe343c87b62 Mon Sep 17 00:00:00 2001 From: Kirk Lazarus <7864917+kirklandsig@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:16:16 -0400 Subject: [PATCH 033/187] Sound: a stream that ran dry plays again once it has data mix_voice stops mixing a voice once more than 2 * width frames of silence have been taken (its packets ran out). The centre then never moves, so the history stays full, take_frame is never called again, and silence, which only a frame taken resets, stays over the limit. stream_process starts a stream over only when it has no packets, but the game queues its next packet before DirectSoundDoWork has completed the finished ones, so the stream still has some, and the voice stays stopped with its new packets unplayed. A streamed sound, the music, stopped for good after a hitch longer than its queued packets, such as a level load. A voice stopped this way now starts over (resampler_reset, as stream_process does) as soon as a packet not finished has frames. Packets with none left are marked finished on the way, as take_frame marks them, so DirectSoundDoWork completes them. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_015jhszivGTnJoXcpPBKX3E1 --- port/linux/src/dsound_sdl.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 713248a0..17e796e3 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -540,6 +540,26 @@ static BOOL take_frame(struct sdl_stream *stream, float *frame) } } +/* whether the voice has a frame to take; the packets with none left are +marked finished on the way, as take_frame marks them */ +static BOOL voice_has_frames(struct sdl_stream *stream) +{ + unsigned long position; + + for (position = 0; position < stream->packet_count; position++) + { + struct voice_packet *packet = &stream->packets[(stream->packet_head + position) % MAXIMUM_STREAM_PACKETS]; + + if (packet->finished) + continue; + if (stream->cursor < packet->frames) + return TRUE; + stream->cursor = 0; + packet->finished = TRUE; + } + return FALSE; +} + /* ---------- mixing */ /* mixes one voice into output (frames of stereo float), and into the reverb's @@ -560,6 +580,16 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns rate, and the frames it reaches on each side */ scale = step > 1.0 ? (float)(1.0 / (step < RESAMPLER_MAXIMUM_STRETCH ? step : RESAMPLER_MAXIMUM_STRETCH)) : 1.0f; width = (long)ceilf(RESAMPLER_ZERO_CROSSINGS / scale); + /* a voice that ran dry and stopped (below) starts over once it has + frames again: stream_process starts over only a stream with no packets, + and the next can come before the finished ones are completed */ + if (stream->silence > (unsigned long)(2 * width)) + { + if (!voice_has_frames(stream)) + return; + resampler_reset(stream); + stream->gains_valid = FALSE; + } voice_gains(stream, &target_left, &target_right, &target_room, &target_direct_lowpass, &target_room_lowpass); if (!stream->gains_valid) { From 626c42df6013f40f1b7b56269571b54844acb57c Mon Sep 17 00:00:00 2001 From: justinfilip Date: Wed, 7 Oct 2026 17:53:37 -0500 Subject: [PATCH 034/187] Skip a help text custom icon instead of halting on a missing button icon. --- source/interface/hud_messaging.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/source/interface/hud_messaging.c b/source/interface/hud_messaging.c index 5eb0af3e..638eb68d 100644 --- a/source/interface/hud_messaging.c +++ b/source/interface/hud_messaging.c @@ -1685,7 +1685,17 @@ void hud_messaging_update( } else { - error(_error_silent, "help text cannot use custom icons"); + /* Help text has no custom icons. Leave this one out. + The button icon list is the wrong list, and its index + is still NONE. */ + static boolean help_custom_icon_reported = FALSE; + + if (!help_custom_icon_reported) + { + help_custom_icon_reported = TRUE; + error(_error_silent, "help text cannot use custom icons"); + } + break; } } else @@ -1693,7 +1703,7 @@ void hud_messaging_update( icon_index = element->data; } - if (icon_index < hud_globals->messaging.button_icons.count) + if (icon_index >= 0 && icon_index < hud_globals->messaging.button_icons.count) { struct icon_hud_element_definition const *icon; From 44da50e411193910f162f39fb0aaacc1b6891ea6 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:26:59 -0400 Subject: [PATCH 035/187] Take no co-op activating cluster below zero from the host objects_port_set_activating_cluster takes the cluster a co-op host sends in its presentation (network_coop.c), and refused only NONE and indices past the BSP's clusters. Any other negative value (activating_cluster is sent plus one, so a host sending -5 or 0x8001) was stored, and the client then read the cluster PVS before the start of its array. Any negative index now means none, as NONE already did. --- source/objects/objects.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/objects/objects.c b/source/objects/objects.c index fa608720..72d40297 100644 --- a/source/objects/objects.c +++ b/source/objects/objects.c @@ -558,7 +558,8 @@ void object_pvs_set_camera_point( void objects_port_set_activating_cluster( short cluster_index) { - if (cluster_index == NONE || cluster_index >= global_structure_bsp_get()->clusters.count) + /* (port: a co-op host sends it, so any index outside the BSP's clusters is none) */ + if (cluster_index < 0 || cluster_index >= global_structure_bsp_get()->clusters.count) { object_globals->pvs_activation_type = _pvs_activation_normal; return; From 577b45d50c54c24298f6d3aa88577d25756b2502 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:27:56 -0400 Subject: [PATCH 036/187] Compile a vertex shader that fails only once vertex_shader_get kept a failed compile's 0, which it also takes to mean not compiled yet, so a vertex program the driver refused was translated and compiled again at every draw that used it, with its whole GLSL logged each time: a stall every frame, and a debug.txt that grows without end. Each of a program's four shaders is now tried once, as pixel shaders and programs already are. --- port/linux/src/d3d8_gl.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index b3343d34..7f0dad08 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -332,6 +332,10 @@ struct vertex_shader_object struct vertex_array_entry *vertex_array; unsigned long vertex_array_streams; #endif + /* which of shader[] and lit_shader[] have been compiled (bits 0 and 1, + and 2 and 3): one that failed stays 0 and is not compiled again at each + draw */ + unsigned char shaders_tried; }; /* ---------- programs */ @@ -2489,12 +2493,14 @@ static GLuint vertex_shader_get(struct vertex_shader_object *program, BOOL immed { int variant = immediate ? 1 : 0; GLuint *shader = lit ? &program->lit_shader[variant] : &program->shader[variant]; + unsigned char tried = (unsigned char)(1 << (variant + (lit ? 2 : 0))); - if (!*shader) + if (!*shader && !(program->shaders_tried & tried)) { char *source = nv2a_vertex_shader_to_glsl(program->instructions, program->instruction_count, immediate ? 0 : device.vertex_shader->packed_mask, lit ? &program->lighting : NULL); + program->shaders_tried |= tried; *shader = xgpu_compile_shader(GL_VERTEX_SHADER, source, "vertex"); if (debug_settings.dump_shaders) { From 57404d568547b682f379f1690c8cb1a79971723d Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:28:16 -0400 Subject: [PATCH 037/187] Choose no gametype in Edit Gametypes when there are none With no gametypes listed (the built-in ones could not be made: a save root that cannot be written, or its lock timed out), the list's update set the chosen row to MIN(focused, count - 1), which is -1 and passes the chosen < count checks: gametypes[-1] was shown, and A or X began editing or deleting it. The chosen row is now at least 0, as gametype_edit_read already keeps it, which with no gametypes chooses none. --- port/linux/game/menu_functions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 2732aa8e..d7cdd010 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -4936,7 +4936,7 @@ static void gametype_edit_list_update(struct widget_instance *list) gametype_edit_read(); focused = list_scroll(list, &gametype_edit.first, gametype_edit.count, GAMETYPE_EDIT_ROWS); if (focused != NONE) - gametype_edit.chosen = (short)MIN(focused, gametype_edit.count - 1); + gametype_edit.chosen = (short)MAX(0, MIN(focused, gametype_edit.count - 1)); rows_update(list, (short)MIN(gametype_edit.count, GAMETYPE_EDIT_ROWS), gametype_edit_row_text); visible_set(named(description, "gametype_right_item", 0), gametype_edit.count > 0); if (gametype_edit.chosen < gametype_edit.count) From 6694d6180e803893db1b500151955fc66d5e80b6 Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Wed, 7 Oct 2026 21:13:27 -0400 Subject: [PATCH 038/187] Fix Escape restarting the level from the console --- .github/workflows/build.yml | 4 + port/linux/src/xinput_sdl.c | 12 +- source/input/input.h | 2 + source/input/input_xbox.c | 21 ++- source/main/console.c | 7 + source/main/main.c | 3 +- tools/test_console_escape.py | 241 +++++++++++++++++++++++++++++++++++ 7 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 tools/test_console_escape.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd692e74..db281b43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,6 +70,7 @@ jobs: run: | python -m pytest -q tools/test_linux_port.py python tools/test_light_storage.py + python tools/test_console_escape.py - name: Compile cache run: ccache --show-stats @@ -205,6 +206,9 @@ jobs: - name: Test light storage run: python tools/test_light_storage.py + - name: Test console Escape ownership + run: python tools/test_console_escape.py + - name: Compile cache run: ccache --show-stats diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index a2e87267..05a860f2 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -299,18 +299,20 @@ static void keyboard_gamepad(const struct platform_input_state *input, XINPUT_GA pad->bAnalogButtons[XINPUT_GAMEPAD_Y] |= analog(k[SDL_SCANCODE_TAB]); } -/* the keys held when the game and the menus switch count as up until let go +/* the keys held when the game, menus or console switch count as up until let go of: the escape that opens the pause menu does not also back out of it, nor -the one that closes it pause the game again */ +the one that closes it pause the game again. Closing the console must also +consume its held keys before gameplay or menu bindings see them. */ static void keys_held_over_switch(struct platform_input_state *input) { static unsigned char held[SDL_SCANCODE_COUNT]; - static int menus = -1; + static int context = -1; + int next_context = (input->menus != FALSE) | (console_is_active() ? 2 : 0); int scancode; - if (menus != (input->menus != FALSE)) + if (context != next_context) { - menus = input->menus != FALSE; + context = next_context; memcpy(held, input->keys, sizeof(held)); } for (scancode = 0; scancode < SDL_SCANCODE_COUNT; scancode++) diff --git a/source/input/input.h b/source/input/input.h index bbc1253e..d6562989 100644 --- a/source/input/input.h +++ b/source/input/input.h @@ -211,6 +211,8 @@ void input_dispose(void); void input_activate(void); void input_deactivate(void); void input_flush(void); +/* Consume a debug keyboard key until release without flushing other input. */ +void input_consume_key(short key_code); boolean input_key_is_down(short key_code); boolean input_get_key(struct key_stroke *key); const struct mouse_state *input_get_mouse_state(void); diff --git a/source/input/input_xbox.c b/source/input/input_xbox.c index 1cca8c37..36c108d9 100644 --- a/source/input/input_xbox.c +++ b/source/input/input_xbox.c @@ -267,6 +267,7 @@ static DWORD WINAPI input_keyboard_thread( static struct input_globals input_globals = {0}; static long gamepad_button_down_times[MAXIMUM_GAMEPADS][NUMBER_OF_GAMEPAD_BUTTONS]; static long key_down_times[NUMBER_OF_KEYS]; +static boolean consumed_keys[NUMBER_OF_KEYS]; static const byte gamepad_analog_button_indices[NUMBER_OF_GAMEPAD_ANALOG_BUTTONS] = { @@ -432,6 +433,17 @@ static void release_input_mutex( return; } +void input_consume_key(short key_code) +{ + if (key_code >= 0 && key_code < NUMBER_OF_KEYS) + { + /* A press and release can arrive in the same update. In that case + * clear the current pulse without consuming the next fresh press. */ + consumed_keys[key_code] = input_globals.key_latches[key_code] != FALSE; + input_globals.key_ticks[key_code] = 0; + } +} + void input_suppress( void) { @@ -479,6 +491,7 @@ void input_dispose( void input_flush( void) { + csmemset(consumed_keys, 0, sizeof(consumed_keys)); csmemset( input_globals.gamepad_states, 0, @@ -568,7 +581,8 @@ boolean input_key_is_down( { boolean result = FALSE; - if (!input_globals.suppressed) + if (!input_globals.suppressed && + !(key_code >= 0 && key_code < NUMBER_OF_KEYS && consumed_keys[key_code])) { switch (key_code) { @@ -1098,13 +1112,16 @@ static void input_update_keyboard_devices( if (keystroke.Flags & XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP) { input_globals.key_latches[key.key_code] = FALSE; - if (input_globals.key_ticks[key.key_code] > 1) + if (consumed_keys[key.key_code] || input_globals.key_ticks[key.key_code] > 1) { input_globals.key_ticks[key.key_code] = 0; } + consumed_keys[key.key_code] = FALSE; } else { + if (consumed_keys[key.key_code]) + continue; if (input_globals.buffered_key_write_index < MAXIMUM_BUFFERED_KEYSTROKES) { input_globals.buffered_keys[ diff --git a/source/main/console.c b/source/main/console.c index 4b8a7103..d2e1cadc 100644 --- a/source/main/console.c +++ b/source/main/console.c @@ -140,6 +140,10 @@ void console_close( { if (console_globals.active) { + /* The reset hotkeys belong to the console until their release, even + * when it closes with backquote or an empty command instead of Escape. */ + input_consume_key(_key_escape); + input_consume_key(_key_end); terminal_gets_end(&console_globals.input_state); console_globals.active = FALSE; } @@ -447,6 +451,9 @@ boolean console_update( match_assert("c:\\halo\\SOURCE\\main\\console.c", 184, key->key_code!=NONE); switch (key->key_code) { + case _key_escape: + console_close(); + return FALSE; case _key_backquote: console_close(); break; diff --git a/source/main/main.c b/source/main/main.c index 6b950a3e..4ffdac72 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -3351,7 +3351,8 @@ void main_loop( process_ui_widgets(); bink_playback_update(); - if ((!game_in_editor() && (input_key_is_down(_key_end) || input_key_is_down(_key_escape))) || editor_should_exit()) + if ((!game_in_editor() && !console_is_active() && + (input_key_is_down(_key_end) || input_key_is_down(_key_escape))) || editor_should_exit()) { main_movie_stop(); diff --git a/tools/test_console_escape.py b/tools/test_console_escape.py new file mode 100644 index 00000000..f92c7855 --- /dev/null +++ b/tools/test_console_escape.py @@ -0,0 +1,241 @@ +"""Check console Escape ownership through production keyboard and main-loop code. + +--baseline-ref upstream/main reproduces the old failure without opening a game. +""" +import argparse +from pathlib import Path +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parent.parent + + +def block(source, marker): + start = source.index(marker) + end = source.index('{', start) + 1 + depth = 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +PRELUDE = r''' +#define _CRT_SECURE_NO_WARNINGS +#include +#include +#include +typedef int boolean,BOOL;typedef unsigned DWORD;typedef unsigned char byte; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define WINAPI +#define ERROR_SUCCESS 0 +#define ERROR_HANDLE_EOF 38 +#define XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP 1 +#define XINPUT_DEBUG_KEYSTROKE_FLAG_SHIFT 4 +#define XINPUT_DEBUG_KEYSTROKE_FLAG_CTRL 8 +#define XINPUT_DEBUG_KEYSTROKE_FLAG_ALT 16 +#define VK_OEM_3_BACKQUOTE 0xc0 +#define XDEVICE_TYPE_DEBUG_KEYBOARD 1 +#define XDEVICE_NO_SLOT 0 +#define LONG_BITS 32 +#define TICKS_PER_SECOND 30 +#define UNSIGNED_CHAR_MAX 255 +#define MAXIMUM_BUFFERED_KEYSTROKES 64 +#define MAXIMUM_NUMBER_OF_PREVIOUS_COMMANDS 8 +#define NUMBER_OF_VIRTUAL_CODES 256 +#define NUMBER_OF_ASCII_CODES 128 +#define FLAG(n) (1u<<(n)) +#define SET_FLAG(v,b,s) ((v)=(s)?((v)|FLAG(b)):((v)&~FLAG(b))) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define PIN(v,a,b) MAX((a),MIN((v),(b))) +#define csmemset memset +#define CHECK(c) do{if(!(c)){fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#c);exit(1);}}while(0) +#define match_assert(file,line,c) CHECK(c) +enum {_key_modifier_control=1,_key_modifier_alt=2}; +struct key_stroke {short key_code,ascii_code,modifier_flags;}; +struct platform_keystroke {int virtual_key,ascii,flags;}; +typedef struct {int VirtualKey,Ascii,Flags;} XINPUT_DEBUG_KEYSTROKE,*PXINPUT_DEBUG_KEYSTROKE; +static struct platform_keystroke queue[64];static int read_at,write_at; +static int text_typing,now,engine,editor,editor_exit,movie_stops,commands,completions,closes; +static struct {int switch_to_structure_bsp_index,saving_map,reset_map,lost_map;} main_globals; +static int platform_next_keystroke(struct platform_keystroke *k){if(read_at==write_at)return 0;*k=queue[read_at++];return 1;} +static void enqueue(int vk,int flags){CHECK(write_at<64);queue[write_at++]=(struct platform_keystroke){vk,0,flags};} +static long system_milliseconds(void){return now;} +static int XGetDeviceChanges(int type,unsigned long *a,unsigned long *b){(void)type;(void)a;(void)b;return 0;} +static void XInputClose(void *handle){(void)handle;} +static void *XInputOpen(int a,long b,int c,void *d){(void)a;(void)b;(void)c;(void)d;return NULL;} +static int GetLastError(void){return 0;} +#define _error_silent 0 +static void error(int level,const char *message,...){(void)level;(void)message;CHECK(0);} +static int game_in_editor(void){return editor;} +static int editor_should_exit(void){return editor_exit;} +static int game_engine_running(void){return engine;} +static void main_movie_stop(void){movie_stops++;} +struct edit_text {int unused;}; +struct terminal_gets_state {char result[512];short key_count;struct key_stroke keys[64];struct edit_text edit;}; +static boolean terminal_gets_begin(struct terminal_gets_state *s){s->key_count=0;return TRUE;} +static void terminal_gets_end(struct terminal_gets_state *s){(void)s;closes++;} +static void edit_text_selection_reset(struct edit_text *e){(void)e;} +static int profile_global_enable; +static boolean console_process_command(const char *s){(void)s;commands++;return TRUE;} +static void console_complete(void){completions++;} +enum {SDL_SCANCODE_ESCAPE,SDL_SCANCODE_END,SDL_SCANCODE_W,SDL_SCANCODE_COUNT=512}; +struct platform_input_state {byte keys[SDL_SCANCODE_COUNT];int menus;}; +''' +TESTS = r''' +static unsigned probes; +static void reset(void){ + memset(&input_globals,0,sizeof(input_globals));memset(&console_globals,0,sizeof(console_globals)); + memset(&main_globals,0,sizeof(main_globals));memset(consumed_keys,0,sizeof(consumed_keys)); + read_at=write_at=engine=editor=editor_exit=movie_stops=commands=completions=closes=0;now=0; + memset(input_globals.gamepad_states,0x35,sizeof(input_globals.gamepad_states)); +} +static void tick(void){now+=16;input_update_keyboard_devices();read_at=write_at=0;} +static void terminal_input(void){ + console_globals.input_state.key_count=0; + struct key_stroke key;while(input_get_key(&key))console_globals.input_state.keys[console_globals.input_state.key_count++]=key; +} +static void press_escape(int fast){enqueue(0x1b,0);if(fast)enqueue(0x1b,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick();} +static void escape_close(int fast,int multiplayer){ + reset();engine=multiplayer;console_open();strcpy(console_globals.input_state.result,"(quit)"); + unsigned char controllers[sizeof(input_globals.gamepad_states)];memcpy(controllers,input_globals.gamepad_states,sizeof(controllers)); + press_escape(fast);reset_hotkey();terminal_input();console_update(); + if(BASELINE){CHECK(main_globals.reset_map==!multiplayer);CHECK(console_is_active());return;} + CHECK(!main_globals.reset_map&&!movie_stops&&!console_is_active()&&closes==1&&commands==0); + CHECK(!memcmp(controllers,input_globals.gamepad_states,sizeof(controllers))); + CHECK(!input_key_is_down(_key_escape)); + for(int frame=0;frame<300;frame++){ + if(frame%10==0)enqueue(0x1b,2); /* auto-repeat, filtered after close */ + tick();reset_hotkey();CHECK(!main_globals.reset_map);CHECK(!input_key_is_down(_key_escape));probes++; + } + enqueue(0x1b,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick(); + console_open();press_escape(0);terminal_input();console_update();CHECK(!console_is_active()); +} +static void end_and_other_close(void){ + for(int closing=0;closing<3;closing++){ + reset();console_open();enqueue(0x23,0);tick();reset_hotkey(); + CHECK(!main_globals.reset_map);CHECK(input_key_is_down(_key_end)); + if(closing==0)console_close(); + else{enqueue(closing==1?VK_OEM_3_BACKQUOTE:0x0d,0);tick();terminal_input();console_update();} + CHECK(!console_is_active()); + for(int frame=0;frame<300;frame++){tick();reset_hotkey();CHECK(!main_globals.reset_map&&!input_key_is_down(_key_end));probes++;} + enqueue(0x23,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick();console_open();enqueue(0x23,0);tick();CHECK(input_key_is_down(_key_end)); + } + /* Commands and completion retain their normal behavior. */ + reset();console_open();strcpy(console_globals.input_state.result,"fov_desired 100"); + enqueue(0x0d,0);tick();terminal_input();console_update();CHECK(commands==1&&console_is_active()); + enqueue(0x09,0);tick();terminal_input();console_update();CHECK(completions==1&&console_is_active()); +} +static void consumption(void){ + /* Releasing on the first frame after close must not expose a new pulse. */ + reset();console_open();press_escape(0);reset_hotkey();terminal_input();console_update(); + enqueue(0x1b,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick();reset_hotkey(); + CHECK(!console_is_active()&&!main_globals.reset_map&&!input_key_is_down(_key_escape)); + /* Escape cancels the remaining command keys in the same buffered batch. */ + reset();console_open();strcpy(console_globals.input_state.result,"(quit)"); + enqueue(0x1b,0);enqueue(0x0d,0);tick();terminal_input();console_update(); + CHECK(!console_is_active()&&!commands); + reset();input_globals.key_latches[_key_escape]=TRUE;input_globals.key_ticks[_key_escape]=5; + input_globals.key_latches[_key_w]=TRUE;input_globals.key_ticks[_key_w]=7; + input_consume_key(_key_escape);CHECK(!input_key_is_down(_key_escape));CHECK(input_key_is_down(_key_w)==7); + console_open();enqueue(0x1b,2);tick();CHECK(input_globals.buffered_key_write_index==0&&!input_key_is_down(_key_escape)); + enqueue(0x1b,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick();CHECK(!consumed_keys[_key_escape]); + enqueue(0x1b,0);tick();CHECK(input_key_is_down(_key_escape)); + /* A full flush clears consumption; invalid keys cannot touch state. */ + input_consume_key(_key_escape);input_flush();CHECK(!consumed_keys[_key_escape]); + input_consume_key(-1);input_consume_key(NUMBER_OF_KEYS);CHECK(!input_key_is_down(_key_escape)); +} +static void ownership(void){ + for(int menu=0;menu<2;menu++)for(int fast=0;fast<2;fast++){ + reset();struct platform_input_state input={.menus=menu};keys_held_over_switch(&input); + console_open();keys_held_over_switch(&input); + input.keys[SDL_SCANCODE_ESCAPE]=1;input.keys[SDL_SCANCODE_W]=1;keys_held_over_switch(&input); + console_close(); + for(int frame=0;frame<300;frame++){ + input.keys[SDL_SCANCODE_ESCAPE]=!fast;input.keys[SDL_SCANCODE_W]=!fast;keys_held_over_switch(&input); + CHECK(!input.keys[SDL_SCANCODE_ESCAPE]&&!input.keys[SDL_SCANCODE_W]);probes++; + } + memset(input.keys,0,sizeof(input.keys));keys_held_over_switch(&input); + input.keys[SDL_SCANCODE_ESCAPE]=1;keys_held_over_switch(&input);CHECK(input.keys[SDL_SCANCODE_ESCAPE]); + /* The existing pause/menu transition still swallows the opening press. */ + input.menus=!menu;keys_held_over_switch(&input);CHECK(!input.keys[SDL_SCANCODE_ESCAPE]); + memset(input.keys,0,sizeof(input.keys));keys_held_over_switch(&input); + input.keys[SDL_SCANCODE_ESCAPE]=1;keys_held_over_switch(&input);CHECK(input.keys[SDL_SCANCODE_ESCAPE]); + } +} +static void legacy_and_editor(void){ + reset();input_globals.key_ticks[_key_escape]=1;reset_hotkey();CHECK(main_globals.reset_map&&movie_stops==1); + reset();input_globals.key_ticks[_key_end]=1;reset_hotkey();CHECK(main_globals.reset_map); + reset();engine=1;input_globals.key_ticks[_key_escape]=1;reset_hotkey();CHECK(!main_globals.reset_map&&movie_stops==1); + reset();editor=1;input_globals.key_ticks[_key_escape]=1;reset_hotkey();CHECK(!main_globals.reset_map); + console_open();editor_exit=1;reset_hotkey();CHECK(main_globals.reset_map&&movie_stops==1); +} +int main(void){ + for(int fast=0;fast<2;fast++)for(int multiplayer=0;multiplayer<2;multiplayer++)escape_close(fast,multiplayer); + if(BASELINE){puts("REPRODUCED: console Escape requests campaign reset before console update and does not close the console");return 0;} + end_and_other_close();consumption();ownership();legacy_and_editor(); + printf("PASS: %u held-input frames; console Escape closes without reset/pause spill, End editing/other close paths, repeat/release/fast taps, normal command/completion, keyboard menu transitions, unchanged co-op controller data and legacy/editor hotkeys\n",probes); +} +''' + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--cc', default='clang') + parser.add_argument('--baseline-ref') + args = parser.parse_args() + + def read(path): + if args.baseline_ref: + return subprocess.check_output(['git', 'show', f'{args.baseline_ref}:{path}'], cwd=ROOT, text=True) + return (ROOT/path).read_text() + + inputs = read('source/input/input_xbox.c') + console = read('source/main/console.c') + port = read('port/linux/src/xinput_sdl.c') + main_loop = read('source/main/main.c') + key_header = read('source/input/input.h') + start = key_header.rfind('enum', 0, key_header.index('_key_escape = 0')) + keys = block(key_header[start:], 'enum') + ';\n' + globals_source = r''' +static struct {void *keyboard_handle;byte key_ticks[NUMBER_OF_KEYS],key_latches[NUMBER_OF_KEYS]; + boolean suppressed;short buffered_key_read_index,buffered_key_write_index; + struct key_stroke buffered_keys[MAXIMUM_BUFFERED_KEYSTROKES];byte gamepad_states[4][32];} input_globals; +static long key_down_times[NUMBER_OF_KEYS]; +static boolean consumed_keys[NUMBER_OF_KEYS]; +''' + source = PRELUDE + keys + globals_source + source += block(console, 'struct console_globals') + ';\nstatic struct console_globals console_globals;\n' + source += block(console, 'boolean console_is_active(') + '\n' + for marker in ('static const short virtual_key_to_key_code', 'static const short ascii_to_key_code'): + start = inputs.index(marker) + source += inputs[start:inputs.index(';', start)+1] + '\n' + for marker in ('static void update_hold_ticks(', 'void input_flush(', 'boolean input_key_is_down(', 'boolean input_get_key('): + source += block(inputs[inputs.index('/* ---------- public code */'):], marker) + '\n' + # The device poll calls the queue adapter declared later in this harness. + source += 'DWORD WINAPI XInputDebugGetKeystroke(PXINPUT_DEBUG_KEYSTROKE);\n' + source += block(inputs[inputs.index('/* ---------- private code */'):], 'static void input_update_keyboard_devices(') + '\n' + source += (block(inputs, 'void input_consume_key(') if not args.baseline_ref else 'static void input_consume_key(short key){(void)key;}') + '\n' + source += block(port, 'DWORD WINAPI XInputDebugGetKeystroke(') + '\n' + for marker in ('void console_open(', 'void console_close(', 'boolean console_update('): + source += block(console, marker) + '\n' + source += block(port, 'static void keys_held_over_switch(') + '\n' + source += block(main_loop, 'void main_reset_map(') + '\n' + source += 'static void reset_hotkey(void){' + block(main_loop, 'if ((!game_in_editor() &&') + '}\n' + source += f'\n#define BASELINE {int(bool(args.baseline_ref))}\n' + TESTS + flags = ['-std=gnu11', '-O2', '-fuse-ld=lld'] + flags += ['--target=i686-pc-windows-msvc'] if sys.platform == 'win32' else ['-m32'] + with tempfile.TemporaryDirectory(prefix='halo-console-escape-') as directory: + path = Path(directory) + (path/'test.c').write_text(source) + exe = path/'test.exe' + subprocess.run([args.cc, *flags, str(path/'test.c'), '-o', str(exe)], check=True) + subprocess.run([str(exe)], check=True, timeout=15) + + +if __name__ == '__main__': + main() From dc71f6820bdde315f4aae907ac3e45c432cf56f1 Mon Sep 17 00:00:00 2001 From: Fernando Date: Wed, 7 Oct 2026 23:30:29 -0300 Subject: [PATCH 039/187] Fix Android main menu quit --- port/android/app/build.gradle | 9 ++++++++- port/android/app/src/main/AndroidManifest.xml | 2 +- port/linux/src/sdl_platform.c | 10 ++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/port/android/app/build.gradle b/port/android/app/build.gradle index 97b47577..da36bd00 100644 --- a/port/android/app/build.gradle +++ b/port/android/app/build.gradle @@ -16,13 +16,15 @@ def buildNumber = buildNumberText.isInteger() ? buildNumberText.toInteger() : 0 // main signed with it can install over the one before; without it, the // debug key of the computer that builds def signingKey = System.getenv("HALO_ANDROID_KEYSTORE") +def debugSideBySide = project.hasProperty("haloDebugSideBySide") android { namespace "com.halo.decomp" compileSdk 35 defaultConfig { - applicationId "com.halo.decomp" + applicationId debugSideBySide ? "com.halo.decomp.debug" : "com.halo.decomp" + manifestPlaceholders = [updateAuthority: "com.halo.decomp.update"] minSdk 28 targetSdk 35 versionCode Math.max(1, buildNumber) @@ -54,6 +56,7 @@ android { buildFeatures { buildConfig true + resValues true } signingConfigs { @@ -71,6 +74,10 @@ android { debug { debuggable true minifyEnabled false + if (debugSideBySide) { + resValue "string", "app_name", "Halo Debug" + manifestPlaceholders = [updateAuthority: "com.halo.decomp.debug.update"] + } signingConfig signingKey ? signingConfigs.halo : signingConfigs.debug } release { diff --git a/port/android/app/src/main/AndroidManifest.xml b/port/android/app/src/main/AndroidManifest.xml index 08d788e9..b9d0b472 100644 --- a/port/android/app/src/main/AndroidManifest.xml +++ b/port/android/app/src/main/AndroidManifest.xml @@ -77,7 +77,7 @@ diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 58130ee4..59872e49 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -1139,12 +1139,14 @@ static void platform_show_pending_message(void) /* ---------- events */ -/* quits as closing the window does, when the events are next read (the -menus' Quit: port/linux/game/menu_functions.c); Android's menus have none, -as the system closes its apps */ +/* quits as closing the window does (the menus' Quit: +port/linux/game/menu_functions.c) */ void platform_request_quit(void) { -#ifndef HALO_ANDROID +#ifdef HALO_ANDROID + /* The Android guest does not import SDL_PushEvent; exit reaches host_exit. */ + exit(EXIT_SUCCESS); +#else SDL_Event event; memset(&event, 0, sizeof(event)); From 78566f5bf6b05c76e9085b382c334c11d5368b15 Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Wed, 7 Oct 2026 22:36:43 -0400 Subject: [PATCH 040/187] Stop retired audio voices and cancel SDL packets without refill --- port/linux/src/dsound_sdl.c | 4 +- source/sound/sound_manager.c | 22 +++-- tools/harness/__init__.py | 13 +++ tools/harness/audio.py | 82 ++++++++++++++++ tools/harness/tests/sound_stream_flush.c | 75 +++++++++++++++ tools/harness/tests/sound_transition.c | 95 +++++++++++++++++++ .../harness/tests/test_sound_stream_flush.py | 42 ++++++++ tools/harness/tests/test_sound_transition.py | 40 ++++++++ 8 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 tools/harness/audio.py create mode 100644 tools/harness/tests/sound_stream_flush.c create mode 100644 tools/harness/tests/sound_transition.c create mode 100644 tools/harness/tests/test_sound_stream_flush.py create mode 100644 tools/harness/tests/test_sound_transition.py diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index c428e7b0..7c0cf908 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -1355,7 +1355,9 @@ static HRESULT STDMETHODCALLTYPE stream_flush(IDirectSoundStream *object) { struct voice_packet *head = &stream->packets[stream->packet_head]; - stream_complete_head(stream, head->finished ? XMEDIAPACKET_STATUS_SUCCESS : XMEDIAPACKET_STATUS_FLUSHED, + /* Cancellation is synchronous. SUCCESS would let the game's + completion callback refill the stream we are stopping. */ + stream_complete_head(stream, XMEDIAPACKET_STATUS_FLUSHED, head->finished ? head->packet.dwMaxSize : 0); } stream->cursor = 0; diff --git a/source/sound/sound_manager.c b/source/sound/sound_manager.c index 0a57f3e7..f2809395 100644 --- a/source/sound/sound_manager.c +++ b/source/sound/sound_manager.c @@ -632,7 +632,8 @@ static long looping_sound_new( long definition_index, long identifier, struct sound_source const *source); -static void sound_set_definition_end( +/* port: report whether instance limiting leaves this voice alive. */ +static boolean sound_set_definition_end( long sound_index); static long update_potentially_audible_looping_sound( long definition_index, @@ -1497,7 +1498,7 @@ static long looping_sound_new( return looping_sound_index; } -static void sound_set_definition_end( +static boolean sound_set_definition_end( long sound_index) { struct sound_datum *sound = sound_get(sound_index); @@ -1538,18 +1539,23 @@ static void sound_set_definition_end( } else { - return; + return TRUE; } if (channel_index != NONE) { - sound_index = channel_get(channel_index)->sound_index; + /* port: sound_find_like_channel excludes the current voice. */ + sound_stop(channel_get(channel_index)->sound_index); + return TRUE; } + /* port: no other voice can be preempted; tell the caller this one + was retired before it writes to the freed channel. */ sound_stop(sound_index); + return FALSE; } - return; + return TRUE; } static long update_potentially_audible_looping_sound( @@ -2956,7 +2962,11 @@ static void update_channel_for_looping_sound( (!channel->playing_permutation || channel->playing_permutation->next_permutation_index == NONE)) { - sound_set_definition_end(channel->sound_index); + /* port: a definition transition can retire its own voice. */ + if (!sound_set_definition_end(channel->sound_index)) + { + return; + } definition = sound_definition_get(sound->definition_index); pitch_range = TAG_BLOCK_GET_ELEMENT( &definition->pitch_ranges, diff --git a/tools/harness/__init__.py b/tools/harness/__init__.py index 01b8bda3..225d49c1 100644 --- a/tools/harness/__init__.py +++ b/tools/harness/__init__.py @@ -41,6 +41,19 @@ def enum_with(source, member): return source[start:source.index("};", at) + 2] +def structure(source, name): + """An actual named structure's definition, including nested unions/structures.""" + match = re.search(r"^struct\s+" + re.escape(name) + r"\s*\{", source, re.M) + if not match: + raise LookupError(f"structure not found in the sources: {name}") + end = source.index("{", match.start()) + 1 + depth = 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():source.index(";", end) + 1] + + def constant(source, name): """An enum constant's or #define's integer value, following a name it is set to (as the port's capacities are).""" match = re.search(r"\b" + re.escape(name) + r"\s*=\s*(\w+)", source) or \ diff --git a/tools/harness/audio.py b/tools/harness/audio.py new file mode 100644 index 00000000..987e0c45 --- /dev/null +++ b/tools/harness/audio.py @@ -0,0 +1,82 @@ +"""Audio fixtures use the production structures, enums and capacities.""" + +from harness import constant, enum_with, read, structure + + +def manager_types(): + manager = read("source/sound/sound_manager.c") + # The symbol table comment contains enum spellings before their declarations. + manager = manager[manager.index("/* ---------- constants */"):] + definitions = read("source/sound/sound_definitions.h") + classes = read("source/sound/sound_classes.h") + text = "typedef unsigned short word;\nstruct sound_preferences;\n" + for source, member in [(manager, "_sound_impulse ="), (manager, "_sound_waiting_for_cache_bit"), + (manager, "_sound_channel_idle"), (manager, "_sound_fade_mode_linear"), + (manager, "_fade_in_at_start_bit"), (manager, "_sound_spatialization_mode_none"), + (definitions, "_sound_definition_linked_permutations_bit"), + (definitions, "_looping_sound_fake_impulse_sound_bit"), + (read("source/sound/sound_manager.h"), "_looping_sound_refresh_start")]: + text += enum_with(source, member) + "\n" + for source, names in [(classes, ["MAXIMUM_SOUND_INSTANCES_PER_DEFINITION", + "MAXIMUM_SOUND_INSTANCES_PER_OBJECT_PER_DEFINITION"]), + (definitions, ["MAXIMUM_DETAIL_SOUNDS_PER_LOOPING_SOUND"]), + (read("source/networking/network_connection.h"), ["MAXIMUM_NUMBER_OF_LOCAL_PLAYERS"])]: + for name in names: + text += f"#define {name} {constant(source, name)}\n" + for path, names in [ + ("source/math/real_math.h", ["real_matrix4x3"]), + ("source/tag_files/tag_groups.h", ["tag_block", "tag_reference", "tag_data"]), + ("source/objects/objects.h", ["location"]), + ("source/sound/game_sound.h", ["sound_location"]), + ("source/sound/sound_environment_definitions.h", ["sound_environment_definition"]), + ("source/sound/sound_definitions.h", ["real_bounds", "sound_permutation", "sound_pitch_range", + "sound_definition", "looping_sound_scale_modifiers", "looping_sound_track", "looping_sound_definition"]), + ("source/sound/sound_classes.h", ["sound_class_definition"]), + ("source/sound/sound_manager.c", ["sound_source", "sound_listener", "sound_channel_datum", + "sound_channel_summary", "platform_sound_channel_properties", "platform_sound_listener_properties", + "sound_datum", "looping_sound_datum", "sound_platform_definition", "sound_manager_globals"]), + ]: + for name in names: + text += structure(read(path), name) + "\n" + if name == "real_matrix4x3": + text += "typedef struct real_matrix4x3 real_matrix4x3;\n" + return text + + +def stream_types(): + backend = read("port/linux/src/dsound_sdl.c") + game = read("source/sound/sound_dsound_xbox.c") + xdk = read("port/include/xdk/xdk_pdb.h") + text = """typedef unsigned long DWORD, ULONG; +typedef long LONG, HRESULT; +typedef int BOOL; +typedef void *LPVOID, *LPDIRECTSOUND, *LPDIRECTSOUNDBUFFER; +typedef long long __int64; +typedef void (*LPFNXMEDIAOBJECTCALLBACK)(void *, void *, unsigned long); +#define CALLBACK +#define STDMETHODCALLTYPE +#define S_OK 0 +""" + statuses = read("port/include/xdk/xdk_dsound.h") + for name in ["XMEDIAPACKET_STATUS_SUCCESS", "XMEDIAPACKET_STATUS_FLUSHED", + "XMEDIAPACKET_STATUS_FAILURE", "XMEDIAPACKET_STATUS_PENDING"]: + # These SDK status expressions include casts, unlike integer capacities. + text += next(line for line in statuses.splitlines() if line.startswith("#define " + name + " ")) + "\n" + for name in ["MAXIMUM_STREAM_PACKETS", "RESAMPLER_HISTORY"]: + text += f"#define {name} {constant(backend, name)}\n" + for name in ["MAXIMUM_SOUND_CHANNELS", "NUMBER_OF_SOUND_CHANNEL_TYPES"]: + text += f"#define {name} {constant(game, name)}\n" + text += enum_with(game, "_sound_channel_idle") + "\n" + text += structure(xdk, "IDirectSoundStream") + "\n" + text += structure(xdk, "_XMEDIAPACKET") + "\n" + text += structure(xdk, "_DSCAPS") + "\n" + text += """typedef struct IDirectSoundStream IDirectSoundStream, *LPDIRECTSOUNDSTREAM; +typedef struct _XMEDIAPACKET XMEDIAPACKET; +typedef struct _DSCAPS DSCAPS; +""" + text += structure(read("source/sound/sound_environment_definitions.h"), "sound_environment_definition") + "\n" + for name in ["sound_virtual_channel", "sound_channel", "dsound_globals"]: + text += structure(game, name) + "\n" + for name in ["voice_packet", "sdl_stream"]: + text += structure(backend, name) + "\n" + return text diff --git a/tools/harness/tests/sound_stream_flush.c b/tools/harness/tests/sound_stream_flush.c new file mode 100644 index 00000000..fc051eec --- /dev/null +++ b/tools/harness/tests/sound_stream_flush.c @@ -0,0 +1,75 @@ +/* Both sides of the cancellation boundary use their actual structures/functions. */ +#include "harness.h" +#include "types.inc" +static struct sdl_stream stream; +static struct sdl_stream *streams=&stream; +static struct dsound_globals dsound_globals; +static int refills,unlocks,events,resets,lock_depth; +static unsigned long status[8],completed[8]; +#define mixer_lock 0 +#define pthread_mutex_lock(...) ((void)lock_depth++) +#define pthread_mutex_unlock(...) ((void)lock_depth--) +#define channel_get(i) (&dsound_globals.channels[i]) +#define sound_cache_sound_hardware_unlock(...) ((void)unlocks++) +#define interrupt_time_error(...) CHECK(FALSE,"invalid completion") +#define SetEvent(...) ((void)events++) +#define resampler_reset(...) ((void)resets++) +static void dsound_channel_fill(short i) { + (void)i; refills++; + CHECK(refills<10,"a flush repeatedly refills itself"); + stream.packet_count++; dsound_globals.channels[0].packet_count++; +} +static HRESULT stream_flush(IDirectSoundStream *object); +#define DirectSoundStopStream(s) ((void)stream_flush(s)) +/* The real callback's Xbox pointer-sized context is cast to a short channel number. */ +#pragma clang diagnostic ignored "-Wvoid-pointer-to-int-cast" +#include "under_test.inc" +static void callback(void *context,void *packet,DWORD result) { + CHECK(lock_depth==0,"callback runs without the mixer lock"); + dsound_channel_callback(context,packet,result); +} +static void reset(int count,int finished,int wrapped) { + memset(&stream,0,sizeof(stream)); memset(&dsound_globals,0,sizeof(dsound_globals)); + stream.callback=callback; stream.packet_head=wrapped?MAXIMUM_STREAM_PACKETS-2:0; + stream.packet_count=count; stream.cursor=71; + dsound_globals.actual_channel_count=1; + struct sound_channel *channel=channel_get(0); + channel->stream=&stream.object; channel->state=_sound_channel_playing; channel->packet_count=count; + channel->playing_permutation=channel->queued_permutation=(struct sound_permutation *)&stream; + for(int i=0;ifinished=finished==2 || (finished==1 && i==0); + p->samples=malloc(16); p->packet.dwMaxSize=512; + status[i]=XMEDIAPACKET_STATUS_PENDING; completed[i]=999; + p->packet.pdwStatus=&status[i]; p->packet.pdwCompletedSize=&completed[i]; + p->packet.pContext=(void *)(long)(i+1); + } + refills=unlocks=events=resets=lock_depth=0; +} +int main(int argc,char **argv) { + CHECK(argc==2,"case name"); const char *case_name=argv[1]; + int count=!strcmp(case_name,"empty")?0:3; + int finished=!strcmp(case_name,"unfinished")?0:(!strcmp(case_name,"all-finished")?2:1); + reset(count,finished,!strcmp(case_name,"wrapped")); + CASE("ordinary") { + streams_complete_finished(); + CHECK(status[0]==XMEDIAPACKET_STATUS_SUCCESS && completed[0]==512,"ordinary completion succeeds"); + CHECK(refills==1 && unlocks==1 && stream.packet_count==3,"ordinary completion refills"); + stream_flush(&stream.object); return 0; + } + CASE("paused") { dsound_globals.paused=TRUE; } + CASE("no-callback") { stream.callback=NULL; } + channel_stop(0); + CHECK(stream.packet_count==0 && stream.cursor==0 && !lock_depth,"flush empties stream and resets cursor"); + CHECK(refills==0 && resets==1,"cancellation never refills and resets resampling"); + for(int i=0;i +#include "types.inc" +#define SET_FLAG(v,b,s) ((v)=(s)?((v)|FLAG(b)):((v)&~FLAG(b))) +#define TAG_BLOCK_GET_ELEMENT(b,i,t) (&((t *)(b)->address)[i]) +static struct sound_datum sounds[2]; +static struct sound_channel_datum channels[2]; +static struct sound_definition test_definition; +static struct sound_pitch_range range; +static struct sound_permutation permutation; +static struct looping_sound_track track; +static struct looping_sound_definition loop_definition; +static struct looping_sound_datum loop; +static struct sound_class_definition sound_class; +static struct sound_manager_globals sound_manager_globals; +static int queues, property_writes, updates; +static void channel_update(short i) { (void)i; updates++; } +static struct sound_platform_definition platform = {.channel_update=channel_update}; +static real sound_pitch_range_fade_time; +static struct sound_datum *checked_sound(long i) { + CHECK(i>=0 && i<2 && sounds[i].identifier, "access to a retired voice"); return &sounds[i]; +} +#define sound_get(i) checked_sound(i) +#define channel_get(i) (&channels[i]) +#define sound_definition_get(...) (&test_definition) +#define looping_sound_get(...) (&loop) +#define looping_sound_definition_get(...) (&loop_definition) +#define sound_class_get(...) (&sound_class) +#define sound_valid_for_channel(...) TRUE +#define source_distance_squared(...) 0.f +#define sound_definition_find_pitch_range_by_pitch(...) 0 +#define sound_definition_next_permutation(...) 0 +#define sound_scale_value(a,...) (a) +#define sound_definition_get_minimum_distance(...) 0.f +#define sound_manager_master_gain(...) 1.f +#define limit_pitch(a,...) (a) +#define sound_cache_sound_loaded(...) TRUE +#define _sound_cache_sound_request(...) TRUE +#define channel_get_state(...) _sound_channel_playing +#define channel_set_properties_hardware(...) ((void)property_writes++) +#define channel_queue_sound(...) ((void)queues++) +#define update_potentially_audible_looping_sound(...) (abort(), (long)NONE) +#define sound_start_fade(...) abort() +static void sound_stop(long i) { + sounds[i].identifier=0; channels[i].sound_index=NONE; sounds[i].playing_channel_index=NONE; +} +#include "under_test.inc" +static void reset(void) { + memset(sounds,0,sizeof(sounds)); memset(channels,0,sizeof(channels)); memset(&test_definition,0,sizeof(test_definition)); + sounds[0].identifier=sounds[1].identifier=1; + sounds[0].playing_channel_index=0; sounds[1].playing_channel_index=1; + sounds[0].type=_sound_start_track; sounds[0].next_definition_index=1; sounds[0].pitch=1; + sounds[1].definition_index=1; sounds[1].start_time=2000; + sounds[0].source.gain=1; channels[0].sound_index=0; channels[1].sound_index=1; + channels[0].playing_permutation=&permutation; permutation.next_permutation_index=NONE; permutation.gain=1; + range.permutations.address=&permutation; range.permutations.count=1; range.playback_rate=range.natural_pitch=1; + test_definition.pitch_ranges.address=⦥ test_definition.pitch_ranges.count=1; test_definition.gain_modifier=1; + loop_definition.tracks.address=&track; loop_definition.tracks.count=1; track.gain=1; + loop.tracks[0].primary_sound_index=0; + sound_manager_globals.platform_definition=&platform; sound_manager_globals.channel_count=2; + sound_manager_globals.render_time=1000; + sound_class.maximum_number_per_object=1; sound_class.maximum_number_per_definition=4; + queues=property_writes=updates=0; +} +int main(int argc,char **argv) { + CHECK(argc==2,"case name"); const char *case_name=argv[1]; reset(); + CASE("no-channel") { + sounds[0].playing_channel_index=NONE; CHECK(sound_set_definition_end(0),"channel-less voice survives"); + CHECK(sounds[0].identifier && !queues && !property_writes && !updates,"no hardware writes"); return 0; + } + CASE("other-victim") { sounds[1].start_time=0; } + CASE("no-limit") { sounds[1].definition_index=3; } + CASE("definition-limit") { + sounds[1].source_identifier=1; sound_class.maximum_number_per_object=2; + sound_class.maximum_number_per_definition=1; + } + update_channel_for_looping_sound(0,1.f); + CASE("self-retirement") { + CHECK(!sounds[0].identifier && channels[0].sound_index==NONE,"self retired"); + CHECK(!queues && !property_writes && !updates,"retired voice must never refill"); return 0; + } + CASE("definition-limit") { + CHECK(!sounds[0].identifier && !queues && !property_writes && !updates,"test_definition cap retires voice"); return 0; + } + CASE("other-victim") { + CHECK(sounds[0].identifier && !sounds[1].identifier,"only other voice retired"); + CHECK(queues==1 && property_writes==1 && updates==1,"survivor still plays"); return 0; + } + CASE("no-limit") { + CHECK(sounds[0].identifier && sounds[1].identifier,"summary excludes current voice"); + CHECK(queues==1 && property_writes==1 && updates==1,"ordinary transition still plays"); return 0; + } + CHECK(FALSE,"unknown case"); +} diff --git a/tools/harness/tests/test_sound_stream_flush.py b/tools/harness/tests/test_sound_stream_flush.py new file mode 100644 index 00000000..b6352a8d --- /dev/null +++ b/tools/harness/tests/test_sound_stream_flush.py @@ -0,0 +1,42 @@ +"""Cancellation drains SDL packets without refilling the original game channel.""" +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, function, mutated, read, run +from harness.audio import stream_types + +CASES = ["mixed-head", "unfinished", "all-finished", "paused", "repeat", "ordinary", + "wrapped", "no-callback", "empty"] +NEGATIVE_CONTROLS = { + "successful-cancellation": (("stream_complete_head(stream, XMEDIAPACKET_STATUS_FLUSHED,", + "stream_complete_head(stream, head->finished ? XMEDIAPACKET_STATUS_SUCCESS : XMEDIAPACKET_STATUS_FLUSHED,"), "mixed-head"), + "does-not-unlock-cache": (("sound_cache_sound_hardware_unlock(packet_context);", ""), "mixed-head"), + "does-not-reset-resampler": (("resampler_reset(stream);", ""), "repeat"), +} + + +def generated(fault=None): + backend = read("port/linux/src/dsound_sdl.c") + game = read("source/sound/sound_dsound_xbox.c") + code = function(game, "dsound_channel_callback") + "\n" + function(game, "channel_stop") + "\n" + code += "\n".join(function(backend, name) for name in ["packet_release", "stream_complete_head", + "streams_complete_finished", "stream_from_interface", "stream_flush"]) + if fault: + code = mutated(code, *fault) + return (("types.inc", stream_types()), ("under_test.inc", code)) + + +@pytest.mark.parametrize("case", CASES) +def test_case(case): + status, output = run(build("sound_stream_flush", generated()), case) + assert status == 0, output + + +@pytest.mark.parametrize("control", NEGATIVE_CONTROLS) +def test_negative_control(control): + fault, case = NEGATIVE_CONTROLS[control] + status, output = run(build("sound_stream_flush", generated(fault)), case) + assert status == CHECK_FAILED, f"{control} escaped {case}: {output}" diff --git a/tools/harness/tests/test_sound_transition.py b/tools/harness/tests/test_sound_transition.py new file mode 100644 index 00000000..b994a352 --- /dev/null +++ b/tools/harness/tests/test_sound_transition.py @@ -0,0 +1,40 @@ +"""Real instance limiting must not update a voice it retires.""" +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, function, mutated, read, run +from harness.audio import manager_types + +CASES = ["self-retirement", "other-victim", "no-limit", "definition-limit", "no-channel"] +NEGATIVE_CONTROLS = { + "continues-after-retirement": (("if (!sound_set_definition_end(channel->sound_index))", + "if ((sound_set_definition_end(channel->sound_index), FALSE))"), "self-retirement"), + "reports-retired-voice-alive": (("return FALSE;", "return TRUE;"), "self-retirement"), + "includes-own-channel": (("channel->sound_index != NONE && channel->sound_index != sound_index", + "channel->sound_index != NONE"), "no-limit"), +} + + +def generated(fault=None): + manager = read("source/sound/sound_manager.c") + code = "\n".join(function(manager, name) for name in ["sound_channel_summary_build", + "sound_find_like_channel", "sound_set_definition_end", "update_channel_for_looping_sound"]) + if fault: + code = mutated(code, *fault) + return (("types.inc", manager_types()), ("under_test.inc", code)) + + +@pytest.mark.parametrize("case", CASES) +def test_case(case): + status, output = run(build("sound_transition", generated()), case) + assert status == 0, output + + +@pytest.mark.parametrize("control", NEGATIVE_CONTROLS) +def test_negative_control(control): + fault, case = NEGATIVE_CONTROLS[control] + status, output = run(build("sound_transition", generated(fault)), case) + assert status == CHECK_FAILED, f"{control} escaped {case}: {output}" From 3174d332fe5f4ab35cb702066ecb250bb0f5c268 Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Wed, 7 Oct 2026 22:44:06 -0400 Subject: [PATCH 041/187] Retire replaced looping voices and preserve completed fade endpoints --- source/sound/sound_manager.c | 53 +++++++- tools/harness/__init__.py | 15 +- tools/harness/audio.py | 82 +++++++++++ tools/harness/tests/sound_lifecycle.c | 143 ++++++++++++++++++++ tools/harness/tests/test_sound_lifecycle.py | 58 ++++++++ 5 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 tools/harness/audio.py create mode 100644 tools/harness/tests/sound_lifecycle.c create mode 100644 tools/harness/tests/test_sound_lifecycle.py diff --git a/source/sound/sound_manager.c b/source/sound/sound_manager.c index 0a57f3e7..a4d28c0b 100644 --- a/source/sound/sound_manager.c +++ b/source/sound/sound_manager.c @@ -1603,6 +1603,9 @@ static long update_potentially_audible_looping_sound( sound->track_proc = track_loop_track_sound; sound->fade_stop_time = 0; sound->fade_start_time = 0; + /* port: an unfaded voice starts at full gain. */ + sound->fade_interpolation_start = 1.f; + sound->fade_interpolation_end = 1.f; sound->next_definition_index = NONE; sound->pitch_range_index = sound_definition_find_pitch_range_by_pitch( @@ -1786,7 +1789,9 @@ static real sound_calculate_fade( long sound_index) { struct sound_datum *sound = sound_get(sound_index); - real fade = 1.f; + /* port: a completed fade keeps its endpoint. Another query in this frame (or + after a cache delay) must not bring a stopped voice back to full gain. */ + real fade = sound->fade_interpolation_end; if (sound->fade_start_time != sound->fade_stop_time) { @@ -1896,6 +1901,31 @@ static void sound_start_fade( return; } +/* port: retire every owned intro/loop voice when the primary handle changes. */ +static void sound_fade_looping_track_components( + long looping_sound_index, + short track_index, + long except_sound_index, + real seconds) +{ + long sound_index; + + for (sound_index = data_next_index(sound_data, NONE); + sound_index != NONE; + sound_index = data_next_index(sound_data, sound_index)) + { + struct sound_datum *sound = sound_get(sound_index); + + if (sound_index != except_sound_index && + (sound->type == _sound_start_track || sound->type == _sound_loop_track) && + sound->source_identifier == looping_sound_index && + sound->loop_track_index == track_index) + { + sound_start_fade(_sound_fade_mode_linear, seconds, NONE, sound_index); + } + } +} + static short channel_get_state( short channel_index) { @@ -2511,6 +2541,9 @@ long sound_new_impulse( NONE); sound->fade_stop_time = 0; sound->fade_start_time = 0; + /* port: an unfaded voice starts at full gain. */ + sound->fade_interpolation_start = 1.f; + sound->fade_interpolation_end = 1.f; sound->loop_track_index = NONE; _sound_cache_sound_request( sound_permutation_get( @@ -2652,6 +2685,14 @@ boolean sound_refresh_looping( if (refresh_state == _looping_sound_refresh_start) { + /* port: a restart must retire the old primary and pending components. */ + if (track->start_sound.index != NONE || + TEST_FLAG(track->flags, _fade_in_at_start_bit)) + { + sound_fade_looping_track_components( + looping_sound_index, track_index, NONE, + track->fade_out_duration); + } if (track->start_sound.index != NONE) { *playing_sound_index = @@ -2747,6 +2788,16 @@ boolean sound_refresh_looping( } else if (loop->state != _looping_sound_refresh_stop) { + /* port: stopping the loop also stops its other owned components. */ + if (fade_time != 0.f || + TEST_FLAG(track->flags, _fade_out_at_stop_bit) || + (track->stop_sound.index == NONE && + !TEST_FLAG(definition->flags, _looping_sound_fake_impulse_sound_bit))) + { + sound_fade_looping_track_components( + looping_sound_index, track_index, *playing_sound_index, + fade_time != 0.f ? fade_time : track->fade_out_duration); + } if (fade_time != 0.f) { sound_start_fade( diff --git a/tools/harness/__init__.py b/tools/harness/__init__.py index 01b8bda3..e351e53f 100644 --- a/tools/harness/__init__.py +++ b/tools/harness/__init__.py @@ -41,6 +41,19 @@ def enum_with(source, member): return source[start:source.index("};", at) + 2] +def structure(source, name): + """An actual named structure's definition, including nested unions/structures.""" + match = re.search(r"^struct\s+" + re.escape(name) + r"\s*\{", source, re.M) + if not match: + raise LookupError(f"structure not found in the sources: {name}") + end = source.index("{", match.start()) + 1 + depth = 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():source.index(";", end) + 1] + + def constant(source, name): """An enum constant's or #define's integer value, following a name it is set to (as the port's capacities are).""" match = re.search(r"\b" + re.escape(name) + r"\s*=\s*(\w+)", source) or \ @@ -76,7 +89,7 @@ def build(test, generated): executable = work / test command = [os.environ.get("CC", "clang"), "-m32", "-std=gnu99", "-O2", "-Wall", "-Werror", "-Wno-unused-function", "-Wno-unused-variable", "-I", str(HARNESS / "include"), "-I", str(work), - str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable)] + str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable), "-lm"] result = subprocess.run(command, capture_output=True, text=True) if result.returncode: raise RuntimeError(f"{test}.c does not compile:\n{result.stderr}") diff --git a/tools/harness/audio.py b/tools/harness/audio.py new file mode 100644 index 00000000..987e0c45 --- /dev/null +++ b/tools/harness/audio.py @@ -0,0 +1,82 @@ +"""Audio fixtures use the production structures, enums and capacities.""" + +from harness import constant, enum_with, read, structure + + +def manager_types(): + manager = read("source/sound/sound_manager.c") + # The symbol table comment contains enum spellings before their declarations. + manager = manager[manager.index("/* ---------- constants */"):] + definitions = read("source/sound/sound_definitions.h") + classes = read("source/sound/sound_classes.h") + text = "typedef unsigned short word;\nstruct sound_preferences;\n" + for source, member in [(manager, "_sound_impulse ="), (manager, "_sound_waiting_for_cache_bit"), + (manager, "_sound_channel_idle"), (manager, "_sound_fade_mode_linear"), + (manager, "_fade_in_at_start_bit"), (manager, "_sound_spatialization_mode_none"), + (definitions, "_sound_definition_linked_permutations_bit"), + (definitions, "_looping_sound_fake_impulse_sound_bit"), + (read("source/sound/sound_manager.h"), "_looping_sound_refresh_start")]: + text += enum_with(source, member) + "\n" + for source, names in [(classes, ["MAXIMUM_SOUND_INSTANCES_PER_DEFINITION", + "MAXIMUM_SOUND_INSTANCES_PER_OBJECT_PER_DEFINITION"]), + (definitions, ["MAXIMUM_DETAIL_SOUNDS_PER_LOOPING_SOUND"]), + (read("source/networking/network_connection.h"), ["MAXIMUM_NUMBER_OF_LOCAL_PLAYERS"])]: + for name in names: + text += f"#define {name} {constant(source, name)}\n" + for path, names in [ + ("source/math/real_math.h", ["real_matrix4x3"]), + ("source/tag_files/tag_groups.h", ["tag_block", "tag_reference", "tag_data"]), + ("source/objects/objects.h", ["location"]), + ("source/sound/game_sound.h", ["sound_location"]), + ("source/sound/sound_environment_definitions.h", ["sound_environment_definition"]), + ("source/sound/sound_definitions.h", ["real_bounds", "sound_permutation", "sound_pitch_range", + "sound_definition", "looping_sound_scale_modifiers", "looping_sound_track", "looping_sound_definition"]), + ("source/sound/sound_classes.h", ["sound_class_definition"]), + ("source/sound/sound_manager.c", ["sound_source", "sound_listener", "sound_channel_datum", + "sound_channel_summary", "platform_sound_channel_properties", "platform_sound_listener_properties", + "sound_datum", "looping_sound_datum", "sound_platform_definition", "sound_manager_globals"]), + ]: + for name in names: + text += structure(read(path), name) + "\n" + if name == "real_matrix4x3": + text += "typedef struct real_matrix4x3 real_matrix4x3;\n" + return text + + +def stream_types(): + backend = read("port/linux/src/dsound_sdl.c") + game = read("source/sound/sound_dsound_xbox.c") + xdk = read("port/include/xdk/xdk_pdb.h") + text = """typedef unsigned long DWORD, ULONG; +typedef long LONG, HRESULT; +typedef int BOOL; +typedef void *LPVOID, *LPDIRECTSOUND, *LPDIRECTSOUNDBUFFER; +typedef long long __int64; +typedef void (*LPFNXMEDIAOBJECTCALLBACK)(void *, void *, unsigned long); +#define CALLBACK +#define STDMETHODCALLTYPE +#define S_OK 0 +""" + statuses = read("port/include/xdk/xdk_dsound.h") + for name in ["XMEDIAPACKET_STATUS_SUCCESS", "XMEDIAPACKET_STATUS_FLUSHED", + "XMEDIAPACKET_STATUS_FAILURE", "XMEDIAPACKET_STATUS_PENDING"]: + # These SDK status expressions include casts, unlike integer capacities. + text += next(line for line in statuses.splitlines() if line.startswith("#define " + name + " ")) + "\n" + for name in ["MAXIMUM_STREAM_PACKETS", "RESAMPLER_HISTORY"]: + text += f"#define {name} {constant(backend, name)}\n" + for name in ["MAXIMUM_SOUND_CHANNELS", "NUMBER_OF_SOUND_CHANNEL_TYPES"]: + text += f"#define {name} {constant(game, name)}\n" + text += enum_with(game, "_sound_channel_idle") + "\n" + text += structure(xdk, "IDirectSoundStream") + "\n" + text += structure(xdk, "_XMEDIAPACKET") + "\n" + text += structure(xdk, "_DSCAPS") + "\n" + text += """typedef struct IDirectSoundStream IDirectSoundStream, *LPDIRECTSOUNDSTREAM; +typedef struct _XMEDIAPACKET XMEDIAPACKET; +typedef struct _DSCAPS DSCAPS; +""" + text += structure(read("source/sound/sound_environment_definitions.h"), "sound_environment_definition") + "\n" + for name in ["sound_virtual_channel", "sound_channel", "dsound_globals"]: + text += structure(game, name) + "\n" + for name in ["voice_packet", "sdl_stream"]: + text += structure(backend, name) + "\n" + return text diff --git a/tools/harness/tests/sound_lifecycle.c b/tools/harness/tests/sound_lifecycle.c new file mode 100644 index 00000000..67b92a54 --- /dev/null +++ b/tools/harness/tests/sound_lifecycle.c @@ -0,0 +1,143 @@ +#include "harness.h" +#include +#include "types.inc" +#define NUMBEROF(a) (sizeof(a)/sizeof((a)[0])) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define PIN(v,a,b) ((v)<(a)?(a):((v)>(b)?(b):(v))) +#define SET_FLAG(v,b,s) ((v)=(s)?((v)|FLAG(b)):((v)&~FLAG(b))) +#define match_vassert(...) ((void)0) +#define TAG_BLOCK_GET_ELEMENT(b,i,t) (&((t *)(b)->address)[i]) +static struct sound_manager_globals sound_manager_globals; +static struct data_array *sound_data, *looping_sound_data; +static struct looping_sound_datum loops[2]; +static struct looping_sound_track tracks[4]; +static struct looping_sound_definition test_loop_definition; +static struct sound_definition test_definition; +static struct sound_pitch_range range; +static struct sound_permutation permutation; +static boolean loud_dialog_hack; +static struct sound_datum *checked_sound(long index) { + struct sound_datum *sound=datum_get(sound_data,index); + CHECK(index!=NONE && DATUM_INDEX_TO_ABSOLUTE_INDEX(index)maximum_count && sound->identifier,"live voice"); + return sound; +} +#define sound_get(i) checked_sound(i) +#define looping_sound_get(i) (&loops[i]) +#define looping_sound_definition_get(...) (&test_loop_definition) +#define sound_definition_get(...) (&test_definition) +#define sound_permutation_get(...) (&permutation) +#define sound_definition_is_playable(...) TRUE +#define sound_is_active() TRUE +#define error(...) abort() +#define sound_definition_get_maximum_distance(...) 100.f +#define source_audible(...) 0 +#define real_seed_random_range(...) 1.f +#define sound_definition_find_pitch_range_by_pitch(...) 0 +#define sound_definition_next_permutation(...) 0 +#define sound_scale_value(a,...) (a) +#define sound_scale_random_value(...) 1.f +#define real_seed_random(...) 1.f +#define game_time_get() 0 +#define sound_definition_promote(...) _sound_promotion_dont +#define sound_travel_milliseconds(...) 0 +#define source_distance(...) 0.f +#define valid_real_normal3d(...) TRUE +#define render_debug_looping_sound(...) ((void)0) +#define player_effect_continuous_refresh(...) ((void)0) +#define csmemcpy memcpy +static int _sound_cache_sound_request(struct sound_permutation *p,int a,int b,int c) { + (void)p;(void)a;(void)b;(void)c;return TRUE; +} +static boolean track_loop_track_sound(long i,void const *d,struct sound_source *s) { + (void)i;(void)d;(void)s;return TRUE; +} +static long data_next_index(struct data_array *data,long previous) { + for(long i=previous==NONE?0:DATUM_INDEX_TO_ABSOLUTE_INDEX(previous)+1;imaximum_count;i++) { + struct datum_header *header=(void *)(data->data+i*data->size); + if(header->identifier) return ((long)(unsigned short)header->identifier<<16)|i; + } + return NONE; +} +static long looping_sound_find(long identifier) { (void)identifier;return loops[0].identifier?0:NONE; } +static long looping_sound_new(long definition_index,long identifier,struct sound_source const *source) { + (void)identifier; memset(&loops[0],0,sizeof(loops[0]));loops[0].identifier=1; + loops[0].definition_index=definition_index;loops[0].source=*source;return 0; +} +static void sound_set_definition_begin(long i,long d) { sound_get(i)->next_definition_index=d; } +#include "under_test.inc" +static void reset(void) { + sound_data=game_state_data_new("sounds",64,sizeof(struct sound_datum)); + looping_sound_data=game_state_data_new("loops",2,sizeof(struct looping_sound_datum)); + memset(loops,0,sizeof(loops));memset(tracks,0,sizeof(tracks));memset(&test_loop_definition,0,sizeof(test_loop_definition)); + test_loop_definition.tracks.count=1;test_loop_definition.tracks.address=tracks; + test_loop_definition.continuous_damage_effect.index=NONE; + for(int i=0;i<4;i++) { + tracks[i].start_sound.index=1;tracks[i].loop_sound.index=2;tracks[i].stop_sound.index=NONE; + tracks[i].alternate_loop_sound.index=tracks[i].alternate_stop_sound.index=NONE; + tracks[i].flags=FLAG(_fade_out_at_stop_bit);tracks[i].fade_out_duration=.1f; + } + sound_manager_globals.render_time=1000; + sound_manager_globals.initialized=sound_manager_globals.enabled=TRUE; + test_definition.compression=_sound_compression_xbox_adpcm;test_definition.encoding=_sound_encoding_mono; +} +static void advance(int hz,int milliseconds) { + long start=sound_manager_globals.render_time; + for(int frame=1;frame<=hz*milliseconds/1000+1;frame++) { + sound_manager_globals.render_time=start+frame*1000/hz; + for(long i=data_next_index(sound_data,NONE);i!=NONE;i=data_next_index(sound_data,i)) { + struct sound_datum *sound=sound_get(i); + if(sound_calculate_fade(i)==0.f) { + loops[sound->source_identifier].component_sound_count--; + if(loops[sound->source_identifier].tracks[sound->loop_track_index].primary_sound_index==i) + loops[sound->source_identifier].tracks[sound->loop_track_index].primary_sound_index=NONE; + datum_delete(sound_data,i); + } + } + } +} +static void refresh(int state) { struct sound_source source={.scale=1.f,.gain=1.f};sound_refresh_looping(0,123,&source,state,FALSE,0.f); } +static int live(long i) { return ((struct sound_datum *)datum_get(sound_data,i))->identifier!=0; } +int main(int argc,char **argv) { + CHECK(argc==2,"case name");char *split=strchr(argv[1],':');CHECK(split,"frame rate");*split='\0'; + const char *case_name=argv[1];int hz=atoi(split+1);reset(); + CASE("fresh-impulse") { + struct sound_source source={.scale=1.f,.gain=1.f};long i=sound_new_impulse(0,&source,0,NULL,NULL,0); + CHECK(i!=NONE && sound_calculate_fade(i)==1.f,"new impulse audible");return 0; + } + if(!strcmp(case_name,"four-tracks")) test_loop_definition.tracks.count=4; + refresh(_looping_sound_refresh_start);long primary=loops[0].tracks[0].primary_sound_index,secondary=NONE; + CASE("fresh-loop") { CHECK(sound_calculate_fade(primary)==1.f,"new loop audible");return 0; } + if(!strcmp(case_name,"fade-end") || !strcmp(case_name,"crossfade")) { + sound_start_fade(!strcmp(case_name,"crossfade")?_sound_fade_mode_crossfade:_sound_fade_mode_linear,.1f,NONE,primary); + for(int f=0;f<=100;f++) { + sound_manager_globals.render_time=1000+f; + /* Store each result as real: i386's x87 may otherwise keep excess precision. */ + volatile real a=sound_calculate_fade(primary),b=sound_calculate_fade(primary); + CHECK(a==b,"repeated fade queries agree at frame %d: %a / %a",f,(double)a,(double)b); + } + CHECK(sound_calculate_fade(primary)==0.f,"fade remains zero at its endpoint"); + sound_manager_globals.render_time=5000;CHECK(sound_calculate_fade(primary)==0.f,"completed fade never revives");return 0; + } + CASE("pending") { secondary=update_potentially_audible_looping_sound(2,0,0,_sound_loop_track); } + CASE("restart") { refresh(_looping_sound_refresh_start); } + CASE("intro-loop") { + tracks[0].flags|=FLAG(_fade_in_at_start_bit);tracks[0].fade_in_duration=.2f; + refresh(_looping_sound_refresh_start); + } + CASE("rapid") { + advance(hz,34);refresh(_looping_sound_refresh_stop);advance(hz,34); + refresh(_looping_sound_refresh_start);advance(hz,34); + } + CASE("other-owner") { loops[1].source=loops[0].source;secondary=update_potentially_audible_looping_sound(2,1,0,_sound_loop_track); } + CASE("other-track") { secondary=update_potentially_audible_looping_sound(2,0,1,_sound_loop_track); } + CASE("stop-cue") { tracks[0].stop_sound.index=3; } + CASE("fake-impulse") { test_loop_definition.flags=FLAG(_looping_sound_fake_impulse_sound_bit);tracks[0].flags=0; } + refresh(_looping_sound_refresh_stop); + CASE("repeated-stop") { for(int frame=0;frame<5;frame++) { advance(hz,20);refresh(_looping_sound_refresh_stop); } } + advance(hz,300); + CASE("other-owner") { CHECK(!live(primary) && live(secondary),"other owner survives");return 0; } + CASE("other-track") { CHECK(!live(primary) && live(secondary),"other track survives");return 0; } + CASE("stop-cue") { CHECK(!live(primary) && loops[0].component_sound_count==1,"authored stop cue survives");return 0; } + CASE("fake-impulse") { CHECK(live(primary),"ordered impulse preserves authored ending");return 0; } + CHECK(loops[0].component_sound_count==0,"all owned start/loop voices retire within their fade duration");return 0; +} diff --git a/tools/harness/tests/test_sound_lifecycle.py b/tools/harness/tests/test_sound_lifecycle.py new file mode 100644 index 00000000..a5e029dd --- /dev/null +++ b/tools/harness/tests/test_sound_lifecycle.py @@ -0,0 +1,58 @@ +"""Production fades and voice creation at multiple frame rates, including negative controls.""" +import re +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, enum_with, function, mutated, read, run +from harness.audio import manager_types + +CASES = ["fresh-loop", "fresh-impulse", "fade-end", "crossfade", "pending", "restart", "intro-loop", + "rapid", "other-owner", "other-track", "stop-cue", "fake-impulse", "repeated-stop", "four-tracks"] +NEGATIVE_CONTROLS = { + "revives-completed-fade": (("real fade = sound->fade_interpolation_end;", "real fade = 1.f;"), "fade-end"), + "keeps-replaced-voice": (("sound_index != except_sound_index &&", "FALSE &&"), "restart"), + "cancels-other-owner": (("sound->source_identifier == looping_sound_index &&", "TRUE &&"), "other-owner"), + "cancels-other-track": (("sound->loop_track_index == track_index)", "TRUE)"), "other-track"), + "silent-new-loop": (("sound->fade_interpolation_end = 1.f;", "sound->fade_interpolation_end = 0.f;"), "fresh-loop"), + "silent-new-impulse": (("sound->fade_interpolation_end = 1.f;", "sound->fade_interpolation_end = 0.f;"), "fresh-impulse"), +} + + +def generated(fault=None, case=None): + manager = read("source/sound/sound_manager.c") + names = ["update_potentially_audible_looping_sound", "sound_new_impulse", "sound_calculate_fade", + "sound_start_fade", "sound_fade_looping_track_components", "sound_refresh_looping"] + code = [] + for name in names: + text = function(manager, name) + if fault and ((case == "fresh-loop" and name == names[0]) or + (case == "fresh-impulse" and name == names[1]) or + (case not in ("fresh-loop", "fresh-impulse") and name in names[2:])): + if fault[0] in text: + text = mutated(text, *fault) + code.append(text) + enums = manager_types() + constants_section = manager[manager.index("/* ---------- constants */"):] + for member in ["_sound_promotion_dont", "_sound_compression_xbox_adpcm", "_sound_encoding_mono"]: + enums += enum_with(constants_section, member) + "\n" + enums += enum_with(read("source/sound/sound_classes.h"), "_sound_class_unit_dialog") + "\n" + for name in ["sound_fade_exponent", "sound_inaudible_fade_out_time", "speed_of_sound_threshold"]: + enums += re.search(r"^.*\b" + name + r"\s*=.*;", manager, re.M).group(0) + "\n" + return (("types.inc", enums), ("under_test.inc", "\n".join(code))) + + +@pytest.mark.parametrize("hz", [30, 60, 120, 240]) +@pytest.mark.parametrize("case", CASES) +def test_case(case, hz): + status, output = run(build("sound_lifecycle", generated()), f"{case}:{hz}") + assert status == 0, output + + +@pytest.mark.parametrize("control", NEGATIVE_CONTROLS) +def test_negative_control(control): + fault, case = NEGATIVE_CONTROLS[control] + status, output = run(build("sound_lifecycle", generated(fault, case)), f"{case}:240") + assert status == CHECK_FAILED, f"{control} escaped {case}: {output}" From fc616b8c232dd1d66f228c089fc5cf493f09986d Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Wed, 7 Oct 2026 21:45:46 -0500 Subject: [PATCH 042/187] Spinner descriptions' text indices above the Custom Edition maps' Text boxes take a string index from 0x5000 as a spinner's extra item's description (kills_to_win_extra_descriptions). Custom Edition multiplayer maps' display indices now run from 0x4000 to 0x6000 (8192 maps), so a list of more than 4096 showed the next maps' names as kills-to-win descriptions. The descriptions move to 0x7000, which custom_edition_maps.c already keeps every map's display index below. --- source/interface/ui_widget.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index ef959fa3..ed64d383 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -1443,8 +1443,9 @@ static wchar_t *spinner_string_list_get_string( /* ---------- globals */ /* port: text boxes' string list indices from here are the descriptions of -spinners' extra items (kills_to_win_extra_descriptions) */ -#define SPINNER_EXTRA_DESCRIPTION_BASE 0x5000 +spinners' extra items (kills_to_win_extra_descriptions), above the Custom +Edition maps' display indices (custom_edition_maps.c keeps them below 0x7000) */ +#define SPINNER_EXTRA_DESCRIPTION_BASE 0x7000 /* ... and the pixels a spinner with extra items is wider (for three digits) */ #define SPINNER_EXTRA_WIDTH 12 From c7c4feab414aef1e316108bd4ee33a81bca37359 Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Wed, 7 Oct 2026 23:49:07 -0400 Subject: [PATCH 043/187] Use native transparent part links for enclosed energy at map load --- source/models/models.c | 85 +++++++- source/models/models.h | 3 + .../rasterizer_transparent_enclosure.h | 159 ++++++++++++++ .../rasterizer_transparent_geometry.h | 9 + .../rasterizer_xbox_transparent_geometry.c | 48 +++++ source/render/render.c | 4 + tools/harness/__init__.py | 15 +- tools/harness/render_types.py | 20 ++ .../tests/test_transparent_enclosure.py | 57 +++++ tools/harness/tests/transparent_enclosure.c | 196 ++++++++++++++++++ 10 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 source/rasterizer/rasterizer_transparent_enclosure.h create mode 100644 tools/harness/render_types.py create mode 100644 tools/harness/tests/test_transparent_enclosure.py create mode 100644 tools/harness/tests/transparent_enclosure.c diff --git a/source/models/models.c b/source/models/models.c index f087bf87..cb06148d 100644 --- a/source/models/models.c +++ b/source/models/models.c @@ -83,6 +83,9 @@ symbols in this file: #include "shaders/shaders.h" #include "rasterizer/rasterizer_console_vars.h" #include "rasterizer/rasterizer_model_types.h" +/* port: model tags and static enclosure recognition at map initialization. */ +#include "cache/cache_files.h" +#include "rasterizer/rasterizer_transparent_geometry.h" /* ---------- constants */ @@ -385,7 +388,8 @@ static void render_model_parts( if (sort_filth_countnext_part_index>0 || part->previous_part_index>0)) + /* port: part zero can be the head of a link to a nonzero part. */ + (part->next_part_index>0 || part->previous_part_index>=0)) { sort_filth[sort_filth_count].part_index = part_index; sort_filth[sort_filth_count].next_part_index = part->next_part_index; @@ -458,6 +462,85 @@ static void render_model_parts( /* ---------- public code */ +/* port: use native energy-before-glass links, without changing glass passes. + Keep authored links and ambiguous/skinned models as they are. */ +static void model_geometry_fix_transparent_part_links( + struct model const *model, struct model_geometry *geometry) +{ + short i, glass = NONE, energy = NONE, transparent_count = 0; + struct model_geometry_part *parts = geometry->parts.address; + + if (model->nodes.count != 1 || geometry->parts.count < 2 || + geometry->parts.count > MAXIMUM_PARTS_PER_MODEL_GEOMETRY || !parts) + { + return; + } + for (i = 0; i < geometry->parts.count; ++i) + { + struct shader *shader; + struct model_shader_reference const *reference; + if (parts[i].flags || parts[i].previous_part_index != NONE || parts[i].next_part_index != NONE || + !VALID_INDEX(parts[i].shader_index, model->shaders.count)) + { + return; + } + reference = TAG_BLOCK_GET_ELEMENT(&model->shaders, parts[i].shader_index, struct model_shader_reference); + shader = shader_definition_get(reference->shader.index); + if (shader_type_is_transparent(shader->base.type)) + { + ++transparent_count; + if (shader->base.type == _shader_type_transparent_glass) + { + glass = i; + } + else if (shader->base.type == _shader_type_transparent_generic) + { + energy = i; + } + } + } + if (transparent_count == 2 && glass != NONE && energy != NONE && + rasterizer_transparent_geometry_is_enclosure( + shader_definition_get(TAG_BLOCK_GET_ELEMENT(&model->shaders, parts[glass].shader_index, struct model_shader_reference)->shader.index), + &parts[glass].vertex_buffer, &parts[glass].triangle_buffer, + shader_definition_get(TAG_BLOCK_GET_ELEMENT(&model->shaders, parts[energy].shader_index, struct model_shader_reference)->shader.index), + &parts[energy].vertex_buffer)) + { + /* A native link cannot target part zero. These parts have no authored + links; swap their complete descriptors only when glass is part zero. */ + if (glass == 0) + { + struct model_geometry_part swap = parts[glass]; + parts[glass] = parts[energy]; + parts[energy] = swap; + glass = energy; + energy = 0; + } + parts[energy].next_part_index = (char)glass; + parts[glass].previous_part_index = (char)energy; + } +} + +/* port: the meshes do not change between frames; their links need no cache + invalidation beyond loading fresh tags for the next map. */ +void models_fix_transparent_part_links(void) +{ + struct tag_iterator iterator; + long index; + + tag_iterator_new(&iterator, MODELS_GROUP_TAG); + while ((index = tag_iterator_next(&iterator)) != NONE) + { + struct model *model = model_definition_get(index); + long geometry; + for (geometry = 0; geometry < model->geometries.count; ++geometry) + { + model_geometry_fix_transparent_part_links(model, + TAG_BLOCK_GET_ELEMENT(&model->geometries, geometry, struct model_geometry)); + } + } +} + void model_interpolate_node_orientations( struct model const *model, struct real_orientation *original_node_orientations, diff --git a/source/models/models.h b/source/models/models.h index 44c49396..bf2148b7 100644 --- a/source/models/models.h +++ b/source/models/models.h @@ -27,6 +27,9 @@ union real_vector3d; /* ---------- prototypes/MODELS.C */ +/* port: apply native transparent part links once per loaded model. */ +void models_fix_transparent_part_links(void); + void model_interpolate_node_orientations( struct model const *model, struct real_orientation *original_node_orientations, diff --git a/source/rasterizer/rasterizer_transparent_enclosure.h b/source/rasterizer/rasterizer_transparent_enclosure.h new file mode 100644 index 00000000..93ec0c1c --- /dev/null +++ b/source/rasterizer/rasterizer_transparent_enclosure.h @@ -0,0 +1,159 @@ +/* port: recognize a closed convex shell once when model tags load. Positions + are the first three floats in both supported model vertex formats. */ +#ifndef __RASTERIZER_TRANSPARENT_ENCLOSURE_H +#define __RASTERIZER_TRANSPARENT_ENCLOSURE_H + +static boolean rasterizer_transparent_encloses( + byte const *outer, long outer_count, long outer_stride, + word const *indices, long triangle_count, boolean strip, + byte const *inner, long inner_count, long inner_stride) +{ + real points[64][3], center[3] = {0}, extent = 0, epsilon; + short welded[64], triangles[128][3]; + long i, j, k, unique_count = 0, face_count = 0; + + if (!outer || !inner || !indices || outer_count < 4 || outer_count > 64 || + inner_count <= 0 || inner_count > 1024 || triangle_count < 4 || triangle_count > 128 || + outer_stride < 3 * sizeof(real) || inner_stride < 3 * sizeof(real)) + { + return FALSE; + } + for (i = 0; i < outer_count; ++i) + { + real const *p = (real const *)(outer + i * outer_stride); + for (k = 0; k < 3; ++k) + { + if (!(p[k] > -1000000.0f && p[k] < 1000000.0f)) + { + return FALSE; + } + center[k] += p[k] / outer_count; + } + } + for (i = 0; i < outer_count; ++i) + { + for (k = 0; k < 3; ++k) + { + extent = MAX(extent, fabs(((real const *)(outer + i * outer_stride))[k] - center[k])); + } + } + if (!(extent > 0.000001f)) + { + return FALSE; + } + epsilon = extent * 0.00001f; + for (i = 0; i < outer_count; ++i) + { + real const *p = (real const *)(outer + i * outer_stride); + for (j = 0; j < unique_count; ++j) + { + if (fabs(p[0] - points[j][0]) < epsilon && + fabs(p[1] - points[j][1]) < epsilon && fabs(p[2] - points[j][2]) < epsilon) + { + break; + } + } + welded[i] = (short)j; + if (j == unique_count) + { + memcpy(points[j], p, sizeof(points[j])); + ++unique_count; + } + } + for (i = 0; i < triangle_count; ++i) + { + long start = strip ? i : 3 * i; + short ids[3]; + real ab[3], ac[3], normal[3], length, distance = 0; + for (k = 0; k < 3; ++k) + { + word index = indices[start + k]; + if (index >= outer_count) + { + return FALSE; + } + ids[k] = welded[index]; + } + if (strip && (i & 1)) + { + short swap = ids[0]; + ids[0] = ids[1]; + ids[1] = swap; + } + if (ids[0] == ids[1] || ids[1] == ids[2] || ids[2] == ids[0]) + { + continue; + } + for (k = 0; k < 3; ++k) + { + ab[k] = points[ids[1]][k] - points[ids[0]][k]; + ac[k] = points[ids[2]][k] - points[ids[0]][k]; + } + normal[0] = ab[1] * ac[2] - ab[2] * ac[1]; + normal[1] = ab[2] * ac[0] - ab[0] * ac[2]; + normal[2] = ab[0] * ac[1] - ab[1] * ac[0]; + length = square_root(normal[0]*normal[0] + normal[1]*normal[1] + normal[2]*normal[2]); + if (!(length > epsilon * epsilon)) + { + return FALSE; + } + for (k = 0; k < 3; ++k) + { + normal[k] /= length; + distance += normal[k] * (center[k] - points[ids[0]][k]); + } + /* Halo's model winding points inward. Require convexity and strict + containment, so intersecting or reversed meshes keep their order. */ + if (!(distance > epsilon)) + { + return FALSE; + } + for (j = 0; j < unique_count + inner_count; ++j) + { + real const *p = j < unique_count ? points[j] : (real const *)(inner + (j - unique_count) * inner_stride); + distance = 0; + for (k = 0; k < 3; ++k) + { + if (!(p[k] > -1000000.0f && p[k] < 1000000.0f)) + { + return FALSE; + } + distance += normal[k] * (p[k] - points[ids[0]][k]); + } + if (!(distance >= (j < unique_count ? -epsilon : epsilon))) + { + return FALSE; + } + } + memcpy(triangles[face_count++], ids, sizeof(ids)); + } + if (face_count < 4) + { + return FALSE; + } + /* Every welded edge needs exactly one reverse edge. */ + for (i = 0; i < face_count; ++i) + { + for (k = 0; k < 3; ++k) + { + short a = triangles[i][k], b = triangles[i][(k + 1) % 3]; + long reverse = 0, forward = 0, edge; + for (j = 0; j < face_count; ++j) + { + for (edge = 0; edge < 3; ++edge) + { + short c = triangles[j][edge], d = triangles[j][(edge + 1) % 3]; + reverse += c == b && d == a; + forward += c == a && d == b; + } + } + if (reverse != 1 || forward != 1) + { + return FALSE; + } + } + } + return TRUE; +} + +#endif diff --git a/source/rasterizer/rasterizer_transparent_geometry.h b/source/rasterizer/rasterizer_transparent_geometry.h index ae18bf31..74de91ea 100644 --- a/source/rasterizer/rasterizer_transparent_geometry.h +++ b/source/rasterizer/rasterizer_transparent_geometry.h @@ -12,6 +12,15 @@ Narrow cross-translation-unit interface owned by RASTERIZER_TRANSPARENT_GEOMETRY struct transparent_geometry_group; +/* port: static enclosure recognition, used when model tags load. */ +struct shader; +struct vertex_buffer; +struct triangle_buffer; +boolean rasterizer_transparent_geometry_is_enclosure( + struct shader const *glass, struct vertex_buffer const *outer, + struct triangle_buffer const *triangles, + struct shader const *energy, struct vertex_buffer const *inner); + void rasterizer_transparent_geometry_groups_begin( void); void rasterizer_transparent_geometry_groups_end( diff --git a/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c b/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c index 64710cc2..03dd128f 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c +++ b/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c @@ -862,6 +862,54 @@ void rasterizer_transparent_geometry_dispose_aux_buffer( /* ---------- private code */ +/* port: mesh and material checks run only when model tags load. */ +#include "rasterizer/rasterizer_transparent_enclosure.h" + +boolean rasterizer_transparent_geometry_is_enclosure( + struct shader const *glass, struct vertex_buffer const *outer, + struct triangle_buffer const *triangles, + struct shader const *energy, struct vertex_buffer const *inner) +{ + struct shader_transparent_glass_definition const *shell = (void const *)glass; + struct shader_transparent_generic const *core; + byte *outer_data = NULL, *inner_data = NULL, *index_data = NULL; + boolean result; + + if (!glass || !energy || glass->base.type != _shader_type_transparent_glass || + energy->base.type != _shader_type_transparent_generic) + { + return FALSE; + } + core = &((struct shader_transparent_generic_definition const *)energy)->generic; + if (!TEST_FLAG(shell->flags, _shader_transparent_glass_flag_two_sided_bit) || + TEST_FLAG(shell->flags, _shader_transparent_glass_flag_decal_bit) || + shell->reflection_type == _shader_transparent_glass_reflection_type_dynamic_mirror || + core->flags != FLAG(_shader_transparent_flag_two_sided_bit) || + TEST_FLAG(energy->base.radiosity.flags, _shader_radiosity_FILTHY_transparent_lit_bit) || + core->framebuffer_blend_function != _framebuffer_blend_function_add || + core->framebuffer_fade_mode != _framebuffer_fade_mode_none || + core->extra_layers.count || core->lens_flare.index != NONE || + !outer || !inner || !triangles || !outer->hardware_format || !inner->hardware_format || + !triangles->hardware_format || outer->offset || inner->offset || + (outer->type != _rasterizer_vertex_type_model_compressed && outer->type != _rasterizer_vertex_type_model_uncompressed) || + (inner->type != _rasterizer_vertex_type_model_compressed && inner->type != _rasterizer_vertex_type_model_uncompressed) || + (triangles->type != _triangle_buffer_type_triangles && triangles->type != _triangle_buffer_type_precompiled_strip)) + { + return FALSE; + } + IDirect3DVertexBuffer8_Lock(outer->hardware_format, 0, 0, &outer_data, D3DLOCK_READONLY); + IDirect3DVertexBuffer8_Lock(inner->hardware_format, 0, 0, &inner_data, D3DLOCK_READONLY); + IDirect3DIndexBuffer8_Lock(triangles->hardware_format, 0, 0, &index_data, D3DLOCK_READONLY); + result = rasterizer_transparent_encloses(outer_data, outer->count, + rasterizer_geometry_get_vertex_size(outer->type), (word const *)index_data, + triangles->count, triangles->type == _triangle_buffer_type_precompiled_strip, + inner_data, inner->count, rasterizer_geometry_get_vertex_size(inner->type)); + IDirect3DIndexBuffer8_Unlock(triangles->hardware_format); + IDirect3DVertexBuffer8_Unlock(inner->hardware_format); + IDirect3DVertexBuffer8_Unlock(outer->hardware_format); + return result; +} + /* port: an extra layer's draw, no deeper than the layers may nest (a map's layers could name each other in a loop); said once */ static void transparent_geometry_layer_draw( diff --git a/source/render/render.c b/source/render/render.c index d9109fc5..dc251459 100644 --- a/source/render/render.c +++ b/source/render/render.c @@ -87,6 +87,8 @@ symbols in this file: #include "effects/weather_particle_systems.h" #include "main/main.h" #include "structures/structures.h" +/* port: static transparent part links for the newly loaded model tags. */ +#include "models/models.h" /* ---------- constants */ @@ -170,6 +172,8 @@ void render_initialize( void render_initialize_for_new_map( void) { + /* port: preserve retail's energy-then-two-sided-glass compositing. */ + models_fix_transparent_part_links(); render_objects_initialize_for_new_map(); } diff --git a/tools/harness/__init__.py b/tools/harness/__init__.py index 01b8bda3..e351e53f 100644 --- a/tools/harness/__init__.py +++ b/tools/harness/__init__.py @@ -41,6 +41,19 @@ def enum_with(source, member): return source[start:source.index("};", at) + 2] +def structure(source, name): + """An actual named structure's definition, including nested unions/structures.""" + match = re.search(r"^struct\s+" + re.escape(name) + r"\s*\{", source, re.M) + if not match: + raise LookupError(f"structure not found in the sources: {name}") + end = source.index("{", match.start()) + 1 + depth = 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():source.index(";", end) + 1] + + def constant(source, name): """An enum constant's or #define's integer value, following a name it is set to (as the port's capacities are).""" match = re.search(r"\b" + re.escape(name) + r"\s*=\s*(\w+)", source) or \ @@ -76,7 +89,7 @@ def build(test, generated): executable = work / test command = [os.environ.get("CC", "clang"), "-m32", "-std=gnu99", "-O2", "-Wall", "-Werror", "-Wno-unused-function", "-Wno-unused-variable", "-I", str(HARNESS / "include"), "-I", str(work), - str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable)] + str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable), "-lm"] result = subprocess.run(command, capture_output=True, text=True) if result.returncode: raise RuntimeError(f"{test}.c does not compile:\n{result.stderr}") diff --git a/tools/harness/render_types.py b/tools/harness/render_types.py new file mode 100644 index 00000000..18e8f70e --- /dev/null +++ b/tools/harness/render_types.py @@ -0,0 +1,20 @@ +"""Production layouts shared by the CPU-only transparent rendering tests.""" +from harness import enum_with, read, structure + + +def transparent_types(): + declarations = [] + for path, names in [ + ("source/tag_files/tag_groups.h", ["tag_block", "tag_reference"]), + ("source/shaders/shader_definitions.h", ["shader_radiosity_properties", "shader_physics_properties", "shader_base", "shader"]), + ("source/rasterizer/rasterizer_geometry.h", ["vertex_buffer", "triangle_buffer"]), + ("source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c", ["shader_transparent_generic", "shader_transparent_generic_definition", "shader_transparent_glass_definition", "transparent_geometry_group"]), + ]: + text = read(path) + declarations += [structure(text, name) for name in names] + text = read("source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c") + for member in ["_shader_type_screen", "_shader_transparent_flag_alpha_tested_bit", "_shader_transparent_glass_flag_alpha_tested_bit", "_shader_transparent_glass_reflection_type_bumped_cube_map", "_shader_radiosity_FILTHY_transparent_lit_bit", "_framebuffer_fade_mode_none", "_framebuffer_blend_function_alpha_blend", "_rasterizer_geometry_no_sort_bit"]: + declarations.append(enum_with(text, member)) + text = read("source/rasterizer/rasterizer_geometry.h") + declarations += [enum_with(text, "_rasterizer_vertex_type_environment_uncompressed"), enum_with(text, "_triangle_buffer_type_triangles")] + return "\n".join(declarations) diff --git a/tools/harness/tests/test_transparent_enclosure.py b/tools/harness/tests/test_transparent_enclosure.py new file mode 100644 index 00000000..e630e9f6 --- /dev/null +++ b/tools/harness/tests/test_transparent_enclosure.py @@ -0,0 +1,57 @@ +"""Static enclosure recognition and actual native model-part linking, without GL.""" +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, constant, enum_with, function, mutated, read, run, structure +from harness.render_types import transparent_types + +CASES = ["closed", "open", "outside", "reversed", "invalid", "strips", "uncompressed", + "materials", "authored-links", "part-zero", "glass-zero", "opaque-part", "skinned", + "additional-transparent", "frame-cost", "map-reload", "render-links"] +CONTROLS = { + "revived-part-zero-gate": (("part->previous_part_index>=0", "part->previous_part_index>0"), "render-links"), + "lost-enclosure-link": (("parts[energy].next_part_index = (char)glass;", "parts[energy].next_part_index = NONE;"), "part-zero"), + "wrong-target-zero": (("if (glass == 0)", "if (FALSE)"), "glass-zero"), + "overwrites-authored-links": (("parts[i].previous_part_index != NONE || parts[i].next_part_index != NONE ||", "FALSE ||"), "authored-links"), + "accepts-open-mesh": (("reverse != 1 || forward != 1", "reverse < 0 || forward < 0"), "open"), + "accepts-other-blend": (("core->framebuffer_blend_function != _framebuffer_blend_function_add", "FALSE"), "materials"), +} + + +def generated(control=None): + models = read("source/models/models.c") + raster = read("source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c") + types = f"#define TAG_STRING_LENGTH {constant(read('source/tag_files/tag_files.h'), 'TAG_STRING_LENGTH')}\n#define UNSIGNED_SHORT_MAX {constant(read('source/cseries/cseries.h'), 'UNSIGNED_SHORT_MAX')}\n" + transparent_types() + for path, names in [ + ("source/models/model_definitions.h", ["model", "model_region", "model_region_permutation"]), + ("source/models/models.c", ["model_shader_reference", "model_geometry", "model_geometry_part", "shader_model_definition", "render_sort_filth"]), + ("source/render/render.h", ["render_skinning"]), + ("source/cache/cache_files.h", ["tag_iterator"]), + ]: + types += "\n" + "\n".join(structure(read(path), name) for name in names) + for member in ["_render_model_immediate_bit", "_render_model_pass_solid", "_model_geometry_part_stripped_bit", "_shader_model_detail_after_reflection_bit"]: + types += "\n" + enum_with(models, member) + types += "\n" + enum_with(read("source/models/model_definitions.h"), "MODELS_GROUP_TAG") + code = read("source/rasterizer/rasterizer_transparent_enclosure.h") + code += "\n" + function(raster, "rasterizer_transparent_geometry_is_enclosure") + for name in ["model_geometry_fix_transparent_part_links", "models_fix_transparent_part_links", "render_model_parts"]: + code += "\n" + function(models, name) + if control: + fault, _ = CONTROLS[control] + code = mutated(code, *fault) + return (("types.inc", types), ("under_test.inc", code)) + + +@pytest.mark.parametrize("case", CASES) +def test_case(case): + status, output = run(build("transparent_enclosure", generated()), case) + assert status == 0, output + + +@pytest.mark.parametrize("control", CONTROLS) +def test_negative_control(control): + status, output = run(build("transparent_enclosure", generated(control)), CONTROLS[control][1]) + assert status == CHECK_FAILED, f"{control} escaped its check: {output}" diff --git a/tools/harness/tests/transparent_enclosure.c b/tools/harness/tests/transparent_enclosure.c new file mode 100644 index 00000000..11382635 --- /dev/null +++ b/tools/harness/tests/transparent_enclosure.c @@ -0,0 +1,196 @@ +#include "harness.h" +#include +#include +typedef unsigned short word; +typedef struct { real x, y; } real_vector2d; +typedef struct { real red, green, blue; } real_rgb_color; +typedef struct { real alpha, red, green, blue; } real_argb_color; +typedef struct { real_vector3d n; real d; } real_plane3d; +typedef void real_matrix4x3; +#define MAX(a,b) ((a) > (b) ? (a) : (b)) +#define MIN(a,b) ((a) < (b) ? (a) : (b)) +#define VALID_INDEX(i,n) ((i) >= 0 && (i) < (n)) +#define square_root sqrt +#include "types.inc" + +static struct model model; +static struct model_geometry geometry; +static struct model_geometry_part parts[4]; +static struct model_shader_reference references[4]; +static struct shader_transparent_generic_definition energy_material; +static struct shader_transparent_glass_definition glass_material; +static struct shader opaque_material; +static struct model_region region; +static struct model_region_permutation permutation; +static int locks, submissions, iterator_position; +static short previous[32], next[32]; +static real shell[5][8] = {{-1,-1,0},{1,-1,0},{1,1,0},{-1,1,0},{0,0,2}}; +static word indices[64] = {0,1,2,0,2,3,0,4,1,1,4,2,2,4,3,3,4,0}; +static real sphere[6][8] = {{-.22f,0,.7f},{.22f,0,.7f},{0,-.22f,.7f},{0,.22f,.7f},{0,0,.48f},{0,0,.92f}}; + +#define TAG_BLOCK_GET_ELEMENT(b,i,t) ((t *)(b)->address + (i)) +#define shader_definition_get(i) ((i)==0 ? (struct shader *)&energy_material : (i)==1 ? (struct shader *)&glass_material : &opaque_material) +#define model_definition_get(i) test_model_get() +#define shader_type_is_transparent(t) ((t)>=_shader_type_transparent_generic) +#define shader_type_is_valid_for_model(t) ((t)>=_shader_type_model) +#define shader_get_and_verify_type(s,t) (s) +#define model_data_error(m,e) ((void)0) +#define render_model_no_geometry FALSE +#define matrix4x3_transform_point(m,p,o) (*(o)=*(p)) +#define rasterizer_model_draw(...) ((void)0) +#define rasterizer_environment_shadow_model_draw(...) ((void)0) +#define rasterizer_debug_model_vertices(...) ((void)0) +#define D3DLOCK_READONLY 128 + +static void tag_iterator_new(struct tag_iterator *iterator, long tag) +{ + iterator_position = 0; +} +static struct model *test_model_get(void) +{ + return &model; +} +static long tag_iterator_next(struct tag_iterator *iterator) +{ + return iterator_position++ == 0 ? 0 : NONE; +} +static long rasterizer_geometry_get_vertex_size(short type) +{ + return type == _rasterizer_vertex_type_model_compressed ? 32 : 68; +} +static void IDirect3DVertexBuffer8_Lock(void *buffer, long offset, long size, byte **out, long flags) +{ + locks++; + *out = buffer; +} +#define IDirect3DIndexBuffer8_Lock IDirect3DVertexBuffer8_Lock +#define IDirect3DVertexBuffer8_Unlock(b) ((void)0) +#define IDirect3DIndexBuffer8_Unlock(b) ((void)0) +static void rasterizer_model_transparent_geometry_submit(struct shader *shader, short permutation_index, + struct triangle_buffer const *triangles, long first_triangle, long triangle_count, + struct vertex_buffer const *vertices, long offset, real_point3d const *centroid, + struct render_sort_filth *sort) +{ + int i = submissions++; + previous[i] = next[i] = NONE; + if (sort) + { + sort->group_index = i; + sort->previous_group_presorted_index_reference = previous + i; + sort->next_group_presorted_index_reference = next + i; + } +} +#include "under_test.inc" + +static void initialize(void) +{ + int i; + memset(&model, 0, sizeof(model)); + memset(parts, 0, sizeof(parts)); + memset(&energy_material, 0, sizeof(energy_material)); + memset(&glass_material, 0, sizeof(glass_material)); + locks = 0; + energy_material.shader.type = _shader_type_transparent_generic; + energy_material.generic.flags = FLAG(_shader_transparent_flag_two_sided_bit); + energy_material.generic.framebuffer_blend_function = _framebuffer_blend_function_add; + energy_material.generic.lens_flare.index = NONE; + glass_material.shader.base.type = _shader_type_transparent_glass; + glass_material.flags = FLAG(_shader_transparent_glass_flag_two_sided_bit); + opaque_material.base.type = _shader_type_model; + model.nodes.count = 1; + model.geometries.count = 1; model.geometries.address = &geometry; + model.shaders.count = 4; model.shaders.address = references; + geometry.parts.count = 2; geometry.parts.address = parts; + for (i=0; i<4; i++) + { + parts[i].shader_index = i; + references[i].shader.index = i; + parts[i].previous_part_index = parts[i].next_part_index = NONE; + parts[i].vertex_buffer.type = _rasterizer_vertex_type_model_compressed; + } + parts[0].vertex_buffer.count = 6; parts[0].vertex_buffer.hardware_format = sphere; + parts[1].vertex_buffer.count = 5; parts[1].vertex_buffer.hardware_format = shell; + parts[1].triangle_buffer.type = _triangle_buffer_type_triangles; + parts[1].triangle_buffer.count = 6; parts[1].triangle_buffer.hardware_format = indices; + region.permutations.count = 1; region.permutations.address = &permutation; + model.regions.count = 1; model.regions.address = ®ion; + memset(&permutation, 0, sizeof(permutation)); +} +static boolean recognizes(void) +{ + return rasterizer_transparent_geometry_is_enclosure((struct shader *)&glass_material, + &parts[1].vertex_buffer, &parts[1].triangle_buffer, + (struct shader *)&energy_material, &parts[0].vertex_buffer); +} +static void draw_frame(void) +{ + struct render_skinning skinning = {NULL, 1}; + char permutation_indices[] = {0}; + submissions = 0; + render_model_parts(&model, permutation_indices, 1, &skinning, 1, 0, 0, 0); + CHECK(submissions == 2, "two transparent parts"); + CHECK(next[0] == 1 && previous[1] == 0, "native links must include part zero"); + CHECK(glass_material.flags == FLAG(_shader_transparent_glass_flag_two_sided_bit), "retail two-sided glass retained"); +} + +int main(int argc, char **argv) +{ + const char *case_name = argc > 1 ? argv[1] : "closed"; + int i; + initialize(); + CASE("closed") { CHECK(recognizes(), "closed shell"); } + CASE("open") { parts[1].triangle_buffer.count = 5; CHECK(!recognizes(), "missing face"); } + CASE("outside") { sphere[0][0] = 5; CHECK(!recognizes(), "outside core"); } + CASE("reversed") { for(i=0;i<18;i+=3) { word swap=indices[i];indices[i]=indices[i+1];indices[i+1]=swap; } CHECK(!recognizes(), "reverse winding"); } + CASE("invalid") { + indices[0]=64; CHECK(!recognizes(), "bad index"); indices[0]=0; + shell[0][0]=NAN; CHECK(!recognizes(), "invalid outer point"); shell[0][0]=-1; + sphere[0][0]=NAN; CHECK(!recognizes(), "invalid inner point"); + CHECK(!rasterizer_transparent_encloses((byte*)shell,5,0,indices,6,FALSE,(byte*)sphere,6,32), "bad stride"); + } + CASE("strips") { + word triangles[18];long cursor=0; + memcpy(triangles,indices,sizeof(triangles)); + for(i=0;i<6;i++) { + word a=triangles[3*i],b=triangles[3*i+1]; + if(i) { + if((cursor+2)&1){word swap=a;a=b;b=swap;} + indices[cursor]=indices[cursor-1];cursor++; + indices[cursor++]=a; + } + indices[cursor++]=a;indices[cursor++]=b;indices[cursor++]=triangles[3*i+2]; + } + parts[1].triangle_buffer.type=_triangle_buffer_type_precompiled_strip; + parts[1].triangle_buffer.count=cursor-2;CHECK(recognizes(), "triangle strip with degenerates"); + } + CASE("uncompressed") { + real outer[5][17],inner[6][17]; + for(i=0;i<5;i++)memcpy(outer[i],shell[i],12); + for(i=0;i<6;i++)memcpy(inner[i],sphere[i],12); + parts[1].vertex_buffer.hardware_format=outer;parts[0].vertex_buffer.hardware_format=inner; + parts[1].vertex_buffer.type=parts[0].vertex_buffer.type=_rasterizer_vertex_type_model_uncompressed; + CHECK(recognizes(), "uncompressed vertices"); + } + CASE("materials") { + energy_material.generic.framebuffer_blend_function=_framebuffer_blend_function_alpha_blend; + CHECK(!recognizes(), "other blend untouched");initialize(); + glass_material.flags=0;CHECK(!recognizes(), "one-sided glass untouched");initialize(); + glass_material.reflection_type=_shader_transparent_glass_reflection_type_dynamic_mirror; + CHECK(!recognizes(), "dynamic mirror untouched");initialize(); + energy_material.generic.extra_layers.count=1;CHECK(!recognizes(), "layers untouched"); + } + CASE("part-zero") { models_fix_transparent_part_links(); CHECK(parts[0].next_part_index==1 && parts[1].previous_part_index==0, "link from part zero"); } + CASE("glass-zero") { + struct model_geometry_part swap=parts[0];parts[0]=parts[1];parts[1]=swap; + models_fix_transparent_part_links(); + CHECK(parts[0].shader_index==0 && parts[1].shader_index==1 && parts[0].next_part_index==1, "no native link to zero"); + } + CASE("opaque-part") {geometry.parts.count=3;models_fix_transparent_part_links();CHECK(parts[0].next_part_index==1 && parts[2].previous_part_index==NONE, "opaque base retained");} + CASE("skinned") {model.nodes.count=2;models_fix_transparent_part_links();CHECK(!locks && parts[0].next_part_index==NONE, "skinned model retained");} + CASE("authored-links") {parts[0].next_part_index=2;models_fix_transparent_part_links();CHECK(!locks && parts[0].next_part_index==2, "authored links retained");} + CASE("additional-transparent") {geometry.parts.count=3;opaque_material.base.type=_shader_type_transparent_glass;models_fix_transparent_part_links();CHECK(!locks && parts[0].next_part_index==NONE, "ambiguous model retained");} + CASE("frame-cost") {models_fix_transparent_part_links();CHECK(locks==3,"one recognizer call");for(i=0;i<1000;i++)draw_frame();CHECK(locks==3,"no per-frame recognition");models_fix_transparent_part_links();CHECK(locks==3,"already linked geometry retained");} + CASE("map-reload") {models_fix_transparent_part_links();initialize();models_fix_transparent_part_links();CHECK(locks==3 && parts[0].next_part_index==1,"fresh map links");} + CASE("render-links") {models_fix_transparent_part_links();draw_frame();} + return 0; +} From b3a6490f394e926f071fb334695f3d3c27e3c92f Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Thu, 8 Oct 2026 00:03:41 -0400 Subject: [PATCH 044/187] Order transparent models next to planar BSP glass --- .../rasterizer_transparent_geometry.c | 5 + .../rasterizer_transparent_geometry.h | 3 + .../rasterizer_xbox_transparent_geometry.c | 369 +++++++++++++++ tools/harness/__init__.py | 15 +- tools/harness/render_types.py | 20 + .../tests/test_transparent_plane_order.py | 81 ++++ tools/harness/tests/transparent_plane_order.c | 432 ++++++++++++++++++ 7 files changed, 924 insertions(+), 1 deletion(-) create mode 100644 tools/harness/render_types.py create mode 100644 tools/harness/tests/test_transparent_plane_order.py create mode 100644 tools/harness/tests/transparent_plane_order.c diff --git a/source/rasterizer/rasterizer_transparent_geometry.c b/source/rasterizer/rasterizer_transparent_geometry.c index b1b251ee..858fd4d1 100644 --- a/source/rasterizer/rasterizer_transparent_geometry.c +++ b/source/rasterizer/rasterizer_transparent_geometry.c @@ -518,6 +518,11 @@ static void rasterizer_sort_external( sizeof(*transparent_geometry_group_sorted_indices), group_sorted_indices_cmpfn); + /* port: move model packets next to their glass surface, leaving other + packets on their original side of every surface. */ + rasterizer_transparent_geometry_order_models( + transparent_geometry_group_sorted_indices, transparent_geometry_group_count); + for (group_index = 0; group_indexplane.n; + real length = 0; + short k; + + if (!surface->shader || surface->shader->base.type != _shader_type_transparent_glass || + surface->object_index || surface->source_object_index || surface->node_matrix_count || + surface->effect_type || surface->active_camouflage_transparent_source_object_index || + surface->cortana_hack || surface->previous_group_presorted_index != NONE || + surface->next_group_presorted_index != NONE || + !TEST_FLAG(surface->geometry_flags, _rasterizer_geometry_no_sort_bit) || + (surface->geometry_flags & ~(FLAG(_rasterizer_geometry_no_sort_bit) | FLAG(_rasterizer_geometry_no_fog_bit) | + FLAG(_rasterizer_geometry_atmospheric_fog_but_no_planar_fog_bit))) || + !(surface->plane.d > -1000000.0f && surface->plane.d < 1000000.0f)) + { + return FALSE; + } + if (TEST_FLAG(((struct shader_transparent_glass_definition const *)surface->shader)->flags, + _shader_transparent_glass_flag_decal_bit) || + ((struct shader_transparent_glass_definition const *)surface->shader)->reflection_type == + _shader_transparent_glass_reflection_type_dynamic_mirror) + { + return FALSE; + } + for (k = 0; k < 3; ++k) + { + if (!(normal[k] >= -1.001f && normal[k] <= 1.001f)) + { + return FALSE; + } + length += normal[k] * normal[k]; + } + return length > 0.998f && length < 1.002f; +} + +/* port: bound the actual skinned positions once per group, only when planar + glass is queued. Unsupported or invalid geometry retains centroid sorting. */ +static boolean transparent_model_world_bounds( + struct transparent_geometry_group const *group, real bounds[2][3]) +{ + struct vertex_buffer const *vertices = group->vertex_buffer; + real_matrix4x3 const *matrices = group->node_matrices; + byte *data = NULL; + long i, k, stride; + boolean valid = TRUE; + + if (!group->shader || !shader_type_is_transparent(group->shader->base.type) || + !shader_type_is_valid_for_model(group->shader->base.type) || + group->shader->base.type == _shader_type_transparent_water || + shader_is_water_decal(group->shader) || group->effect_type || + group->cortana_hack || group->active_camouflage_transparent_source_object_index || + (group->geometry_flags & ~(FLAG(_rasterizer_geometry_no_fog_bit) | + FLAG(_rasterizer_geometry_atmospheric_fog_but_no_planar_fog_bit) | + FLAG(_rasterizer_geometry_parts_define_local_nodes_bit))) || + !vertices || !vertices->hardware_format || vertices->offset || vertices->count <= 0 || + vertices->count > UNSIGNED_SHORT_MAX || !matrices || group->node_matrix_count <= 0 || + group->node_matrix_count > MAXIMUM_NODES_PER_MODEL || + (vertices->type != _rasterizer_vertex_type_model_compressed && + vertices->type != _rasterizer_vertex_type_model_uncompressed)) + { + return FALSE; + } + stride = rasterizer_geometry_get_vertex_size(vertices->type); + IDirect3DVertexBuffer8_Lock(vertices->hardware_format, 0, 0, &data, D3DLOCK_READONLY); + if (!data) + { + valid = FALSE; + } + for (i = 0; valid && i < vertices->count; ++i) + { + real_point3d const *point = (real_point3d const *)(data + i * stride); + real_point3d world; + real const *p = (real const *)&world; + if (group->node_matrix_count == 1) + { + matrix4x3_transform_point(matrices, point, &world); + } + else + { + short nodes[2], node; + real weights[2]; + world.x = world.y = world.z = 0; + if (vertices->type == _rasterizer_vertex_type_model_compressed) + { + struct model_vertex_compressed const *vertex = (void const *)point; + nodes[0] = vertex->nodes[0] / 3; + nodes[1] = vertex->nodes[1] / 3; + /* The draw declaration reads v6 as NORMSHORT1. Match GPU + skinning rather than the legacy byte-based decompressor. */ + weights[0] = (real)vertex->node_weight * (1.0f / 32767.0f); + weights[1] = 1.f - weights[0]; + if ((weights[0] > 0 && vertex->nodes[0] % 3) || + (weights[1] > 0 && vertex->nodes[1] % 3)) + { + valid = FALSE; + } + } + else + { + struct model_vertex_uncompressed const *vertex = (void const *)point; + nodes[0] = vertex->nodes[0]; + nodes[1] = vertex->nodes[1]; + weights[0] = vertex->node_weights[0]; + weights[1] = vertex->node_weights[1]; + if (!(fabs(weights[0] + weights[1] - 1.f) < 0.00001f)) + { + valid = FALSE; + } + } + for (node = 0; valid && node < 2; ++node) + { + real_point3d transformed; + if (!(weights[node] >= 0 && weights[node] <= 1)) + { + valid = FALSE; + } + else if (weights[node] > 0) + { + if (!VALID_INDEX(nodes[node], group->node_matrix_count)) + { + valid = FALSE; + break; + } + matrix4x3_transform_point(matrices + nodes[node], point, &transformed); + world.x += weights[node] * transformed.x; + world.y += weights[node] * transformed.y; + world.z += weights[node] * transformed.z; + } + } + } + for (k = 0; valid && k < 3; ++k) + { + if (!(p[k] > -1000000.0f && p[k] < 1000000.0f)) + { + valid = FALSE; + break; + } + bounds[0][k] = i ? MIN(bounds[0][k], p[k]) : p[k]; + bounds[1][k] = i ? MAX(bounds[1][k], p[k]) : p[k]; + } + } + IDirect3DVertexBuffer8_Unlock(vertices->hardware_format); + return valid; +} + +/* port: the whole model block must lie on one side. Touching the plane within + float error is allowed; intersections and cameras on the plane are ambiguous. */ +static short transparent_plane_model_order( + struct transparent_geometry_group const *surface, real const bounds[2][3]) +{ + real const *normal = (real const *)&surface->plane.n; + real const *camera = (real const *)&global_window_parameters.camera.position; + real low = -surface->plane.d, high = low, distance = low; + real epsilon = 0.0001f + 0.0000005f * fabs(surface->plane.d); + short side, k; + + for (k = 0; k < 3; ++k) + { + if (!(camera[k] > -1000000.0f && camera[k] < 1000000.0f)) + { + return 0; + } + low += normal[k] * bounds[normal[k] < 0 ? 1 : 0][k]; + high += normal[k] * bounds[normal[k] < 0 ? 0 : 1][k]; + distance += normal[k] * camera[k]; + } + side = low >= -epsilon && high > epsilon ? 1 : high <= epsilon && low < -epsilon ? -1 : 0; + if (!side || !(fabs(distance) > epsilon)) + { + return 0; + } + return (distance > 0) == (side > 0) ? -1 : 1; +} + +/* port: native links dispatch a whole chain from either queued member. */ +static short transparent_chain_root(short index, long count) +{ + long steps; + for (steps = 0; steps < count; ++steps) + { + struct transparent_geometry_group const *group = rasterizer_transparent_geometry_get_group_from_presorted_index(index); + short previous = group->previous_group_presorted_index; + if (previous == NONE) + { + return index; + } + if (!VALID_INDEX(previous, count) || + ((struct transparent_geometry_group *)rasterizer_transparent_geometry_get_group_from_presorted_index(previous))->next_group_presorted_index != index) + { + return NONE; + } + index = previous; + } + return NONE; +} + +/* port: preserve the order of surfaces and unrelated groups. Move a model + block immediately after its last required surface, or before its first. + Stable gaps avoid the old topological sort moving a floor across other + objects. Conflicting planes or malformed links leave the input untouched. */ +void rasterizer_transparent_geometry_order_models(short *order, long count) +{ + short roots[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS]; + short units[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS]; + short surfaces[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS]; + short original_gap[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS]; + short target_gap[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS]; + short result[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS]; + boolean seen[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS] = {0}; + boolean unit_seen[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS] = {0}; + long i, j, gap, phase, unit_count = 0, surface_count = 0, emitted = 0; + boolean changed = FALSE; + + if (count < 2 || count > RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS) + { + return; + } + /* Avoid reading any model vertices in scenes without eligible BSP glass. */ + for (i = 0; i < count; ++i) + { + if (transparent_planar_glass(rasterizer_transparent_geometry_get_group_from_presorted_index((short)i))) + { + ++surface_count; + } + } + if (!surface_count) + { + return; + } + surface_count = 0; + for (i = 0; i < count; ++i) + { + short root; + if (!VALID_INDEX(order[i], count) || seen[order[i]]) + { + return; + } + seen[order[i]] = TRUE; + root = transparent_chain_root(order[i], count); + if (root == NONE) + { + return; + } + roots[order[i]] = root; + if (!unit_seen[root]) + { + unit_seen[root] = TRUE; + units[unit_count++] = root; + original_gap[root] = target_gap[root] = (short)surface_count; + if (transparent_planar_glass(rasterizer_transparent_geometry_get_group_from_presorted_index(root))) + { + surfaces[surface_count++] = root; + original_gap[root] = target_gap[root] = NONE; + } + } + } + for (i = 0; i < unit_count; ++i) + { + short root = units[i], member = root; + long steps = 0, k, lower = 0, upper = surface_count; + real bounds[2][3], part_bounds[2][3]; + boolean valid = TRUE; + if (original_gap[root] == NONE) + { + continue; + } + do + { + struct transparent_geometry_group const *group; + if (!VALID_INDEX(member, count) || roots[member] != root || steps++ >= count) + { + return; + } + group = rasterizer_transparent_geometry_get_group_from_presorted_index(member); + if (group->object_index != ((struct transparent_geometry_group *)rasterizer_transparent_geometry_get_group_from_presorted_index(root))->object_index) + { + valid = FALSE; + } + if (valid && !transparent_model_world_bounds(group, part_bounds)) + { + valid = FALSE; + } + if (valid) + { + for (k = 0; k < 3; ++k) + { + bounds[0][k] = steps == 1 ? part_bounds[0][k] : MIN(bounds[0][k], part_bounds[0][k]); + bounds[1][k] = steps == 1 ? part_bounds[1][k] : MAX(bounds[1][k], part_bounds[1][k]); + } + } + member = group->next_group_presorted_index; + } + while (member != NONE); + if (!valid) + { + continue; + } + for (j = 0; j < surface_count; ++j) + { + short relation = transparent_plane_model_order( + rasterizer_transparent_geometry_get_group_from_presorted_index(surfaces[j]), bounds); + if (relation < 0) + { + lower = MAX(lower, j + 1); + } + else if (relation > 0) + { + upper = MIN(upper, j); + } + } + if (lower > upper) + { + return; + } + target_gap[root] = (short)PIN(original_gap[root], lower, upper); + changed |= target_gap[root] != original_gap[root]; + } + if (!changed) + { + return; + } + for (gap = 0; gap <= surface_count; ++gap) + { + /* Arrivals from earlier gaps, unchanged objects, arrivals from later + gaps: each category retains its original centroid order. */ + for (phase = -1; phase <= 1; ++phase) + { + for (i = 0; i < unit_count; ++i) + { + short root = units[i], member = root; + long direction = original_gap[root] < target_gap[root] ? -1 : + original_gap[root] > target_gap[root] ? 1 : 0; + if (target_gap[root] != gap || direction != phase) + { + continue; + } + do + { + if (emitted >= count) + { + return; + } + result[emitted++] = member; + member = ((struct transparent_geometry_group *)rasterizer_transparent_geometry_get_group_from_presorted_index(member))->next_group_presorted_index; + } + while (member != NONE); + } + } + if (gap < surface_count) + { + if (emitted >= count) + { + return; + } + result[emitted++] = surfaces[gap]; + } + } + if (emitted == count) + { + memcpy(order, result, count * sizeof(*order)); + } +} diff --git a/tools/harness/__init__.py b/tools/harness/__init__.py index 01b8bda3..e351e53f 100644 --- a/tools/harness/__init__.py +++ b/tools/harness/__init__.py @@ -41,6 +41,19 @@ def enum_with(source, member): return source[start:source.index("};", at) + 2] +def structure(source, name): + """An actual named structure's definition, including nested unions/structures.""" + match = re.search(r"^struct\s+" + re.escape(name) + r"\s*\{", source, re.M) + if not match: + raise LookupError(f"structure not found in the sources: {name}") + end = source.index("{", match.start()) + 1 + depth = 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():source.index(";", end) + 1] + + def constant(source, name): """An enum constant's or #define's integer value, following a name it is set to (as the port's capacities are).""" match = re.search(r"\b" + re.escape(name) + r"\s*=\s*(\w+)", source) or \ @@ -76,7 +89,7 @@ def build(test, generated): executable = work / test command = [os.environ.get("CC", "clang"), "-m32", "-std=gnu99", "-O2", "-Wall", "-Werror", "-Wno-unused-function", "-Wno-unused-variable", "-I", str(HARNESS / "include"), "-I", str(work), - str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable)] + str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable), "-lm"] result = subprocess.run(command, capture_output=True, text=True) if result.returncode: raise RuntimeError(f"{test}.c does not compile:\n{result.stderr}") diff --git a/tools/harness/render_types.py b/tools/harness/render_types.py new file mode 100644 index 00000000..18e8f70e --- /dev/null +++ b/tools/harness/render_types.py @@ -0,0 +1,20 @@ +"""Production layouts shared by the CPU-only transparent rendering tests.""" +from harness import enum_with, read, structure + + +def transparent_types(): + declarations = [] + for path, names in [ + ("source/tag_files/tag_groups.h", ["tag_block", "tag_reference"]), + ("source/shaders/shader_definitions.h", ["shader_radiosity_properties", "shader_physics_properties", "shader_base", "shader"]), + ("source/rasterizer/rasterizer_geometry.h", ["vertex_buffer", "triangle_buffer"]), + ("source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c", ["shader_transparent_generic", "shader_transparent_generic_definition", "shader_transparent_glass_definition", "transparent_geometry_group"]), + ]: + text = read(path) + declarations += [structure(text, name) for name in names] + text = read("source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c") + for member in ["_shader_type_screen", "_shader_transparent_flag_alpha_tested_bit", "_shader_transparent_glass_flag_alpha_tested_bit", "_shader_transparent_glass_reflection_type_bumped_cube_map", "_shader_radiosity_FILTHY_transparent_lit_bit", "_framebuffer_fade_mode_none", "_framebuffer_blend_function_alpha_blend", "_rasterizer_geometry_no_sort_bit"]: + declarations.append(enum_with(text, member)) + text = read("source/rasterizer/rasterizer_geometry.h") + declarations += [enum_with(text, "_rasterizer_vertex_type_environment_uncompressed"), enum_with(text, "_triangle_buffer_type_triangles")] + return "\n".join(declarations) diff --git a/tools/harness/tests/test_transparent_plane_order.py b/tools/harness/tests/test_transparent_plane_order.py new file mode 100644 index 00000000..e65c2c44 --- /dev/null +++ b/tools/harness/tests/test_transparent_plane_order.py @@ -0,0 +1,81 @@ +"""Actual transparent queue sorting, skinning and plane ordering, without GL.""" +import sys +import re +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, constant, function, mutated, read, run, structure +from harness.render_types import transparent_types + +CASES = ["same-side", "opposite-side", "reverse-normal", "unrelated", "multiple-models", + "multiple-planes", "conflict", "intersect", "camera-plane", "invalid-plane", + "invalid-model", "materials", "rigid-transform", "skinned", "compressed-skinning", + "linked", "linked-union", "invalid-links", "full-queue", "four-views", "no-glass", + "external-sort", "fallback-flags", "invalid-order"] +CONTROLS = { + "same-side-backwards": (("? -1 : 1;", "? 1 : -1;"), "same-side"), + "camera-ignored": (("(distance > 0) == (side > 0)", "side > 0"), "opposite-side"), + "model-left-in-wrong-gap": (("(short)PIN(original_gap[root], lower, upper)", "original_gap[root]"), "unrelated"), + "not-adjacent-to-glass": (("for (phase = -1; phase <= 1; ++phase)", "for (phase = 1; phase >= -1; --phase)"), "unrelated"), + "only-final-linked-part-bounded": (("steps == 1 ?", "TRUE ?"), "linked-union"), + "skinning-ignored": (("group->node_matrix_count == 1", "TRUE"), "skinned"), + "compressed-byte-weight": (("(real)vertex->node_weight * (1.0f / 32767.0f)", "(real)(byte)vertex->node_weight * (1.0f / 255.0f)"), "compressed-skinning"), + "intersection-accepted": (("high <= epsilon && low < -epsilon", "low < -epsilon"), "intersect"), + "sort-hook-removed": (("rasterizer_transparent_geometry_order_models(\n\t\ttransparent_geometry_group_sorted_indices, transparent_geometry_group_count);", "(void)0;"), "external-sort"), +} + + +def generated(control=None): + raster = read("source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c") + core = read("source/rasterizer/rasterizer_transparent_geometry.c") + types = transparent_types() + for path, names in [ + ("source/math/real_math.h", ["real_matrix4x3"]), + ("source/rasterizer/rasterizer_model_types.h", ["model_vertex_compressed", "model_vertex_uncompressed"]), + ]: + types += "\n" + "\n".join(structure(read(path), name) for name in names) + for path, name in [("source/cseries/cseries.h", "UNSIGNED_SHORT_MAX"), + ("source/models/model_definitions.h", "MAXIMUM_NODES_PER_MODEL"), + ("source/rasterizer/rasterizer.h", "RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS")]: + types += f"\n#define {name} {constant(read(path), name)}\n" + code = function(read("source/math/matrix_math.c"), "matrix4x3_transform_point") + for name in ["shader_is_water_decal", "shader_type_is_transparent", "shader_type_is_valid_for_model"]: + code += "\n" + function(read("source/shaders/shaders.c"), name) + for name in ["transparent_planar_glass", "transparent_model_world_bounds", "transparent_plane_model_order", + "transparent_chain_root", "rasterizer_transparent_geometry_order_models"]: + code += "\n" + function(raster, name) + for name in ["rasterizer_sort_internal", "group_sorted_indices_cmpfn", "rasterizer_sort_external"]: + code += "\n" + function(core, name) + if control: + before, after = CONTROLS[control][0] + if control == "only-final-linked-part-bounded": + # Both ends of the union deliberately lose the previous parts. + assert code.count(before) == 2 + code = code.replace(before, after) + else: + code = mutated(code, before, after) + return (("types.inc", types), ("under_test.inc", code)) + + +@pytest.mark.parametrize("case", CASES) +def test_case(case): + status, output = run(build("transparent_plane_order", generated()), case) + assert status == 0, output + + +def test_compressed_weight_declaration(): + # Confirm that the CPU's signed-short weights match the actual draw stream. + source = read("source/rasterizer/xbox/rasterizer_xbox_vertex_shaders_initialize.c") + declaration = re.search(r"vertex_shader_declarations\[\]\s*=\s*\{([^}]+)\}", source).group(1) + tokens = [int(v, 16) for v in re.findall(r"0x[\dA-Fa-f]+", declaration)] + offset = int(re.search(r"vertex_shader_table\[13\]\.declaration = &vertex_shader_declarations\[(0x[\dA-Fa-f]+)", source).group(1), 16) // 4 + weight = next(v for v in tokens[offset:offset+9] if v != 0xFFFFFFFF and v & 0xFFFF == 6) + assert (weight >> 16) & 0xFF == constant(read("port/include/xdk/xdk_d3d8.h"), "D3DVSDT_NORMSHORT1") + + +@pytest.mark.parametrize("control", CONTROLS) +def test_negative_control(control): + status, output = run(build("transparent_plane_order", generated(control)), CONTROLS[control][1]) + assert status == CHECK_FAILED, f"{control} escaped its check: {output}" diff --git a/tools/harness/tests/transparent_plane_order.c b/tools/harness/tests/transparent_plane_order.c new file mode 100644 index 00000000..67bcfe39 --- /dev/null +++ b/tools/harness/tests/transparent_plane_order.c @@ -0,0 +1,432 @@ +/* CPU fake buffers and camera; production queue, skinning and math below. */ +#include "harness.h" +#include +#include + +typedef unsigned short word; +typedef struct { short x, y; } point2d; +typedef struct { real x, y; } real_point2d; +typedef struct { real i, j; } real_vector2d; +typedef struct { real red, green, blue; } real_rgb_color; +typedef struct { real alpha; real_rgb_color rgb; } real_argb_color; +typedef struct { real_vector3d n; real d; } real_plane3d; +#define TAG_STRING_LENGTH 31 +#define VALID_INDEX(i, n) ((i) >= 0 && (i) < (n)) +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define PIN(x, a, b) MIN(MAX(x, a), b) +#define D3DLOCK_READONLY 16 +#define __cdecl +#include "types.inc" +typedef struct real_matrix4x3 real_matrix4x3; + +static struct transparent_geometry_group storage[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS]; +static struct transparent_geometry_group *transparent_geometry_groups = storage; +static short order[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS]; +static short *transparent_geometry_group_sorted_indices = order; +static short transparent_geometry_group_count; +static struct { struct { real_point3d position; } camera; } global_window_parameters; +static struct shader_transparent_glass_definition glass; +static struct shader_transparent_generic_definition materials[NUMBER_OF_SHADER_TYPES]; +static struct vertex_buffer buffers[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS]; +static struct model_vertex_uncompressed vertices[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS][2]; +static struct model_vertex_compressed compressed[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS][2]; +static real_matrix4x3 matrices[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS][2]; +static long locks, unlocks; + +static void *rasterizer_transparent_geometry_get_group_from_presorted_index(short index) +{ + CHECK(VALID_INDEX(index, transparent_geometry_group_count), "invalid group lookup %d", index); + return storage + index; +} + +static long rasterizer_geometry_get_vertex_size(short type) +{ + return type == _rasterizer_vertex_type_model_compressed ? sizeof(struct model_vertex_compressed) : + sizeof(struct model_vertex_uncompressed); +} + +static void IDirect3DVertexBuffer8_Lock(void *buffer, long offset, long size, byte **data, long flags) +{ + CHECK(offset == 0 && size == 0 && flags == D3DLOCK_READONLY, "read-only buffer lock"); + ++locks; + *data = buffer; +} + +static void IDirect3DVertexBuffer8_Unlock(void *buffer) +{ + CHECK(buffer != NULL, "unlock valid buffer"); + ++unlocks; +} + +/* The fake Chicago material only needs its common transparent flag prefix. */ +#define SHADER_GET_TRANSPARENT_GENERIC(s) (&((struct shader_transparent_generic_definition *)(s))->generic) +#define SHADER_GET_TRANSPARENT_CHICAGO(s) SHADER_GET_TRANSPARENT_GENERIC(s) +#define _shader_transparent_draw_before_water_bit _shader_transparent_flag_draw_before_water_bit + +#include "under_test.inc" + +static real_matrix4x3 identity(void) +{ + real_matrix4x3 result = {0}; + result.scale = 1; + result.forward.i = result.left.j = result.up.k = 1; + return result; +} + +static void reset(short count) +{ + short i; + memset(storage, 0, sizeof(storage)); + memset(buffers, 0, sizeof(buffers)); + memset(vertices, 0, sizeof(vertices)); + memset(compressed, 0, sizeof(compressed)); + memset(&glass, 0, sizeof(glass)); + memset(materials, 0, sizeof(materials)); + glass.shader.base.type = _shader_type_transparent_glass; + transparent_geometry_group_count = count; + for (i = 0; i < NUMBER_OF_SHADER_TYPES; ++i) + { + materials[i].shader.type = i; + } + for (i = 0; i < count; ++i) + { + order[i] = i; + storage[i].previous_group_presorted_index = NONE; + storage[i].next_group_presorted_index = NONE; + matrices[i][0] = matrices[i][1] = identity(); + } + global_window_parameters.camera.position = (real_point3d){0, 0, 5}; + locks = unlocks = 0; +} + +static void floor_at(short index, real height) +{ + storage[index].shader = &glass.shader; + storage[index].geometry_flags = FLAG(_rasterizer_geometry_no_sort_bit); + storage[index].plane.n.k = 1; + storage[index].plane.d = height; +} + +static void model_at(short index, real low, real high) +{ + storage[index].shader = (struct shader *)&materials[_shader_type_transparent_generic]; + storage[index].object_index = index + 100; + storage[index].vertex_buffer = buffers + index; + storage[index].node_matrices = matrices[index]; + storage[index].node_matrix_count = 1; + buffers[index].type = _rasterizer_vertex_type_model_uncompressed; + buffers[index].count = 2; + buffers[index].hardware_format = vertices[index]; + vertices[index][0].position = (real_point3d){-1, -1, low}; + vertices[index][1].position = (real_point3d){1, 1, high}; + vertices[index][0].nodes[1] = vertices[index][1].nodes[1] = NONE; + vertices[index][0].node_weights[0] = vertices[index][1].node_weights[0] = 1; +} + +static void link_parts(short first, short second) +{ + storage[first].next_group_presorted_index = second; + storage[second].previous_group_presorted_index = first; + storage[second].object_index = storage[first].object_index; +} + +static void expect(short const *expected, long count) +{ + long i; + rasterizer_transparent_geometry_order_models(order, count); + for (i = 0; i < count; ++i) + { + CHECK(order[i] == expected[i], "order[%ld]=%d, expected %d", i, order[i], expected[i]); + } + CHECK(locks == unlocks, "balanced CPU buffer access"); +} + +#define EXPECT(...) do { short expected[] = {__VA_ARGS__}; expect(expected, sizeof(expected)/sizeof(*expected)); } while (0) + +int main(int argc, char **argv) +{ + char const *case_name; + short i; + CHECK(argc == 2, "one named case"); + case_name = argv[1]; + reset(2); + model_at(0, 0, 2); + floor_at(1, 0); + + CASE("same-side") { EXPECT(1, 0); return 0; } + CASE("opposite-side") + { + order[0] = 1; order[1] = 0; + global_window_parameters.camera.position.z = -5; + EXPECT(0, 1); + return 0; + } + CASE("reverse-normal") + { + storage[1].plane.n.k = -1; + EXPECT(1, 0); + return 0; + } + CASE("unrelated") + { + reset(3); floor_at(0, 0); model_at(2, 1, 2); + global_window_parameters.camera.position.z = -5; + EXPECT(2, 0, 1); /* The intervening widget stays behind the floor. */ + reset(3); model_at(0, 1, 2); floor_at(2, 0); + EXPECT(1, 2, 0); /* Opposite move: widget stays before the floor. */ + reset(3); floor_at(1, 0); model_at(2, 1, 2); + global_window_parameters.camera.position.z = -5; + EXPECT(0, 2, 1); /* Move beside the glass, after an existing widget. */ + reset(3); model_at(0, 1, 2); floor_at(1, 0); + EXPECT(1, 0, 2); /* The arriving model precedes the following widget. */ + return 0; + } + CASE("multiple-models") + { + reset(5); floor_at(0, 0); model_at(2, 1, 2); model_at(4, 1, 2); + global_window_parameters.camera.position.z = -5; + EXPECT(2, 4, 0, 1, 3); + return 0; + } + CASE("multiple-planes") + { + reset(5); floor_at(0, 0); floor_at(2, 2); model_at(4, .5f, 1.5f); + EXPECT(0, 1, 4, 2, 3); + return 0; + } + CASE("conflict") + { + reset(3); floor_at(0, 2); floor_at(1, 0); model_at(2, .5f, 1.5f); + EXPECT(0, 1, 2); + return 0; + } + CASE("intersect") + { + vertices[0][0].position.z = -1; + EXPECT(0, 1); + order[0] = 1; order[1] = 0; + EXPECT(1, 0); + return 0; + } + CASE("camera-plane") + { + global_window_parameters.camera.position.z = 0; + EXPECT(0, 1); + return 0; + } + CASE("invalid-plane") + { + real values[] = {0, .2f, 2, NAN, INFINITY}; + for (i = 0; i < 5; ++i) + { + storage[1].plane.n.k = values[i]; + EXPECT(0, 1); + } + CHECK(locks == 0, "invalid planes do not read models"); + storage[1].plane.n.k = 1; + storage[1].plane.d = NAN; + EXPECT(0, 1); + return 0; + } + CASE("invalid-model") + { + vertices[0][0].position.x = NAN; + EXPECT(0, 1); + vertices[0][0].position.x = -1; + matrices[0][0].scale = INFINITY; + EXPECT(0, 1); + matrices[0][0].scale = 1; + buffers[0].offset = 4; + EXPECT(0, 1); + buffers[0].offset = 0; + buffers[0].count = 0; + EXPECT(0, 1); + return 0; + } + CASE("materials") + { + short types[] = {_shader_type_transparent_generic, _shader_type_transparent_chicago, + _shader_type_transparent_glass, _shader_type_transparent_meter, _shader_type_transparent_plasma}; + for (i = 0; i < 5; ++i) + { + order[0] = 0; order[1] = 1; + storage[0].shader = (struct shader *)(materials + types[i]); + EXPECT(1, 0); + } + return 0; + } + CASE("rigid-transform") + { + real bounds[2][3]; + matrices[0][0].forward = (real_vector3d){0, 0, 1}; + matrices[0][0].up = (real_vector3d){-1, 0, 0}; + matrices[0][0].scale = 2; + matrices[0][0].position.z = 3; + CHECK(transparent_model_world_bounds(storage, bounds), "rotated and scaled bounds"); + CHECK(bounds[0][2] == 1 && bounds[1][2] == 5, "real matrix transform applied"); + EXPECT(1, 0); + return 0; + } + CASE("skinned") + { + real bounds[2][3]; + storage[0].node_matrix_count = 2; + matrices[0][0].position.z = -8; + matrices[0][1].position.z = 8; + for (i = 0; i < 2; ++i) + { + vertices[0][i].nodes[1] = 1; + vertices[0][i].node_weights[0] = .25f; + vertices[0][i].node_weights[1] = .75f; + } + CHECK(transparent_model_world_bounds(storage, bounds), "weighted two-node bounds"); + CHECK(bounds[0][2] == 4 && bounds[1][2] == 6, "actual weights and both nodes"); + EXPECT(1, 0); + vertices[0][0].nodes[1] = 2; + order[0] = 0; order[1] = 1; + EXPECT(0, 1); + vertices[0][0].nodes[1] = 1; + vertices[0][0].node_weights[0] = NAN; + EXPECT(0, 1); + return 0; + } + CASE("compressed-skinning") + { + real bounds[2][3]; + storage[0].node_matrix_count = 2; + matrices[0][0].position.z = -8; + matrices[0][1].position.z = 8; + buffers[0].type = _rasterizer_vertex_type_model_compressed; + buffers[0].hardware_format = compressed[0]; + for (i = 0; i < 2; ++i) + { + compressed[0][i].position = vertices[0][i].position; + compressed[0][i].nodes[1] = 3; + compressed[0][i].node_weight = 8192; + } + CHECK(transparent_model_world_bounds(storage, bounds), "compressed node encoding"); + CHECK(fabs(bounds[0][2] - (8.f - 16.f * 8192.f/32767.f)) < .00001f, "signed normalized short matches GPU declaration"); + EXPECT(1, 0); + order[0] = 0; order[1] = 1; + compressed[0][0].nodes[1] = 4; + EXPECT(0, 1); + return 0; + } + CASE("linked") + { + reset(4); model_at(0, 1, 2); floor_at(1, 0); model_at(3, 0, 3); + link_parts(0, 3); + EXPECT(1, 0, 3, 2); + reset(4); model_at(0, 1, 2); floor_at(1, 0); model_at(3, 0, 3); + link_parts(0, 3); + order[0] = 3; order[1] = 1; order[2] = 2; order[3] = 0; + EXPECT(1, 0, 3, 2); /* The earliest queued member dispatches the same chain. */ + return 0; + } + CASE("linked-union") + { + reset(3); model_at(0, -2, -1); model_at(1, 1, 2); floor_at(2, 0); + link_parts(0, 1); + EXPECT(0, 1, 2); /* Whole chain intersects; bounding only the last part is unsafe. */ + storage[0].effect_type = 1; + EXPECT(0, 1, 2); /* Unsupported first member invalidates the entire chain. */ + return 0; + } + CASE("invalid-links") + { + storage[0].previous_group_presorted_index = 5; + EXPECT(0, 1); + storage[0].previous_group_presorted_index = NONE; + storage[0].next_group_presorted_index = 5; + EXPECT(0, 1); + reset(3); floor_at(0, 0); model_at(1, 1, 2); model_at(2, 1, 2); + link_parts(1, 2); link_parts(2, 1); + EXPECT(0, 1, 2); + return 0; + } + CASE("full-queue") + { + short seen[RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS] = {0}; + reset(RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS); + floor_at(0, 0); + global_window_parameters.camera.position.z = -5; + for (i = 1; i < transparent_geometry_group_count; ++i) + { + model_at(i, 1, 2); + } + rasterizer_transparent_geometry_order_models(order, transparent_geometry_group_count); + CHECK(order[transparent_geometry_group_count-1] == 0, "full queue moves models before glass"); + for (i = 0; i < transparent_geometry_group_count; ++i) + { + CHECK(VALID_INDEX(order[i], transparent_geometry_group_count) && !seen[order[i]]++, "permutation preserved"); + CHECK(i == transparent_geometry_group_count-1 || order[i] == i+1, "stable model order"); + } + CHECK(locks == transparent_geometry_group_count-1 && unlocks == locks, "one scan per model"); + return 0; + } + CASE("four-views") + { + for (i = 0; i < 4; ++i) + { + order[0] = 0; order[1] = 1; + global_window_parameters.camera.position.z = i & 1 ? -5 : 5; + if (i & 1) { EXPECT(0, 1); } else { EXPECT(1, 0); } + } + return 0; + } + CASE("no-glass") + { + storage[1].shader = NULL; + EXPECT(0, 1); + CHECK(!locks, "no glass has no model scan cost"); + return 0; + } + CASE("external-sort") + { + storage[0].z_sort = -20; + storage[1].z_sort = -10; + rasterizer_sort_external(); + CHECK(order[0] == 1 && order[1] == 0, "correction runs after actual qsort"); + CHECK(storage[1].sorted_index == 0 && storage[0].sorted_index == 1, "published sorted indices agree"); + return 0; + } + CASE("fallback-flags") + { + short flags[] = {_rasterizer_geometry_no_sort_bit, _rasterizer_geometry_no_queue_bit, + _rasterizer_geometry_no_zbuffer_bit, _rasterizer_geometry_sky_bit, + _rasterizer_geometry_viewspace_bit, _rasterizer_geometry_first_person_bit}; + for (i = 0; i < 6; ++i) + { + storage[0].geometry_flags = FLAG(flags[i]); + EXPECT(0, 1); + } + storage[0].geometry_flags = 0; + storage[0].active_camouflage_transparent_source_object_index = 100; + EXPECT(0, 1); + storage[0].active_camouflage_transparent_source_object_index = 0; + materials[_shader_type_transparent_generic].generic.flags = FLAG(_shader_transparent_flag_draw_before_water_bit); + EXPECT(0, 1); + materials[_shader_type_transparent_generic].generic.flags = 0; + storage[0].shader = (struct shader *)&materials[_shader_type_transparent_water]; + EXPECT(0, 1); + storage[0].shader = (struct shader *)&materials[_shader_type_transparent_generic]; + glass.flags = FLAG(_shader_transparent_glass_flag_decal_bit); + EXPECT(0, 1); + glass.flags = 0; + glass.reflection_type = _shader_transparent_glass_reflection_type_dynamic_mirror; + EXPECT(0, 1); + return 0; + } + CASE("invalid-order") + { + order[0] = order[1] = 0; + EXPECT(0, 0); + order[0] = 0; order[1] = 2; + EXPECT(0, 2); + CHECK(locks == 0, "invalid order rejected before buffer access"); + return 0; + } + CHECK(FALSE, "unknown case %s", case_name); + return 1; +} From aa7b1075712b35f84df30d8b03acba0a5063b85f Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 00:51:49 -0400 Subject: [PATCH 045/187] Interpolation: the camera's previous snapshot a tick back after a long frame The camera is blended between snapshots of the observer, taken at the first frame after each tick, and the objects between their last two ticks. After a long frame (several ticks before it), the camera's previous snapshot was several ticks old, so for the rest of the tick the camera moved several ticks' worth while the world moved one: in a Pelican, out through its hull for a frame. The previous snapshot is now placed one tick back from the latest, on a steady move between the two, so the camera moves with the world. A cut is still a cut: the test is made on the snapshots as taken. Measured on b30 at 144 frames a second with a 100 ms stall every 50 frames (3 to 4 ticks in the next frame), first person in the Pelican: on the frame after a stall, the camera moved in the Pelican's frame by 23.6 cm (median) and up to 208 cm; now 0.06 cm, and 0.5 cm at the 99th percentile. --- port/linux/README.md | 2 + port/linux/game/render_interpolation.c | 92 ++++++++++++++++---------- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index 6f830f25..e3fb3048 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -330,6 +330,8 @@ Each frame shows the world between the last two ticks the last tick. - Rotations use quaternions. Positions and scales are linear. - A teleport, a respawn or a cut of the camera does not mix. It jumps. +- After a long frame (several ticks in one frame), the camera mixes the + last tick only, as the objects do. Thus the frames are one tick (33 ms) after the calculation. The calculation does not change. diff --git a/port/linux/game/render_interpolation.c b/port/linux/game/render_interpolation.c index 8462295d..d3f1d425 100644 --- a/port/linux/game/render_interpolation.c +++ b/port/linux/game/render_interpolation.c @@ -726,6 +726,48 @@ struct observer_result const *render_interpolation_camera( render_interpolation_blended_camera(local_player_index, observer)); } +/* between two of the observer's results: the position, the axes (up kept +square to forward) and the field of view */ +static void observer_blend( + struct observer_result const *a, + struct observer_result const *b, + real t, + struct observer_result *result) +{ + real along, length; + + *result = *b; + point_lerp(&a->position, &b->position, t, &result->position); + vector_nlerp(&a->forward, &b->forward, t, &result->forward); + vector_nlerp(&a->up, &b->up, t, &result->up); + along = result->up.i * result->forward.i + result->up.j * result->forward.j + result->up.k * result->forward.k; + result->up.i -= result->forward.i * along; + result->up.j -= result->forward.j * along; + result->up.k -= result->forward.k * along; + length = vector_length(&result->up); + if (length > 1e-6f) + { + result->up.i /= length; + result->up.j /= length; + result->up.k /= length; + } + else + { + result->up = b->up; + } + result->field_of_view = lerp(a->field_of_view, b->field_of_view, t); +} + +/* a cut between two of the observer's results (so written that a position +or direction not a number cuts) */ +static boolean observer_cut( + struct observer_result const *a, + struct observer_result const *b) +{ + return !(distance_squared(&a->position, &b->position) <= CAMERA_CUT_DISTANCE * CAMERA_CUT_DISTANCE) || + !(a->forward.i * b->forward.i + a->forward.j * b->forward.j + a->forward.k * b->forward.k >= CAMERA_CUT_COSINE); +} + static struct observer_result const *render_interpolation_blended_camera( short local_player_index, struct observer_result const *observer) @@ -751,22 +793,29 @@ static struct observer_result const *render_interpolation_blended_camera( camera->has_previous = camera->valid; camera->previous = camera->latest; camera->latest = *observer; + /* Several ticks since the last snapshot (a long frame): the objects + are drawn between the last two ticks, so the camera's previous is + where it was a tick ago, as nearly as a steady move from the last + snapshot tells. (Blended across all the ticks since, the camera + moved further each frame than the world it is in, out through a + Pelican's hull for a frame.) A cut stays a cut. */ + if (camera->has_previous && interpolation_tick - camera->tick > 1 && + !observer_cut(&camera->previous, &camera->latest)) + { + real ticks = (real)(interpolation_tick - camera->tick); + struct observer_result previous = camera->previous; + + observer_blend(&previous, &camera->latest, (ticks - 1.0f) / ticks, &camera->previous); + } camera->tick = interpolation_tick; camera->valid = TRUE; } - /* (so written that a position or direction not a number cuts) */ - if (!camera->has_previous || - !(distance_squared(&camera->previous.position, &camera->latest.position) <= - CAMERA_CUT_DISTANCE * CAMERA_CUT_DISTANCE) || - !(camera->previous.forward.i * camera->latest.forward.i + - camera->previous.forward.j * camera->latest.forward.j + - camera->previous.forward.k * camera->latest.forward.k >= CAMERA_CUT_COSINE)) + if (!camera->has_previous || observer_cut(&camera->previous, &camera->latest)) { return observer; } - camera->blended = camera->latest; - point_lerp(&camera->previous.position, &camera->latest.position, t, &camera->blended.position); + observer_blend(&camera->previous, &camera->latest, t, &camera->blended); if (correction_significant(&camera->correction) || correction_significant(&camera->correction_pending)) { real_vector3d drawn; @@ -776,31 +825,6 @@ static struct observer_result const *render_interpolation_blended_camera( camera->blended.position.y += drawn.j; camera->blended.position.z += drawn.k; } - vector_nlerp(&camera->previous.forward, &camera->latest.forward, t, &camera->blended.forward); - vector_nlerp(&camera->previous.up, &camera->latest.up, t, &camera->blended.up); - { - /* keep up perpendicular to forward */ - real_vector3d *forward = &camera->blended.forward; - real_vector3d *up = &camera->blended.up; - real along = up->i * forward->i + up->j * forward->j + up->k * forward->k; - real length; - - up->i -= forward->i * along; - up->j -= forward->j * along; - up->k -= forward->k * along; - length = vector_length(up); - if (length > 1e-6f) - { - up->i /= length; - up->j /= length; - up->k /= length; - } - else - { - *up = camera->latest.up; - } - } - camera->blended.field_of_view = lerp(camera->previous.field_of_view, camera->latest.field_of_view, t); return &camera->blended; } From 33c2baa286df9e4544f075dedc17347e5f15732a Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 01:13:17 -0400 Subject: [PATCH 046/187] Interpolation: a scripted camera that moves with an object drawn with the object A cinematic's camera point relative to an object (a30's lifepod, a dropship), or a unit's eyes in one, is placed from the object as it stands after the last tick. It was blended from its own snapshots, taken at the first frame after each tick, while the object is blended from its own, or steps from tick to tick when its pose snaps, so the camera ran ahead of or behind the object as drawn and showed outside the lifepod's hull. Such a camera is now drawn where the observer has it, moved by the object's drawn root node minus its root node as of the last tick, so it stays put in the object. Measured on a30's opening at 144 frames a second, the camera's position in the lifepod's frame from one frame to the next: - the lifepod's descent (85 ticks, about 400 frames): 52.6 cm median, 200 cm at the 95th percentile, now 0.0 cm and 8.5 cm; off a straight line through the frames around it, 68 cm median, now 1.8 cm - the shot before it: 52 cm at the 95th percentile and up to 244 cm, now 1.4 cm and up to 23 cm --- port/linux/README.md | 2 ++ port/linux/game/render_interpolation.c | 34 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/port/linux/README.md b/port/linux/README.md index e3fb3048..94d5369d 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -332,6 +332,8 @@ Each frame shows the world between the last two ticks - A teleport, a respawn or a cut of the camera does not mix. It jumps. - After a long frame (several ticks in one frame), the camera mixes the last tick only, as the objects do. +- In cinematics, a camera that moves with an object (the lifepod in a30, + a Pelican) moves with the object as it is drawn. Thus the frames are one tick (33 ms) after the calculation. The calculation does not change. diff --git a/port/linux/game/render_interpolation.c b/port/linux/game/render_interpolation.c index d3f1d425..6add1665 100644 --- a/port/linux/game/render_interpolation.c +++ b/port/linux/game/render_interpolation.c @@ -35,6 +35,7 @@ with their unit, and with what it rides. #include "cseries.h" #include "math/real_math.h" #include "objects/objects.h" +#include "camera/camera_scripting.h" #include "camera/director.h" #include "camera/observer.h" #include "cutscene/cinematics.h" @@ -780,6 +781,39 @@ static struct observer_result const *render_interpolation_blended_camera( return observer; } camera = &interpolated_cameras[local_player_index]; + /* A scripted camera that moves with an object (a cinematic's camera + point relative to a lifepod or dropship, or a unit's eyes in one) is + placed by the object as it stands after the last tick. Blended from its + own snapshots (taken at the first frame after each tick, a different + part of a tick each tick), it did not follow the object as drawn, which + is blended from the object's snapshots, or steps from tick to tick when + its pose snaps: it ran up to 2 m ahead of or behind the object, out + through the lifepod's hull in a30. It is drawn where the observer has + it, moved with the object from where the object is to where it is + drawn. (An animated camera has no such object.) */ + if (director_get_perspective(local_player_index) == _director_perspective_scripted) + { + long relative_object_index = scripted_camera_object_relative_to(); + real_matrix4x3 const *drawn; + + if (relative_object_index != NONE && + object_try_and_get_and_verify_type(relative_object_index, _object_mask_all) && + (drawn = render_interpolation_object_node_matrices(relative_object_index)) != NULL) + { + /* (as of the last tick: object_get_node_matrices gives the drawn + pose while a frame is drawn) */ + real_matrix4x3 const *nodes = (real_matrix4x3 const *)object_header_block_get( + relative_object_index, &object_get(relative_object_index)->object.node_matrices); + + camera->blended = *observer; + camera->blended.position.x += drawn[0].position.x - nodes[0].position.x; + camera->blended.position.y += drawn[0].position.y - nodes[0].position.y; + camera->blended.position.z += drawn[0].position.z - nodes[0].position.z; + /* (the snapshots start again after) */ + camera->valid = FALSE; + return &camera->blended; + } + } /* the observer as it stood after each tick (the first frame drawn after the tick) */ if (!camera->valid || camera->tick != interpolation_tick) From f502268853aa6a820f3226284a451a8cf0285f53 Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Thu, 8 Oct 2026 03:12:19 -0400 Subject: [PATCH 047/187] Fix keyboard crouch walking --- source/input/input_abstraction.c | 9 + tools/harness/README.md | 2 + tools/harness/__init__.py | 6 +- tools/harness/tests/keyboard_crouch.c | 186 ++++++++++++++++++++ tools/harness/tests/test_keyboard_crouch.py | 71 ++++++++ 5 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 tools/harness/tests/keyboard_crouch.c create mode 100644 tools/harness/tests/test_keyboard_crouch.py diff --git a/source/input/input_abstraction.c b/source/input/input_abstraction.c index 137a9845..d9b372ca 100644 --- a/source/input/input_abstraction.c +++ b/source/input/input_abstraction.c @@ -265,6 +265,15 @@ static void keyboard_controls_update( /* (diagonals on the unit circle) */ real length = x && y ? 0.70710678f : 1.f; + /* port: full stick uncrouches a grounded biped (player_control.c). + Keep keyboard crouch below its strict 0.98f magnitude cutoff: this + is the largest float below 0.98f, applied after diagonal normalization. + Only keyboard movement with the keyboard's crouch action is scaled. */ + if (TEST_FLAG(held, HALO_KEYBOARD_CROUCH)) + { + length *= 0.97999996f; + } + state->forward_movement = y * length; state->strafe = -x * length; } diff --git a/tools/harness/README.md b/tools/harness/README.md index 407a203e..7cdbc769 100644 --- a/tools/harness/README.md +++ b/tools/harness/README.md @@ -13,6 +13,8 @@ finds every `tests/test_*.py`, so a new test needs no change to CI. - `tests/.c`: the fake world, only what the code reads, with what it calls stubbed as macros that name only the arguments they use; then `#include "under_test.inc"` and a `main` that runs the case named on its command line, checked with `CHECK`. +- `build` accepts optional compiler `flags` when a test needs the port's arithmetic settings (for example, + `-march=x86-64 -ffp-contract=off` for a strict single-precision boundary). - `tests/test_.py`: takes the code, enums and constants from the sources (`function`, `inline`, `enum_with`, `constant`) rather than copying them, so the test checks what the game builds; `build` compiles it (warnings are errors) and each case is a pytest test. diff --git a/tools/harness/__init__.py b/tools/harness/__init__.py index 01b8bda3..62106ae7 100644 --- a/tools/harness/__init__.py +++ b/tools/harness/__init__.py @@ -67,14 +67,14 @@ CHECK_FAILED = 1 # a case's exit status when a CHECK fails (harness.h) @functools.lru_cache(maxsize=None) -def build(test, generated): +def build(test, generated, flags=()): """tests/.c compiled with the generated includes ((name, text), ...) for the game's 32-bit ABI.""" - work = Path(_directory.name) / f"{test}-{abs(hash(generated)):x}" + work = Path(_directory.name) / f"{test}-{abs(hash((generated, flags))):x}" work.mkdir(parents=True, exist_ok=True) for name, text in generated: (work / name).write_text(text) executable = work / test - command = [os.environ.get("CC", "clang"), "-m32", "-std=gnu99", "-O2", "-Wall", "-Werror", "-Wno-unused-function", + command = [os.environ.get("CC", "clang"), "-m32", "-std=gnu99", "-O2", *flags, "-Wall", "-Werror", "-Wno-unused-function", "-Wno-unused-variable", "-I", str(HARNESS / "include"), "-I", str(work), str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable)] result = subprocess.run(command, capture_output=True, text=True) diff --git a/tools/harness/tests/keyboard_crouch.c b/tools/harness/tests/keyboard_crouch.c new file mode 100644 index 00000000..2c519984 --- /dev/null +++ b/tools/harness/tests/keyboard_crouch.c @@ -0,0 +1,186 @@ +/* Actual keyboard input over fake held actions, then the unchanged player-control crouch gate. */ +#include "harness.h" +#include +#include "config.inc" + +typedef struct { real i, j; } real_vector2d; +struct biped_datum { struct { unsigned long flags; } biped; }; +struct test_input { real_vector2d throttle; unsigned long unit_control_flags; }; + +#define MIN(a,b) ((a)>(b)?(b):(a)) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define PIN(n,floor,ceiling) MAX((floor), MIN((n), (ceiling))) +#define SET_FLAG(f,b,v) ((v) ? ((f)|=(unsigned)FLAG(b)) : ((f)&=(unsigned)~FLAG(b))) + +static unsigned long held_actions[MAXIMUM_GAMEPADS]; +static long now; +static boolean controls_enable_crouch; +static unsigned long halo_keyboard_actions(short controller_index) { return held_actions[controller_index]; } +static long system_milliseconds(void) { return now; } +#include "under_test.inc" + +static struct game_input_state keyboard(unsigned long held) +{ + struct game_input_state state = {0}; + held_actions[0] = held; + keyboard_controls_update(0, &state); + return state; +} + +static boolean crouches(struct game_input_state state, unsigned long biped_flags, boolean inhibited) +{ + struct biped_datum biped = {{biped_flags}}; + struct test_input input = {{state.forward_movement, state.strafe}, 0}; + if (inhibited) state.buttons[_game_control_crouch] = 0; + apply_crouch(&biped, &input, state.buttons); + return TEST_FLAG(input.unit_control_flags, _unit_control_crouch_modifier_bit); +} + +static real next_positive_float(real value) +{ + uint32_t bits; + memcpy(&bits, &value, sizeof(bits)); + bits++; + memcpy(&value, &bits, sizeof(value)); + return value; +} + +static unsigned long direction(long x, long y) +{ + return (x < 0 ? FLAG(HALO_KEYBOARD_STRAFE_LEFT) : x > 0 ? FLAG(HALO_KEYBOARD_STRAFE_RIGHT) : 0) | + (y < 0 ? FLAG(HALO_KEYBOARD_MOVE_BACKWARD) : y > 0 ? FLAG(HALO_KEYBOARD_MOVE_FORWARD) : 0); +} + +int main(int argc, char **argv) +{ + const char *case_name = argc > 1 ? argv[1] : ""; + unsigned long crouch = FLAG(HALO_KEYBOARD_CROUCH); + unsigned long forward = FLAG(HALO_KEYBOARD_MOVE_FORWARD); + long x, y; + + CASE("cardinals") + { + for (x = -1; x <= 1; x++) for (y = -1; y <= 1; y++) if ((x != 0) != (y != 0)) + { + struct game_input_state state = keyboard(crouch | direction(x, y)); + CHECK(crouches(state, 0, FALSE), "grounded crouch cancelled for (%ld,%ld)", x, y); + CHECK(state.forward_movement * y + state.strafe * -x > 0.f, "direction reversed"); + } + } + else CASE("diagonals") + { + for (x = -1; x <= 1; x += 2) for (y = -1; y <= 1; y += 2) + { + struct game_input_state state = keyboard(crouch | direction(x, y)); + CHECK(crouches(state, 0, FALSE), "diagonal crouch cancelled for (%ld,%ld)", x, y); + CHECK(state.forward_movement * y == state.strafe * -x && state.forward_movement * y > 0.f, + "diagonal direction changed"); + } + } + else CASE("maximum-safe-input") + { + struct game_input_state state = keyboard(crouch | forward); + CHECK(crouches(state, 0, FALSE), "cardinal cap fails gate"); + state.forward_movement = next_positive_float(state.forward_movement); + CHECK(!crouches(state, 0, FALSE), "cap is below the largest safe cardinal float"); + state = keyboard(crouch | forward | FLAG(HALO_KEYBOARD_STRAFE_LEFT)); + CHECK(crouches(state, 0, FALSE), "diagonal cap fails gate"); + state.forward_movement = next_positive_float(state.forward_movement); + state.strafe = next_positive_float(state.strafe); + CHECK(!crouches(state, 0, FALSE), "diagonal components are below the largest safe floats"); + } + else CASE("run-and-release") + { + for (x = -1; x <= 1; x++) for (y = -1; y <= 1; y++) if (x || y) + { + struct game_input_state running = keyboard(direction(x, y)); + struct game_input_state walking = keyboard(crouch | direction(x, y)); + struct game_input_state released = keyboard(direction(x, y)); + real component = x && y ? 0.70710678f : 1.f; + CHECK(running.forward_movement == y * component && running.strafe == -x * component, + "normal running changed"); + CHECK(crouches(walking, 0, FALSE) && !crouches(released, 0, FALSE), "release keeps crouch"); + CHECK(released.forward_movement == running.forward_movement && released.strafe == running.strafe, + "release does not restore full movement"); + } + } + else CASE("controller-and-mixed-input") + { + long controller; + for (controller = 0; controller < MAXIMUM_GAMEPADS; controller++) + { + struct game_input_state state = {0}; + state.forward_movement = 1.f; + state.buttons[_game_control_crouch] = 7; + held_actions[controller] = 0; + keyboard_controls_update(controller, &state); + CHECK(state.forward_movement == 1.f && !crouches(state, 0, FALSE), "controller full stick changed"); + state.forward_movement = 0.5f; + keyboard_controls_update(controller, &state); + CHECK(state.forward_movement == 0.5f && crouches(state, 0, FALSE), "controller partial stick changed"); + } + { + struct game_input_state state = {0}; + state.forward_movement = 1.f; + state.yaw = 0.4f; + state.pitch = -0.2f; + held_actions[0] = crouch; + keyboard_controls_update(0, &state); + CHECK(state.forward_movement == 1.f && !crouches(state, 0, FALSE), "keyboard crouch scales controller movement"); + state.buttons[_game_control_crouch] = 7; + held_actions[0] = forward; + keyboard_controls_update(0, &state); + CHECK(state.forward_movement == 1.f && !crouches(state, 0, FALSE), "controller crouch scales keyboard movement"); + held_actions[0] = forward | crouch; + keyboard_controls_update(0, &state); + CHECK(crouches(state, 0, FALSE), "mixed input overrides keyboard cap"); + CHECK(state.yaw == 0.4f && state.pitch == -0.2f && state.buttons[_game_control_crouch] == 7, + "look or controller button precedence changed"); + } + } + else CASE("cancelled-keys") + { + struct game_input_state state = {0}; + state.forward_movement = 0.4f; + state.strafe = -0.3f; + held_actions[0] = crouch | forward | FLAG(HALO_KEYBOARD_MOVE_BACKWARD) | + FLAG(HALO_KEYBOARD_STRAFE_LEFT) | FLAG(HALO_KEYBOARD_STRAFE_RIGHT); + keyboard_controls_update(0, &state); + CHECK(state.forward_movement == 0.4f && state.strafe == -0.3f, "opposing keys change analog fallback"); + CHECK(crouches(state, 0, FALSE), "stationary keyboard crouch fails"); + } + else CASE("hold-across-frame-rates") + { + long rates[] = {15, 30, 60, 144, 240, 1000}; + long rate, frame; + for (rate = 0; rate < (long)(sizeof(rates) / sizeof(*rates)); rate++) + { + memset(keyboard_controls, 0, sizeof(keyboard_controls)); + for (frame = 0; frame < rates[rate] * 5; frame++) + { + struct game_input_state state; + now = frame * 1000 / rates[rate]; + state = keyboard(crouch | forward | FLAG(HALO_KEYBOARD_RELOAD)); + CHECK(crouches(state, 0, FALSE), "held crouch fails at %ld FPS, frame %ld", rates[rate], frame); + CHECK(keyboard_controls[0].reload_ticks != 0, "reload lost"); + } + CHECK(keyboard(0).buttons[_game_control_crouch] == 0 && keyboard_controls[0].reload_ticks == 0, + "release retains held buttons"); + } + } + else CASE("crouch-gate-exceptions") + { + struct game_input_state state = keyboard(forward); + struct test_input input = {{0.f, 0.f}, 0}; + state.buttons[_game_control_crouch] = 1; + CHECK(crouches(state, FLAG(_biped_airborne_bit), FALSE), "airborne crouch changed"); + controls_enable_crouch = TRUE; + CHECK(crouches(state, 0, FALSE), "script-enabled full-speed crouch changed"); + state = keyboard(crouch | forward); + CHECK(!crouches(state, 0, TRUE), "inhibited crouch bypassed"); + apply_crouch(NULL, &input, state.buttons); + CHECK(input.unit_control_flags == 0, "non-biped gets crouch"); + } + else { fprintf(stderr, "unknown case: %s\n", case_name); return 2; } + return 0; +} diff --git a/tools/harness/tests/test_keyboard_crouch.py b/tools/harness/tests/test_keyboard_crouch.py new file mode 100644 index 00000000..15cab7ff --- /dev/null +++ b/tools/harness/tests/test_keyboard_crouch.py @@ -0,0 +1,71 @@ +"""Real keyboard movement and player-control crouch gate, including the strict float boundary.""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, constant, enum_with, function, inline, mutated, read, run # noqa: E402 + +CASES = ["cardinals", "diagonals", "maximum-safe-input", "run-and-release", + "controller-and-mixed-input", "cancelled-keys", "hold-across-frame-rates", "crouch-gate-exceptions"] + +# Match portable desktop builds: SSE float arithmetic, with no fused multiply-add. +# Bare -m32 uses x87 excess precision and changes the strict boundary comparison. +FLAGS = ("-march=x86-64", "-ffp-contract=off") + +NEGATIVE_CONTROLS = { + "original-full-speed": (("length *= 0.97999996f;", "length *= 1.f;"), "cardinals"), + "at-the-strict-cutoff": (("length *= 0.97999996f;", "length *= 0.98f;"), "cardinals"), + "unnormalized-diagonals": (("x && y ? 0.70710678f : 1.f", "1.f"), "diagonals"), + "slows-running-too": (("TEST_FLAG(held, HALO_KEYBOARD_CROUCH)", "TRUE"), "run-and-release"), + "uses-controller-crouch": (("TEST_FLAG(held, HALO_KEYBOARD_CROUCH)", + "state->buttons[_game_control_crouch]"), "controller-and-mixed-input"), +} + + +def generated(fault=None): + abstraction = read("source/input/input_abstraction.c") + player = read("source/game/player_control.c") + enums = "\n".join(enum_with(source, member) for source, member in ( + (abstraction, "_game_control_jump"), (player, "_button_jump"), + (read("port/linux/include/halo_keyboard.h"), "HALO_KEYBOARD_MOVE_FORWARD"), + (read("source/units/bipeds.h"), "_biped_airborne_bit"), + (read("source/units/units.h"), "_unit_control_crouch_modifier_bit"))) + for source, name in (("source/input/input.h", "MAXIMUM_GAMEPADS"), + ("source/cseries/cseries.h", "TICKS_PER_SECOND"), + ("source/cseries/cseries.h", "UNSIGNED_CHAR_MAX")): + enums += f"\n#define {name} {constant(read(source), name)}\n" + start = abstraction.index("struct game_input_state\n{") + enums += abstraction[start:abstraction.index("};", start) + 2] + "\n" + start = abstraction.index("static signed char const keyboard_game_controls") + text = abstraction[start:abstraction.index("static void keyboard_hold_ticks", start)] + text += function(abstraction, "keyboard_hold_ticks") + "\n" + update = function(abstraction, "keyboard_controls_update") + text += (mutated(update, *fault) if fault else update) + "\n" + text += inline(read("source/math/real_math.h"), "magnitude_squared2d") + "\n" + + # Compile the actual downstream condition and flag write, rather than a copied threshold. + start = player.index("if (biped &&") + end = player.index("{", start) + 1 + depth = 1 + while depth: + depth += (player[end] == "{") - (player[end] == "}") + end += 1 + gate = "static void apply_crouch(struct biped_datum *biped, struct test_input *input, " \ + "byte const *effective_buttons)\n{\n" + player[start:end] + "\n}\n" + return (("config.inc", enums), ("under_test.inc", text + gate)) + + +@pytest.mark.parametrize("case", CASES) +def test_case(case): + status, output = run(build("keyboard_crouch", generated(), FLAGS), case) + assert status == 0, output + + +@pytest.mark.parametrize("control", NEGATIVE_CONTROLS) +def test_negative_control(control): + fault, case = NEGATIVE_CONTROLS[control] + status, output = run(build("keyboard_crouch", generated(fault), FLAGS), case) + assert status == CHECK_FAILED, f"keyboard input with fault {control} passed {case}: the test cannot see it" From 98a791a7d0b821e10ab0199984f91d81f43bb64f Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Wed, 30 Sep 2026 22:23:21 +0200 Subject: [PATCH 048/187] Track writes by page hashes under ARM translation --- .github/workflows/build.yml | 2 +- port/android/README.md | 10 + port/android/guest/runtime/guest_host.h | 2 + .../guest/runtime/guest_memory_watch.c | 5 + port/android/host/host.h | 4 + port/android/host/host_main.c | 50 ++++ port/android/host/host_memory.c | 57 ++++- port/android/host/host_watch_hash.c | 114 +++++++++ port/android/host/host_watch_hash.h | 57 +++++ port/android/host_imports.list | 1 + port/android/tests/host_watch_hash_test.c | 216 ++++++++++++++++++ port/linux/src/d3d8_gl.c | 1 + port/linux/src/memory_watch.c | 5 + port/linux/src/platform.h | 4 + port/linux/src/port_config.c | 8 +- port/windows/src/win32_memory_watch.c | 5 + tools/test_host_watch_hash.py | 34 +++ 17 files changed, 571 insertions(+), 4 deletions(-) create mode 100644 port/android/host/host_watch_hash.c create mode 100644 port/android/host/host_watch_hash.h create mode 100644 port/android/tests/host_watch_hash_test.c create mode 100644 tools/test_host_watch_hash.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7e092a14..6d89d654 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: - name: Test run: | - python -m pytest -q tools/test_linux_port.py tools/harness + python -m pytest -q tools/test_linux_port.py tools/test_host_watch_hash.py tools/harness python tools/test_light_storage.py - name: Compile cache diff --git a/port/android/README.md b/port/android/README.md index ed15f790..aa8308ee 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -118,6 +118,7 @@ These settings are only for Android: | --- | --- | | `display.screen_width` | The number of columns of the 480-line picture. `0` (the default): the shape of the display (1068 on a 20:9 phone). `640`: the 4:3 shape of the Xbox. | | `debug.sample_seconds` | Refer to "Find problems". | +| `debug.memory_watch` | `true` (the default): the app notices the game's writes to textures and vertices by page protection. `false`: it compares page contents once a frame instead, which is slower. Refer to "Limits". | ## Internet play @@ -310,6 +311,8 @@ assembly of the port is necessary: at this interval. This finds hangs on devices without root access. - Set `gl_debug = true` in `[debug]` of `config.toml`. The log then shows the OpenGL ES errors. +- The log says which write tracking the app uses: "page protection" + (devices) or "page hashes" (the emulator, or `debug.memory_watch = false`). ## Limits @@ -320,3 +323,10 @@ assembly of the port is necessary: - The game does not accept touch input. Use a controller or a keyboard. - Kernels with 16 KB pages (a developer option of Android 15) do not operate. The Xbox memory uses 4 KB pages. +- The x86 Android emulator runs the app through its ARM translation. The + translation cannot deliver the page faults that the renderer uses to + notice changed textures and vertices. On the emulator the app compares + page contents once a frame instead ("write tracking: page hashes" in the + log). This is slower, and a write shows one frame later, so moving + geometry can glitch briefly. `debug.memory_watch = false` does the same + on a device. diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h index e77403c2..6534c3ad 100644 --- a/port/android/guest/runtime/guest_host.h +++ b/port/android/guest/runtime/guest_host.h @@ -46,6 +46,8 @@ unsigned int host_memory_watch_generation(unsigned int address, unsigned int siz unsigned int host_memory_watch_serial(void); void host_memory_watch_prepare_write(unsigned int address, unsigned int size); void host_memory_watch_forget(unsigned int address, unsigned int size); +/* memory_watch_begin_frame (port/linux/src/platform.h) */ +void host_memory_watch_begin_frame(void); /* ---------- SDL (guest/runtime/guest_sdl.c) diff --git a/port/android/guest/runtime/guest_memory_watch.c b/port/android/guest/runtime/guest_memory_watch.c index 4f339852..d5ccfd2c 100644 --- a/port/android/guest/runtime/guest_memory_watch.c +++ b/port/android/guest/runtime/guest_memory_watch.c @@ -39,3 +39,8 @@ void memory_watch_forget(void *address, unsigned long size) { host_memory_watch_forget((unsigned int)address, size); } + +void memory_watch_begin_frame(void) +{ + host_memory_watch_begin_frame(); +} diff --git a/port/android/host/host.h b/port/android/host/host.h index eef727c6..1cebeee7 100644 --- a/port/android/host/host.h +++ b/port/android/host/host.h @@ -37,6 +37,10 @@ mappings) from pools of address space it reserves below 4 GB on demand. */ /* reserves the fixed ranges; returns 0 on success */ int host_memory_initialize(uint32_t image_base, uint32_t image_size); +/* Makes the write tracking compare page contents instead of catching page +faults (host_watch_hash.h), for when the app cannot receive its own +SIGSEGV (ARM translation). Call before the guest starts. */ +void host_memory_watch_use_hashes(void); /* page-granular allocations below 4 GB; NULL on failure */ void *host_low_map(size_t size, int protection); void host_low_unmap(void *address, size_t size); diff --git a/port/android/host/host_main.c b/port/android/host/host_main.c index 401c85bf..46d2e759 100644 --- a/port/android/host/host_main.c +++ b/port/android/host/host_main.c @@ -31,6 +31,7 @@ that runs here. #include #include #include +#include #include #include #include @@ -181,6 +182,37 @@ static int config_sample_seconds(const char *path, char *text, size_t size) return found; } +/* a boolean setting of config.toml (a dotted name), or otherwise when the +file, the setting or a boolean is missing */ +static int config_boolean_or(const char *path, const char *name, int otherwise) +{ + toml_result_t result = toml_parse_file_ex(path); + int value = otherwise; + + if (!result.ok) + return otherwise; + { + toml_datum_t datum = toml_seek(result.toptab, name); + + if (datum.type == TOML_BOOLEAN) + value = datum.u.boolean ? 1 : 0; + } + toml_free(result); + return value; +} + +/* Whether the app runs through an ARM translator. The x86 emulator runs the +app's ARM code through one, which ro.dalvik.vm.native.bridge names; it +cannot deliver the page faults of the write tracking to the app +(host_memory.c). */ +static int native_bridge_active(void) +{ + char value[PROP_VALUE_MAX] = ""; + + __system_property_get("ro.dalvik.vm.native.bridge", value); + return value[0] && strcmp(value, "0") != 0; +} + /* POSIX TZ for the current local offset (the guest's musl has no zone database) */ static void time_zone(char *buffer, size_t size) @@ -233,6 +265,10 @@ static uint32_t make_boot(const struct environment *environment) #define MAIN_STACK_SIZE (16 * 1024 * 1024) +/* the thread that runs the game: passes the display and the settings to the +guest through its environment, chooses the write tracking (page +protection, or page hashes when translated or when debug.memory_watch is +false), and runs the guest's main */ static void *game_main(void *unused) { struct environment environment = { { 0 }, 0 }; @@ -308,6 +344,20 @@ static void *game_main(void *unused) if (config_sample_seconds(path, seconds, sizeof(seconds))) host_debug_start_sampler(seconds); } + if (native_bridge_active()) + { + host_memory_watch_use_hashes(); + host_logf(HOST_LOG_INFO, "write tracking: page hashes (ARM translation)"); + } + else if (!config_boolean_or(path, "debug.memory_watch", 1)) + { + host_memory_watch_use_hashes(); + host_logf(HOST_LOG_INFO, "write tracking: page hashes (debug.memory_watch = false)"); + } + else + { + host_logf(HOST_LOG_INFO, "write tracking: page protection"); + } boot = make_boot(&environment); host_logf(HOST_LOG_INFO, "data %s, saves %s", data_root, save_root); host_run_guest_main(boot); diff --git a/port/android/host/host_memory.c b/port/android/host/host_memory.c index 8e5a3e45..3c74519d 100644 --- a/port/android/host/host_memory.c +++ b/port/android/host/host_memory.c @@ -17,10 +17,13 @@ This file also implements guest memory write tracking (the interface of port/linux/src/memory_watch.c): the renderer write-protects the pages behind the textures it caches, and the SIGSEGV handler here records the first write to each. Other faults are reported (with guest-relative addresses) and -passed on to the previous handler. +passed on to the previous handler. Under ARM translation (the x86 emulator) +page faults cannot be caught, and the tracking compares page contents +instead (host_watch_hash.c). */ #include "host.h" +#include "host_watch_hash.h" #include #include @@ -478,6 +481,21 @@ static uint8_t page_protected[WATCH_PAGE_COUNT]; static uint32_t page_generation[WATCH_PAGE_COUNT]; static volatile uint32_t current_generation = 1; static int watch_active; +static int watch_hashing; +static uint8_t page_hash_watched[WATCH_PAGE_COUNT]; +static uint64_t page_hash[WATCH_PAGE_COUNT]; +static uint32_t page_hashed_frame[WATCH_PAGE_COUNT]; +static struct watch_hash page_hashes = +{ + .base = (const uint8_t *)(uintptr_t)HALO_GUEST_WINDOW_BASE, + .page_count = WATCH_PAGE_COUNT, + .watched = page_hash_watched, + .hash = page_hash, + .hashed_frame = page_hashed_frame, + .generation = page_generation, + .current_generation = ¤t_generation, + .frame = 1, +}; static int in_window(uint64_t address) { @@ -601,6 +619,14 @@ void host_memory_watch_initialize(void) watch_active = 1; } +void host_memory_watch_use_hashes(void) +{ + watch_hashing = 1; +} + +/* memory_watch_protect: write-protects the pages of a range, or keeps their +hash under host_memory_watch_use_hashes; a size outside the guest window +is ignored */ void host_memory_watch_protect(uint32_t address, uint32_t size) { uint64_t first, last, page; @@ -611,6 +637,11 @@ void host_memory_watch_protect(uint32_t address, uint32_t size) last = watch_page((uint64_t)address + size - 1); if (last >= WATCH_PAGE_COUNT) last = WATCH_PAGE_COUNT - 1; + if (watch_hashing) + { + watch_hash_protect(&page_hashes, (uint32_t)first, (uint32_t)last); + return; + } for (page = first; page <= last; page++) { if (!page_protected[page]) @@ -626,6 +657,8 @@ uint32_t host_memory_watch_serial(void) return current_generation; } +/* memory_watch_generation: the newest generation of the pages of a range. +Under hashing this hashes the pages again, once a frame. */ uint32_t host_memory_watch_generation(uint32_t address, uint32_t size) { uint64_t first, last, page; @@ -637,6 +670,8 @@ uint32_t host_memory_watch_generation(uint32_t address, uint32_t size) last = watch_page((uint64_t)address + size - 1); if (last >= WATCH_PAGE_COUNT) last = WATCH_PAGE_COUNT - 1; + if (watch_hashing) + return watch_hash_generation(&page_hashes, (uint32_t)first, (uint32_t)last); for (page = first; page <= last; page++) { if (page_generation[page] > newest) @@ -645,11 +680,14 @@ uint32_t host_memory_watch_generation(uint32_t address, uint32_t size) return newest; } +/* memory_watch_prepare_write: unprotects the pages the host itself is about +to write into. Nothing under hashing: the write shows in the next hash and +there is no protection to fault on. */ void host_memory_watch_prepare_write(uint32_t address, uint32_t size) { uint64_t start = address, first, last, page; - if (!watch_active || !size) + if (!watch_active || watch_hashing || !size) return; if (start + size <= HALO_GUEST_WINDOW_BASE || start >= (uint64_t)HALO_GUEST_WINDOW_BASE + HALO_GUEST_WINDOW_SIZE) return; @@ -666,6 +704,8 @@ void host_memory_watch_prepare_write(uint32_t address, uint32_t size) } } +/* memory_watch_forget: the pages of a range were remapped or reprotected, so +treat them as written and unwatched */ void host_memory_watch_forget(uint32_t address, uint32_t size) { uint64_t first, last, page; @@ -676,9 +716,22 @@ void host_memory_watch_forget(uint32_t address, uint32_t size) last = watch_page((uint64_t)address + size - 1); if (last >= WATCH_PAGE_COUNT) last = WATCH_PAGE_COUNT - 1; + if (watch_hashing) + { + watch_hash_forget(&page_hashes, (uint32_t)first, (uint32_t)last); + return; + } for (page = first; page <= last; page++) { page_protected[page] = 0; page_generation[page] = __sync_add_and_fetch(¤t_generation, 1); } } + +/* memory_watch_begin_frame: pages may be hashed again; nothing under page +protection */ +void host_memory_watch_begin_frame(void) +{ + if (watch_hashing) + watch_hash_begin_frame(&page_hashes); +} diff --git a/port/android/host/host_watch_hash.c b/port/android/host/host_watch_hash.c new file mode 100644 index 00000000..7bf2a6fb --- /dev/null +++ b/port/android/host/host_watch_hash.c @@ -0,0 +1,114 @@ +/* +HOST_WATCH_HASH.C + +Write tracking by page contents (host_watch_hash.h). +*/ + +#include "host_watch_hash.h" + +/* +FNV-style mixing in four independent lanes, so that the multiplies of +one lane do not wait for another's. A change of one word changes its +lane's state for good (xor and an odd multiply are both invertible), so +a single-word write always shows. Changes of several words could cancel +each other, which is unlikely for game data. (host_watch_hash.h) +*/ +uint64_t watch_hash_page(const uint8_t *page) +{ + const uint64_t *words = (const uint64_t *)page; + uint64_t lane0 = 0xcbf29ce484222325ULL, lane1 = 0x9e3779b97f4a7c15ULL; + uint64_t lane2 = 0xc2b2ae3d27d4eb4fULL, lane3 = 0x165667b19e3779f9ULL; + uint32_t index; + + for (index = 0; index < WATCH_HASH_PAGE_SIZE / 8; index += 4) + { + lane0 = (lane0 ^ words[index + 0]) * 0x100000001b3ULL; + lane1 = (lane1 ^ words[index + 1]) * 0x100000001b3ULL; + lane2 = (lane2 ^ words[index + 2]) * 0x100000001b3ULL; + lane3 = (lane3 ^ words[index + 3]) * 0x100000001b3ULL; + } + return lane0 ^ ((lane1 << 16) | (lane1 >> 48)) ^ ((lane2 << 32) | (lane2 >> 32)) ^ + ((lane3 << 48) | (lane3 >> 16)); +} + +/* clamps last to the watched memory; 0 when nothing is left of the range */ +static int clamp(const struct watch_hash *watch, uint32_t first, uint32_t *last) +{ + if (first >= watch->page_count || first > *last) + return 0; + if (*last >= watch->page_count) + *last = watch->page_count - 1; + return 1; +} + +static const uint8_t *page_memory(const struct watch_hash *watch, uint32_t page) +{ + return watch->base + (uint64_t)page * WATCH_HASH_PAGE_SIZE; +} + +void watch_hash_protect(struct watch_hash *watch, uint32_t first, uint32_t last) +{ + uint32_t page; + + if (!clamp(watch, first, &last)) + return; + for (page = first; page <= last; page++) + { + if (watch->watched[page]) + continue; + watch->watched[page] = 1; + watch->hash[page] = watch_hash_page(page_memory(watch, page)); + watch->hashed_frame[page] = watch->frame; + } +} + +uint32_t watch_hash_generation(struct watch_hash *watch, uint32_t first, uint32_t last) +{ + uint32_t page, newest = 0; + + if (!clamp(watch, first, &last)) + return 0; + for (page = first; page <= last; page++) + { + if (watch->watched[page] && watch->hashed_frame[page] != watch->frame) + { + uint64_t hash = watch_hash_page(page_memory(watch, page)); + + watch->hashed_frame[page] = watch->frame; + if (hash != watch->hash[page]) + { + watch->hash[page] = hash; + watch->generation[page] = __sync_add_and_fetch(watch->current_generation, 1); + } + } + if (watch->generation[page] > newest) + newest = watch->generation[page]; + } + return newest; +} + +void watch_hash_forget(struct watch_hash *watch, uint32_t first, uint32_t last) +{ + uint32_t page; + + if (!clamp(watch, first, &last)) + return; + for (page = first; page <= last; page++) + { + watch->watched[page] = 0; + watch->generation[page] = __sync_add_and_fetch(watch->current_generation, 1); + } +} + +/* +The serial changes each frame, so a cache that skips generation queries +while the serial stays the same (xbox_textures.c's recent_textures) asks +again once a frame. (host_watch_hash.h) +*/ +void watch_hash_begin_frame(struct watch_hash *watch) +{ + watch->frame++; + if (!watch->frame) + watch->frame = 1; + __sync_add_and_fetch(watch->current_generation, 1); +} diff --git a/port/android/host/host_watch_hash.h b/port/android/host/host_watch_hash.h new file mode 100644 index 00000000..8f0838f3 --- /dev/null +++ b/port/android/host/host_watch_hash.h @@ -0,0 +1,57 @@ +/* +HOST_WATCH_HASH.H + +Write tracking by page contents, for processes that cannot catch their own +page protection faults: the ARM translation of the x86 Android emulator +cannot deliver SIGSEGV to the app, so the protection-based tracking of +host_memory.c would stop the game at its first write to a cached page. + +A watched page's hash is kept. Asking for its generation hashes it again, +at most once a frame (watch_hash_begin_frame), and a different hash counts +as a write. A write is therefore seen in the next frame, not at once. +*/ + +#ifndef HOST_WATCH_HASH_H +#define HOST_WATCH_HASH_H + +#include + +#define WATCH_HASH_PAGE_SIZE 0x1000u + +struct watch_hash +{ + /* the memory of page 0 */ + const uint8_t *base; + uint32_t page_count; + /* page_count entries each */ + uint8_t *watched; + uint64_t *hash; + uint32_t *hashed_frame; + /* shared with the protection-based tracking (host_memory.c) */ + uint32_t *generation; + volatile uint32_t *current_generation; + uint32_t frame; +}; + +/* a hash of one page (WATCH_HASH_PAGE_SIZE bytes, 8-byte aligned) that +changes whenever a word of it changes */ +uint64_t watch_hash_page(const uint8_t *page); + +/* Starts watching pages first to last (inclusive), remembering their hashes. +Pages already watched keep their hash; pages beyond page_count are +ignored. */ +void watch_hash_protect(struct watch_hash *watch, uint32_t first, uint32_t last); + +/* The newest generation among pages first to last (inclusive), after hashing +again the watched ones not yet hashed this frame; a page whose hash +changed gets a new generation. 0 if the range is outside the watched +memory. */ +uint32_t watch_hash_generation(struct watch_hash *watch, uint32_t first, uint32_t last); + +/* The pages were remapped: treat them as written and unwatched. */ +void watch_hash_forget(struct watch_hash *watch, uint32_t first, uint32_t last); + +/* A new frame: pages may be hashed again, and the serial changes. */ +void watch_hash_begin_frame(struct watch_hash *watch); + +#endif diff --git a/port/android/host_imports.list b/port/android/host_imports.list index e7a81fb1..cfa8882f 100644 --- a/port/android/host_imports.list +++ b/port/android/host_imports.list @@ -13,6 +13,7 @@ host_memory_watch_generation host_memory_watch_serial host_memory_watch_prepare_write host_memory_watch_forget +host_memory_watch_begin_frame host_sdl_init host_sdl_set_hint host_sdl_get_error diff --git a/port/android/tests/host_watch_hash_test.c b/port/android/tests/host_watch_hash_test.c new file mode 100644 index 00000000..9b8515eb --- /dev/null +++ b/port/android/tests/host_watch_hash_test.c @@ -0,0 +1,216 @@ +/* +HOST_WATCH_HASH_TEST.C + +Tests of the page-hash write tracking (port/android/host/host_watch_hash.c). +Built and run by tools/test_host_watch_hash.py; exits nonzero on a failure. +*/ + +#include "../host/host_watch_hash.h" + +#include +#include +#include + +#define PAGES 4 + +static int failures; + +#define CHECK(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + failures++; \ + } \ + } while (0) + +struct fixture +{ + uint8_t *memory; + uint8_t watched[PAGES]; + uint64_t hash[PAGES]; + uint32_t hashed_frame[PAGES]; + uint32_t generation[PAGES]; + volatile uint32_t current; + struct watch_hash watch; +}; + +static void setup(struct fixture *f) +{ + memset(f, 0, sizeof(*f)); + f->memory = aligned_alloc(WATCH_HASH_PAGE_SIZE, PAGES * WATCH_HASH_PAGE_SIZE); + memset(f->memory, 0x5a, PAGES * WATCH_HASH_PAGE_SIZE); + f->current = 1; + f->watch.base = f->memory; + f->watch.page_count = PAGES; + f->watch.watched = f->watched; + f->watch.hash = f->hash; + f->watch.hashed_frame = f->hashed_frame; + f->watch.generation = f->generation; + f->watch.current_generation = &f->current; + f->watch.frame = 1; +} + +static void teardown(struct fixture *f) +{ + free(f->memory); +} + +static uint8_t *byte_of(struct fixture *f, uint32_t page, uint32_t offset) +{ + return f->memory + page * WATCH_HASH_PAGE_SIZE + offset; +} + +static void test_unchanged_page_keeps_generation(void) +{ + struct fixture f; + uint32_t before; + + setup(&f); + watch_hash_protect(&f.watch, 0, 0); + before = watch_hash_generation(&f.watch, 0, 0); + watch_hash_begin_frame(&f.watch); + CHECK(watch_hash_generation(&f.watch, 0, 0) == before); + teardown(&f); +} + +static void test_write_is_seen_next_frame(void) +{ + struct fixture f; + uint32_t before, after; + + setup(&f); + watch_hash_protect(&f.watch, 1, 1); + before = watch_hash_generation(&f.watch, 1, 1); + *byte_of(&f, 1, 100) ^= 1; + /* the page was hashed in this frame already */ + CHECK(watch_hash_generation(&f.watch, 1, 1) == before); + watch_hash_begin_frame(&f.watch); + after = watch_hash_generation(&f.watch, 1, 1); + CHECK(after > before); + CHECK(f.current >= after); + teardown(&f); +} + +static void test_any_byte_is_seen(void) +{ + static const uint32_t offsets[] = { 0, 2047, WATCH_HASH_PAGE_SIZE - 1 }; + unsigned index; + + for (index = 0; index < sizeof(offsets) / sizeof(offsets[0]); index++) + { + struct fixture f; + uint32_t before; + + setup(&f); + watch_hash_protect(&f.watch, 2, 2); + watch_hash_begin_frame(&f.watch); + before = watch_hash_generation(&f.watch, 2, 2); + *byte_of(&f, 2, offsets[index]) += 1; + watch_hash_begin_frame(&f.watch); + CHECK(watch_hash_generation(&f.watch, 2, 2) > before); + teardown(&f); + } +} + +static void test_hash_at_most_once_per_frame(void) +{ + struct fixture f; + uint32_t before; + + setup(&f); + watch_hash_protect(&f.watch, 0, 0); + watch_hash_begin_frame(&f.watch); + before = watch_hash_generation(&f.watch, 0, 0); + CHECK(f.hashed_frame[0] == f.watch.frame); + *byte_of(&f, 0, 8) ^= 0xff; + CHECK(watch_hash_generation(&f.watch, 0, 0) == before); + watch_hash_begin_frame(&f.watch); + CHECK(watch_hash_generation(&f.watch, 0, 0) > before); + teardown(&f); +} + +static void test_unwatched_page_is_not_hashed(void) +{ + struct fixture f; + + setup(&f); + *byte_of(&f, 3, 0) ^= 1; + watch_hash_begin_frame(&f.watch); + CHECK(watch_hash_generation(&f.watch, 3, 3) == 0); + CHECK(f.hashed_frame[3] == 0); + teardown(&f); +} + +static void test_forget_unwatches_and_bumps(void) +{ + struct fixture f; + uint32_t before; + + setup(&f); + watch_hash_protect(&f.watch, 2, 2); + before = watch_hash_generation(&f.watch, 2, 2); + watch_hash_forget(&f.watch, 2, 2); + CHECK(f.watched[2] == 0); + CHECK(watch_hash_generation(&f.watch, 2, 2) > before); + teardown(&f); +} + +static void test_range_returns_newest(void) +{ + struct fixture f; + uint32_t newest; + + setup(&f); + watch_hash_protect(&f.watch, 0, 3); + watch_hash_begin_frame(&f.watch); + *byte_of(&f, 3, 5) ^= 1; + watch_hash_begin_frame(&f.watch); + newest = watch_hash_generation(&f.watch, 0, 3); + CHECK(newest == f.generation[3]); + CHECK(watch_hash_generation(&f.watch, 0, 2) < newest); + teardown(&f); +} + +static void test_begin_frame_changes_serial(void) +{ + struct fixture f; + uint32_t serial; + + setup(&f); + serial = f.current; + watch_hash_begin_frame(&f.watch); + CHECK(f.current != serial); + teardown(&f); +} + +static void test_ranges_are_clamped(void) +{ + struct fixture f; + + setup(&f); + watch_hash_protect(&f.watch, 2, 99); + CHECK(f.watched[2] == 1 && f.watched[3] == 1); + CHECK(watch_hash_generation(&f.watch, 5, 9) == 0); + watch_hash_forget(&f.watch, 7, 8); + teardown(&f); +} + +int main(void) +{ + test_unchanged_page_keeps_generation(); + test_write_is_seen_next_frame(); + test_any_byte_is_seen(); + test_hash_at_most_once_per_frame(); + test_unwatched_page_is_not_hashed(); + test_forget_unwatches_and_bumps(); + test_range_returns_newest(); + test_begin_frame_changes_serial(); + test_ranges_are_clamped(); + if (failures) + printf("%d failures\n", failures); + else + printf("all tests passed\n"); + return failures != 0; +} diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 912e3f1a..68d9eda0 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -4718,6 +4718,7 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat x, y + height, x + width, y, GL_COLOR_BUFFER_BIT, GL_LINEAR); platform_video_swap(); xgpu_gl_state_invalidate(); + memory_watch_begin_frame(); xgpu_texture_cache_begin_frame(); #ifdef HALO_ANDROID if (xgpu_capabilities.atomic_counters) diff --git a/port/linux/src/memory_watch.c b/port/linux/src/memory_watch.c index 843022cd..89105cd2 100644 --- a/port/linux/src/memory_watch.c +++ b/port/linux/src/memory_watch.c @@ -223,3 +223,8 @@ void memory_watch_forget(void *address, unsigned long size) page_generation[page] = __sync_add_and_fetch(¤t_generation, 1); } } + +/* page protection sees each write at once: nothing to do per frame */ +void memory_watch_begin_frame(void) +{ +} diff --git a/port/linux/src/platform.h b/port/linux/src/platform.h index 504cbea2..e1bf6cfb 100644 --- a/port/linux/src/platform.h +++ b/port/linux/src/platform.h @@ -162,6 +162,10 @@ unsigned long memory_watch_serial(void); void memory_watch_prepare_write(void *address, unsigned long size); /* the range was remapped or reprotected: treat it as written and unwatched */ void memory_watch_forget(void *address, unsigned long size); +/* a new frame starts (Present): tracking that compares page contents +(Android under ARM translation, host_watch_hash.h) hashes a page at most +once a frame, so it sees a write in the next frame, not at once */ +void memory_watch_begin_frame(void); /* ---------- time */ diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 2d0d5f58..697147f0 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -59,6 +59,7 @@ struct config_setting enum config_type type; /* as it is written in the file */ const char *default_value; + /* NULL: none, for a setting the Android app reads from the file itself */ const char *environment; enum config_environment environment_style; unsigned platforms; @@ -451,6 +452,11 @@ static const struct config_setting config_settings[] = { "debug.sample_seconds", _config_real, "0.0", "HALO_SAMPLE", _environment_value, _platform_android, "Log where every game thread is this often, in seconds (read by the\n" "app, port/android/host/host_debug.c); 0 never." }, + { "debug.memory_watch", _config_boolean, "true", NULL, _environment_value, _platform_android, + "Notice the game's writes to cached textures and vertices by page\n" + "protection; false compares page contents once a frame instead, which is\n" + "slower. Under ARM translation (the x86 emulator) the app always compares\n" + "contents. Read by the app from the file (port/android/host/host_main.c)." }, }; #define NUMBER_OF_CONFIG_SETTINGS (sizeof(config_settings) / sizeof(config_settings[0])) @@ -940,7 +946,7 @@ static void config_load(void) for (index = 0; index < NUMBER_OF_CONFIG_SETTINGS; index++) { const struct config_setting *setting = &config_settings[index]; - const char *environment = getenv(setting->environment); + const char *environment = setting->environment ? getenv(setting->environment) : NULL; if (!environment) continue; diff --git a/port/windows/src/win32_memory_watch.c b/port/windows/src/win32_memory_watch.c index 0cbafe3e..f85faae3 100644 --- a/port/windows/src/win32_memory_watch.c +++ b/port/windows/src/win32_memory_watch.c @@ -155,3 +155,8 @@ void memory_watch_forget(void *address, unsigned long size) page_generation[page] = InterlockedIncrement(¤t_generation); } } + +/* page protection sees each write at once: nothing to do per frame */ +void memory_watch_begin_frame(void) +{ +} diff --git a/tools/test_host_watch_hash.py b/tools/test_host_watch_hash.py new file mode 100644 index 00000000..c965e711 --- /dev/null +++ b/tools/test_host_watch_hash.py @@ -0,0 +1,34 @@ +"""Runs the C tests of the Android host's page-hash write tracking +(port/android/host/host_watch_hash.c) with the build machine's C compiler. +The unit has no Android dependency, so it compiles anywhere.""" +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent + + +def compiler(): + for name in ("clang", "cc", "gcc"): + path = shutil.which(name) + if path: + return path + return None + + +def test_host_watch_hash(tmp_path): + cc = compiler() + if not cc: + pytest.skip("no C compiler") + binary = tmp_path / "host_watch_hash_test" + subprocess.run( + [cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-O1", + str(ROOT / "port" / "android" / "tests" / "host_watch_hash_test.c"), + str(ROOT / "port" / "android" / "host" / "host_watch_hash.c"), + "-o", str(binary)], + check=True, + ) + result = subprocess.run([str(binary)], capture_output=True, text=True) + assert result.returncode == 0, result.stdout + result.stderr From 0ca45fb4e74a789bd669fce59c87a9b183b6f8d0 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Wed, 30 Sep 2026 22:24:06 +0200 Subject: [PATCH 049/187] Build the Android port from WSL --- .gitattributes | 4 ++ port/android/README.md | 36 ++++++++++++++- tools/test_wsl_sync_tree.py | 87 +++++++++++++++++++++++++++++++++++++ tools/wsl_build_android.sh | 32 ++++++++++++++ tools/wsl_desktop_check.sh | 19 ++++++++ tools/wsl_setup_android.sh | 47 ++++++++++++++++++++ tools/wsl_sync_tree.sh | 80 ++++++++++++++++++++++++++++++++++ 7 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 .gitattributes create mode 100644 tools/test_wsl_sync_tree.py create mode 100644 tools/wsl_build_android.sh create mode 100644 tools/wsl_desktop_check.sh create mode 100644 tools/wsl_setup_android.sh create mode 100644 tools/wsl_sync_tree.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..3773b563 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Files that a Linux shell runs directly must keep LF line endings in a +# Windows checkout (core.autocrlf): a CRLF shebang line does not start. +*.sh text eol=lf +port/android/gradlew text eol=lf diff --git a/port/android/README.md b/port/android/README.md index aa8308ee..771958fa 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -31,6 +31,36 @@ ninja) and these items: 1.2.5 and SDL 3.4.16 to `build/android/third_party`. Gradle downloads the Android Gradle Plugin. +On Windows, build in WSL2 (Ubuntu 24.04). The Android build does not run +on Windows itself. Enter +`wsl -d Ubuntu -- bash tools/wsl_setup_android.sh` one time: it installs +the tools and the Android SDK and NDK in WSL (apt asks for your +password). Then enter +`wsl -d Ubuntu -- bash tools/wsl_build_android.sh` for each build. The +script copies the repository into WSL (`~/halo-build/src`) with LF line +endings (a Windows checkout has CRLF line endings, which the build's text +tools do not accept), builds there, and puts the APK in +`dist/android/app-debug.apk`. Only the changed files are copied, so the +next build is incremental. Install the APK with the adb of Windows. + +A build that you make yourself has a different signature from the builds +of GitHub Actions. Android installs it only after you remove the app, and +removing the app deletes its data folder (`maps/`, `save/` and +`config.toml`). Thus a change between the builds of GitHub Actions and your +own builds loses the saved games, unless you copy them first (below). +Your own builds all have the same signature (the debug key of WSL, +`~/.android`), so they install over each other and keep the data +(`adb install -r`). + +To keep the saved games before you remove the app, enter +`adb pull /sdcard/Android/data/com.halo.decomp/files/save/u` (the profiles). +After the new install, before the first start of the app, enter +`adb push u /sdcard/Android/data/com.halo.decomp/files/save/u`. The app reads +and overwrites what adb pushes. adb cannot add files to `save/u` or `save/z` once the app made them, or +change files that the app wrote, so push before the first start. adb cannot read +some files that the app writes (the `blam.lst` files of `save/z`), so `adb pull` +of the whole `save` folder stops there. + ## Build and install the app 1. Go to the root folder of the repository. @@ -69,7 +99,9 @@ To install the data from a computer: | Settings | `config.toml` | To make a copy of the saved games, enter -`adb pull /sdcard/Android/data/com.halo.decomp/files/save`. +`adb pull /sdcard/Android/data/com.halo.decomp/files/save/u` (the profiles). +An `adb pull` of the whole `save` folder stops at the `blam.lst` files of +`save/z`, which adb cannot read. ## Controls @@ -161,7 +193,7 @@ signature. GitHub Actions signs each build with the key in the `ANDROID_KEYSTORE_BASE64` and `ANDROID_KEYSTORE_PASSWORD` secrets of the repository. If you installed a build that has a different signature, remove that build before you install a new build. Removing the app deletes its data -folder: first make a copy of `maps/` and `save/`. +folder: first make a copy of `maps/` and `save/u`. ## Widescreen diff --git a/tools/test_wsl_sync_tree.py b/tools/test_wsl_sync_tree.py new file mode 100644 index 00000000..84a4ee89 --- /dev/null +++ b/tools/test_wsl_sync_tree.py @@ -0,0 +1,87 @@ +"""Tests of tools/wsl_sync_tree.sh, which copies a Windows working tree into +WSL for the Android build (tools/wsl_build_android.sh). Needs bash and +rsync, so it runs on Linux (WSL) only.""" +import os +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent +SCRIPT = ROOT / "tools" / "wsl_sync_tree.sh" + +pytestmark = pytest.mark.skipif( + os.name != "posix" or not shutil.which("rsync") or not shutil.which("bash"), + reason="needs bash and rsync (Linux)") + + +def sync(source, mirror, work): + return subprocess.run(["bash", str(SCRIPT), str(source), str(mirror), str(work)], + capture_output=True, text=True) + + +@pytest.fixture +def trees(tmp_path): + source, mirror, work = tmp_path / "source", tmp_path / "mirror", tmp_path / "work" + (source / "src").mkdir(parents=True) + (source / "src" / "a.c").write_bytes(b"int a;\r\n") + (source / "src" / "b.c").write_bytes(b"int b;\n") + return source, mirror, work + + +def test_first_sync_copies_with_lf(trees): + source, mirror, work = trees + result = sync(source, mirror, work) + assert result.returncode == 0, result.stderr + assert (work / "src" / "a.c").read_bytes() == b"int a;\n" + assert (work / "src" / "b.c").read_bytes() == b"int b;\n" + + +def test_unchanged_sync_copies_nothing(trees): + source, mirror, work = trees + sync(source, mirror, work) + result = sync(source, mirror, work) + assert result.returncode == 0, result.stderr + assert "copied 0 changed files" in result.stdout + + +def test_deleted_file_goes_away(trees): + source, mirror, work = trees + sync(source, mirror, work) + (source / "src" / "b.c").unlink() + result = sync(source, mirror, work) + assert result.returncode == 0, result.stderr + assert not (work / "src" / "b.c").exists() + + +def test_change_after_failed_sync_still_arrives(trees): + source, mirror, work = trees + sync(source, mirror, work) + (source / "src" / "a.c").write_bytes(b"int a2;\r\n") + # the copy into the build tree fails after rsync has updated the mirror + (work / "src" / "a.c").chmod(0o000) + (work / "src").chmod(0o500) + failed = sync(source, mirror, work) + (work / "src").chmod(0o755) + (work / "src" / "a.c").chmod(0o644) + assert failed.returncode != 0 + result = sync(source, mirror, work) + assert result.returncode == 0, result.stderr + assert (work / "src" / "a.c").read_bytes() == b"int a2;\n" + + +def test_deletion_survives_a_failed_sync(trees): + source, mirror, work = trees + sync(source, mirror, work) + (source / "src" / "b.c").unlink() + # rsync deletes b.c from the mirror, then fails on the unreadable new file + (source / "src" / "c.c").write_bytes(b"int c;\n") + (source / "src" / "c.c").chmod(0o000) + failed = sync(source, mirror, work) + (source / "src" / "c.c").chmod(0o644) + assert failed.returncode != 0 + result = sync(source, mirror, work) + assert result.returncode == 0, result.stderr + assert not (work / "src" / "b.c").exists() + assert (work / "src" / "c.c").read_bytes() == b"int c;\n" diff --git a/tools/wsl_build_android.sh b/tools/wsl_build_android.sh new file mode 100644 index 00000000..f8a6372a --- /dev/null +++ b/tools/wsl_build_android.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Builds the Android APK in WSL from a Windows working tree. The Android +# build needs Linux (the NDK paths and the shell commands of its ninja +# rules), and it is faster on the Linux file system, so the sources are +# copied to ~/halo-build/src first (tools/wsl_sync_tree.sh: LF line +# endings, only the changed files). build/ stays there, so builds are +# incremental. The APK is copied back to dist/android/. +# +# wsl -d Ubuntu -- bash /mnt/c/path/to/the/repository/tools/wsl_build_android.sh [--release] +set -euo pipefail + +# (not from ~/.profile: under set -u, Ubuntu's .bashrc stops at its unset PS1) +export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}" +export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/27.2.12479018}" +SOURCE="$(cd "$(dirname "$0")/.." && pwd)" +WORK="$HOME/halo-build/src" +# (the APK is signed with the debug key in both flavors: --release only +# configures an optimised build, and names the file app-release.apk) +FLAVOR=debug +CONFIGURE=() +if [ "${1:-}" = "--release" ]; then + FLAVOR=release + CONFIGURE+=(--release) +fi + +bash "$SOURCE/tools/wsl_sync_tree.sh" "$SOURCE" "$HOME/halo-build/mirror" "$WORK" +cd "$WORK" +python3 configure.py "${CONFIGURE[@]}" +ninja android_apk +mkdir -p "$SOURCE/dist/android" +cp "port/android/app/build/outputs/apk/debug/app-debug.apk" "$SOURCE/dist/android/app-$FLAVOR.apk" +echo "APK: dist/android/app-$FLAVOR.apk" diff --git a/tools/wsl_desktop_check.sh b/tools/wsl_desktop_check.sh new file mode 100644 index 00000000..4a442ad6 --- /dev/null +++ b/tools/wsl_desktop_check.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Compiles the given platform files (port/linux/src) as the Android guest +# build does, but without HALO_ANDROID, syntax only: the desktop branches of +# the shared code. Warnings are off, but a call of an undeclared function is +# still an error. Only files that the guest build compiles can be checked +# (not memory_watch.c or posix_*). Run in ~/halo-build/src after a build. +set -uo pipefail +cd "$HOME/halo-build/src" +status=0 +for file in "$@"; do + object="build/android/guest/obj/port/linux/src/${file%.c}.o" + command=$(ninja -t commands "$object" | tail -1) + if [ -z "$command" ]; then echo "no command for $object"; status=1; continue; fi + # (the command chains the compile with the assembly steps: keep the compile) + command=$(echo "$command" | sed -E 's/ && .*//; s/ -DHALO_ANDROID(=[^ ]*)?//g; s/ -w( |$)/ /g; s/ -o [^ ]+//; s/ -MMD//; s/ -MF [^ ]+//') + # (__i386__: the prefix header accepts only the 32-bit x86 or Android targets) + if eval "$command -D__i386__ -fsyntax-only -w -Werror=implicit-function-declaration"; then echo "ok $file"; else echo "FAILED $file"; status=1; fi +done +exit $status diff --git a/tools/wsl_setup_android.sh b/tools/wsl_setup_android.sh new file mode 100644 index 00000000..daaea364 --- /dev/null +++ b/tools/wsl_setup_android.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Installs what the Android build needs on Ubuntu 24.04 (WSL2 or native), +# as the GitHub runner image has it (.github/workflows/build.yml): the +# compilers and tools from apt, and the Android SDK command-line tools, +# platform 35, build-tools and a pinned NDK in ~/Android/Sdk. Safe to run +# again: it installs only what is missing. +# +# bash tools/wsl_setup_android.sh +set -euo pipefail + +SDK="${ANDROID_HOME:-$HOME/Android/Sdk}" +TOOLS_ZIP=commandlinetools-linux-11076708_latest.zip +NDK_VERSION=27.2.12479018 +PACKAGES=(clang lld ninja-build cmake openjdk-17-jdk-headless curl unzip rsync git python3 python3-pytest) + +missing=() +for package in "${PACKAGES[@]}"; do + dpkg -s "$package" >/dev/null 2>&1 || missing+=("$package") +done +if [ ${#missing[@]} -gt 0 ]; then + echo "installing: ${missing[*]} (sudo asks for your password)" + sudo apt-get update + sudo apt-get install -y "${missing[@]}" +fi + +if [ ! -x "$SDK/cmdline-tools/latest/bin/sdkmanager" ]; then + mkdir -p "$SDK/cmdline-tools" + temporary=$(mktemp -d) + curl -fL -o "$temporary/tools.zip" "https://dl.google.com/android/repository/$TOOLS_ZIP" + unzip -q "$temporary/tools.zip" -d "$temporary" + rm -rf "$SDK/cmdline-tools/latest" + mv "$temporary/cmdline-tools" "$SDK/cmdline-tools/latest" + rm -rf "$temporary" +fi + +sdkmanager="$SDK/cmdline-tools/latest/bin/sdkmanager" +yes | "$sdkmanager" --sdk_root="$SDK" --licenses >/dev/null || true +"$sdkmanager" --sdk_root="$SDK" --install "platforms;android-35" "build-tools;35.0.0" "ndk;$NDK_VERSION" "platform-tools" + +profile="$HOME/.profile" +if ! grep -q 'ANDROID_HOME=' "$profile" 2>/dev/null; then + { + echo "export ANDROID_HOME=\"$SDK\"" + echo "export ANDROID_NDK_HOME=\"$SDK/ndk/$NDK_VERSION\"" + } >> "$profile" +fi +echo "Android build tools are ready (SDK $SDK, NDK $NDK_VERSION)." diff --git a/tools/wsl_sync_tree.sh b/tools/wsl_sync_tree.sh new file mode 100644 index 00000000..ec677ef0 --- /dev/null +++ b/tools/wsl_sync_tree.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Copies a Windows working tree into a Linux build tree for the Android +# build (tools/wsl_build_android.sh): +# +# bash tools/wsl_sync_tree.sh SOURCE MIRROR WORK +# +# A Windows checkout (core.autocrlf) has CRLF line endings, which break the +# build's text tools (musl's mkalltypes.sed, for one). So SOURCE is first +# mirrored as it is (MIRROR), and each file that the mirror gets new is +# copied on to WORK with LF line endings and the mirror's time. Unchanged +# files are not touched, so ninja rebuilds only what changed. --delete +# matters: the build globs *.c, so a file deleted on Windows must also go +# away here. WORK's build/ is not in SOURCE and stays. +# +# The mirror is updated before WORK is. If a run stops between the two +# (rsync fails because a file changed under it, a copy fails, Ctrl-C), the +# mirror already has files that WORK lacks, and rsync would not report +# them again. So a marker stays while a sync is under way; when the next +# run finds it, it rebuilds the mirror, and every file is compared again. +# The mirror's times are the source's, so the files that did not change +# get the same time in WORK again, and ninja does not rebuild them. +set -euo pipefail + +SOURCE="$1" +MIRROR="$2" +WORK="$3" +PENDING="$MIRROR.pending" + +# what the sync leaves alone (not source, or local data that may be large) +EXCLUDES=( + --exclude=/build/ --exclude=/dist/ --exclude=/assets/ --exclude=/original/ --exclude=/xbox/ --exclude=/.git/ + --exclude=/port/android/app/build/ --exclude=/port/android/build/ --exclude=/port/android/.gradle/ + --exclude=/port/android/local.properties + --exclude=/build.ninja --exclude='/.ninja_*' +) + +RECOVER=0 +if [ -e "$PENDING" ]; then + echo "the last sync did not finish: copying the whole tree again" + rm -rf "$MIRROR" + RECOVER=1 +fi +mkdir -p "$MIRROR" "$WORK" +touch "$PENDING" +changes=$(mktemp) +trap 'rm -f "$changes"' EXIT +rsync -a --delete --out-format='%i %n' "${EXCLUDES[@]}" "$SOURCE/" "$MIRROR/" > "$changes" +if [ "$RECOVER" = 1 ]; then + # the deletion that the failed run made in the mirror is not reported + # again, so remove from WORK what the mirror no longer has + rsync -r --delete --existing --ignore-existing "${EXCLUDES[@]}" "$MIRROR/" "$WORK/" +fi + +# %i is 11 characters, then a space, then the path +copied=0 +while IFS= read -r line; do + item="${line:0:11}" + path="${line:12}" + case "$item" in + '*deleting'*) + rm -rf "${WORK:?}/$path" + ;; + cd*) + mkdir -p "$WORK/$path" + ;; + '>f'*) + mkdir -p "$(dirname "$WORK/$path")" + if grep -Iq . "$MIRROR/$path" && grep -q $'\r' "$MIRROR/$path"; then + sed 's/\r$//' "$MIRROR/$path" > "$WORK/$path" + else + cp "$MIRROR/$path" "$WORK/$path" + fi + chmod --reference="$MIRROR/$path" "$WORK/$path" + touch -r "$MIRROR/$path" "$WORK/$path" + copied=$((copied + 1)) + ;; + esac +done < "$changes" +rm -f "$PENDING" +echo "copied $copied changed files to $WORK" From a0761c4fd1911cfffcf39c2036a9ba196639ffc6 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:31:44 -0400 Subject: [PATCH 050/187] Read an SMBIOS structure only within the firmware table posix_hardware_id_source (Windows) checked that a structure's four-byte header was within the firmware table, not its formatted part: a table cut short in a system information structure (type 1) had its UUID read from up to 20 bytes past the table's end, beyond the buffer. A structure whose formatted part does not fit now ends the walk. --- port/windows/src/win32_p2p.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/port/windows/src/win32_p2p.c b/port/windows/src/win32_p2p.c index f76a6844..647061d1 100644 --- a/port/windows/src/win32_p2p.c +++ b/port/windows/src/win32_p2p.c @@ -195,7 +195,8 @@ int posix_hardware_id_source(char *text, int size) BYTE *structure = table + 8; BYTE *end = table + 8 + (length < table_size - 8 ? length : table_size - 8); - while (structure + 4 <= end && structure[1] >= 4) + /* (each structure's formatted part within the table) */ + while (structure + 4 <= end && structure[1] >= 4 && structure[1] <= end - structure) { BYTE *strings = structure + structure[1]; From 2b89960b0af32ea18fe8ea74bc7f715c3fed6286 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:31:44 -0400 Subject: [PATCH 051/187] Fail a download whose chunk size is not a number read_body took strtoull's answer for a chunk's size line, and 0, the last chunk's size, is also what it answers for a line that is not a number. A broken or cut response then ended as a complete download, cut short (the update's zip then failed its check, or an update check's JSON failed to parse). A size line without a hexadecimal number is now an error. --- port/linux/src/posix_update.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/port/linux/src/posix_update.c b/port/linux/src/posix_update.c index f7fabc96..ca411c8e 100644 --- a/port/linux/src/posix_update.c +++ b/port/linux/src/posix_update.c @@ -340,11 +340,19 @@ static int read_body(struct connection *connection, struct download *download, i for (;;) { char line[128]; + char *after; unsigned long long remaining; if (!connection_read_line(connection, line, sizeof(line))) return 0; - remaining = strtoull(line, NULL, 16); + /* (a size in hexadecimal, perhaps with extensions after a ";": a + line without one is no chunk, and not the last) */ + remaining = strtoull(line, &after, 16); + if (after == line || (*after && *after != ';' && *after != ' ' && *after != '\t' && + *after != '\r' && *after != '\n')) + { + return 0; + } if (!remaining) return 1; while (remaining) From cc820fd8467ac79b044469aa381045b911a740ab Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:29:04 -0400 Subject: [PATCH 052/187] Switches such as HALO_NO_VSYNC=0 no longer turn their setting on An environment variable that only had to be set took effect whatever its value, so HALO_NO_VSYNC=0 turned vsync off, HALO_NO_AUDIO=0 turned audio off and HALO_MOUSE_INVERT=false inverted the mouse. Such a variable that is empty, 0, false, no or off now counts as not set, as a boolean setting's value already reads. --- port/linux/src/port_config.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 2d0d5f58..5a60b6f2 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -37,9 +37,9 @@ enum config_environment /* the variable's text is the value ("0", "false", "no" and "off" are false for a boolean) */ _environment_value, - /* the variable being set at all makes it true */ + /* the variable being set (not empty, "0", "false", "no" or "off") makes it true */ _environment_set_is_true, - /* the variable being set at all makes it false */ + /* the variable being set (not empty, "0", "false", "no" or "off") makes it false */ _environment_set_is_false, }; @@ -761,6 +761,13 @@ static int config_text_is_false(const char *text) return !strcmp(lower, "0") || !strcmp(lower, "false") || !strcmp(lower, "no") || !strcmp(lower, "off"); } +/* whether a variable that only has to be set (_environment_set_is_true or +_environment_set_is_false) is: empty, "0", "false", "no" or "off" is not */ +static int config_environment_set(const char *text) +{ + return text[0] && !config_text_is_false(text); +} + static void config_set_from_text(struct config_value *value, enum config_type type, const char *text) { switch (type) @@ -942,7 +949,7 @@ static void config_load(void) const struct config_setting *setting = &config_settings[index]; const char *environment = getenv(setting->environment); - if (!environment) + if (!environment || (setting->environment_style != _environment_value && !config_environment_set(environment))) continue; switch (setting->environment_style) { From 9fa925c3b0f3b4ce8083609179e0f16edfad29d7 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:29:26 -0400 Subject: [PATCH 053/187] Join games on maps whose names have [ ] ( ) or + A client refused a host's map name with any character outside letters, digits and _ - . space and backslash, so a game on a Custom Edition map named like [H2]_Lockout could not be joined. These five characters mean nothing to a path and are now accepted too. --- source/networking/network_client_manager.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source/networking/network_client_manager.c b/source/networking/network_client_manager.c index 83dc585c..48be9799 100644 --- a/source/networking/network_client_manager.c +++ b/source/networking/network_client_manager.c @@ -2341,7 +2341,8 @@ static boolean network_game_client_map_name_is_valid( { /* (a scenario's tag path, of which the cache takes the name after the last backslash: letters, digits and a few more, none that a path reads - otherwise) */ + otherwise; the [ ] ( ) + that Custom Edition maps' names use, as in + [H2]_Lockout, too) */ char const *character; char const *leaf; @@ -2351,7 +2352,8 @@ static boolean network_game_client_map_name_is_valid( { if (!((*character >= 'a' && *character <= 'z') || (*character >= 'A' && *character <= 'Z') || (*character >= '0' && *character <= '9') || *character == '_' || *character == '-' || - *character == '.' || *character == ' ' || *character == '\\')) + *character == '.' || *character == ' ' || *character == '\\' || + *character == '[' || *character == ']' || *character == '(' || *character == ')' || *character == '+')) { return FALSE; } From 5aad51ac184359fa4bd79318961a9e4c9f910785 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:30:56 -0400 Subject: [PATCH 054/187] Refuse a host's map name that is a Windows device name A host's map name of con, nul, com1 and the like made a client on Windows open that device as the map, which could hang it. Such names are now refused with the other invalid map names. --- source/networking/network_client_manager.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/source/networking/network_client_manager.c b/source/networking/network_client_manager.c index 83dc585c..2f4faae0 100644 --- a/source/networking/network_client_manager.c +++ b/source/networking/network_client_manager.c @@ -2360,6 +2360,22 @@ static boolean network_game_client_map_name_is_valid( return FALSE; leaf = strrchr(map_name, '\\'); leaf = leaf ? leaf + 1 : map_name; + /* port: and not one of Windows's devices (con, nul, com1...), whatever + follows it: maps\com1.map opens the serial port there */ + { + static char const *const devices[] = { "con", "prn", "aux", "nul", "com", "lpt" }; + long stem = (long)strcspn(leaf, ". "); + short device; + + for (device = 0; device < (short)NUMBEROF(devices); device++) + { + if (!_strnicmp(leaf, devices[device], 3) && + (stem == 3 || (device >= 4 && stem == 4 && leaf[3] >= '0' && leaf[3] <= '9'))) + { + return FALSE; + } + } + } return *leaf && leaf[strspn(leaf, ". ")] != 0; } From 22bb651e68fd8f52cd6d46b520f0da0bde11792e Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:31:13 -0400 Subject: [PATCH 055/187] Write the last multiplayer map's name from a buffer of the size written saved_game_file_remember_last_used_multiplayer_map wrote 256 bytes from the caller's name, which the menus' map list keeps in 64, so whatever followed it in memory went into lastmpmp.txt. The name is now copied into a zeroed buffer of the written size first. --- source/saved games/saved_game_files.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/saved games/saved_game_files.c b/source/saved games/saved_game_files.c index c179191e..2650c85f 100644 --- a/source/saved games/saved_game_files.c +++ b/source/saved games/saved_game_files.c @@ -851,17 +851,22 @@ void saved_game_file_remember_last_used_multiplayer_map( char const *map_name) { struct file_reference file; + /* port: the name in a buffer of the size written (callers' may be + smaller: the menus' map list's are 64 bytes), the rest zeros */ + char name[MAXIMUM_FILENAME_LENGTH+1]; match_assert( "c:\\halo\\SOURCE\\saved games\\saved_game_files.c", 1207, map_name); + csmemset(name, 0, sizeof(name)); + csstrncpy(name, map_name, sizeof(name) - 1); if (file_reference_create_from_path(&file, "z:\\lastmpmp.txt", FALSE) && file_create(&file) && file_open(&file, FLAG(_permission_write_bit))) { - if (!file_write(&file, MAXIMUM_FILENAME_LENGTH+1, map_name)) + if (!file_write(&file, MAXIMUM_FILENAME_LENGTH+1, name)) { error(_error_silent, "failed to write to '%s'", "z:\\lastmpmp.txt"); } From 6f0f2992cb5355d13c5afd5f9a2b0da666d802ee Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:33:20 -0400 Subject: [PATCH 056/187] Widgets: a game map's run none of the main menu's profile functions A widget's event handlers name their function by its index in the table, which nothing checked, so a game map's widget could run the main menu's profile functions (deleting and saving player and playlist profiles, the demos) and the port's own menu functions (writing config.toml, quitting, connecting), on its created event too. The shipped maps' widgets run none of these. Now only the port's own menu tags run the port's functions, and only the main menu runs its profile functions; a refusal is logged once. --- .../ui_widget_event_handler_functions.c | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/source/interface/ui_widget_event_handler_functions.c b/source/interface/ui_widget_event_handler_functions.c index 41d3e288..3e9ebbc0 100644 --- a/source/interface/ui_widget_event_handler_functions.c +++ b/source/interface/ui_widget_event_handler_functions.c @@ -3386,6 +3386,57 @@ static boolean multiplayer_profiles_list_initialize( return TRUE; } +/* port: whether a widget of a map's may not run an event handler's function. +A widget's handlers name the functions they run by their index in the +function table, which nothing checks, so a game map's widget could run the +main menu's functions (deleting player and playlist profiles, saving them, +running the demos) and the port's own (writing config.toml, quitting, +connecting), on its created event too, as its screen opens. The shipped +game maps' widgets (their pause screens) run none of these: the port's own +menus' tags (pc_menu_tag) may run the port's, and the main menu (ui.map) +the main menu's. Logged once */ +static boolean ui_widget_function_denied( + struct widget_instance *widget, + word function_index) +{ + extern boolean pc_menu_tag(long tag_index); + static short const main_menu_functions[] = + { + 41, /* mp profile change name */ + 60, /* mp profile save changes */ + 64, 65, 66, 67, /* player profile begin and end editing, change name, save changes */ + 68, 69, 70, 71, /* player profile controller settings */ + 74, 75, 76, 77, 78, 79, 80, /* profile deletion and creation */ + 86, 87, /* the demos */ + }; + static boolean logged = FALSE; + boolean denied = FALSE; + short index; + + if (pc_menu_tag(widget->definition_tag_index)) + return FALSE; + if (function_index >= PC_MENU_FUNCTION_BASE && function_index < 0x8000) + { + denied = TRUE; + } + else if (!main_menu_is_active()) + { + for (index = 0; index < (short)NUMBEROF(main_menu_functions); index++) + { + if (function_index == (word)main_menu_functions[index]) + denied = TRUE; + } + } + if (denied && !logged) + { + logged = TRUE; + error(_error_silent, "a map's widget may not run event handler function %d; it is skipped", + function_index); + } + + return denied; +} + boolean ui_widget_event_handler_function_invoke( struct widget_instance *widget, struct event_record *event, @@ -3397,6 +3448,10 @@ boolean ui_widget_event_handler_function_invoke( match_vassert("c:\\halo\\SOURCE\\interface\\ui_widget_event_handler_functions.c", 478, widget != NULL && widget_deleted != NULL, "(widget != NULL) && (widget_deleted != NULL)"); + /* port: a map's own widgets (not the menus' tags the port adds) may not + run what changes the player's files or settings: ui_widget_function_denied */ + if (ui_widget_function_denied(widget, function_index)) + return TRUE; /* port: the menus' own functions (port/linux/game/menu_functions.c) */ if (function_index >= PC_MENU_FUNCTION_BASE && function_index < 0x8000) { From 99257442f7f30646751e1b155ab3be49100b05a0 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:33:28 -0400 Subject: [PATCH 057/187] Wide printf: bounded conversion specifications wide_format copied a conversion's flags and width digits into a fixed buffer as it read them, with no bound, and a large width padded for a long time. Formats include map text (ui strings, HUD messages), so the specification is now built from the parsed values, width and precision are capped at 4096, and a count conversion's argument is passed over rather than written. --- port/linux/src/msvc_wide.c | 113 +++++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 50 deletions(-) diff --git a/port/linux/src/msvc_wide.c b/port/linux/src/msvc_wide.c index 8d5fa31d..358a34fa 100644 --- a/port/linux/src/msvc_wide.c +++ b/port/linux/src/msvc_wide.c @@ -520,6 +520,10 @@ long msvc_wtol(const wchar_t *string) /* ---------- formatted output */ +/* the widest field and the longest precision a conversion is given (a +format's own, or an argument's) */ +#define WIDE_FORMAT_MAXIMUM_WIDTH 4096L + struct wide_output { wchar_t *buffer; @@ -599,54 +603,32 @@ static int wide_format(struct wide_output *output, const wchar_t *format, va_lis continue; } - specification[specification_length++] = '%'; - /* flags */ - while (*format == '-' || *format == '+' || *format == ' ' || *format == '#' || *format == '0') + /* (the specification handed to snprintf below is built from what + was parsed, bounded: a format may be a map's text, ui strings and + HUD messages among them, so its flags and digits may be any number, + and its width and precision are capped at WIDE_FORMAT_MAXIMUM_WIDTH) */ { - if (*format == '-') - left = TRUE; - specification[specification_length++] = (char)*format++; - } - /* width */ - if (*format == '*') - { - width = va_arg(arguments, int); - if (width < 0) + char flags[8]; + size_t flag_count = 0; + + while (*format == '-' || *format == '+' || *format == ' ' || *format == '#' || *format == '0') { - left = TRUE; - width = -width; - specification[specification_length++] = '-'; + if (*format == '-') + left = TRUE; + if (flag_count < sizeof(flags) - 1) + flags[flag_count++] = (char)*format; + format++; } - specification_length += (size_t)snprintf(specification + specification_length, - sizeof(specification) - specification_length, "%ld", width); - format++; - } - else - { - while (*format >= '0' && *format <= '9') - { - width = width * 10 + (*format - '0'); - specification[specification_length++] = (char)*format++; - } - } - /* precision */ - if (*format == '.') - { - precision = 0; - specification[specification_length++] = '.'; - format++; + /* width */ if (*format == '*') { - precision = va_arg(arguments, int); - if (precision < 0) + width = va_arg(arguments, int); + if (width < 0) { - precision = -1; - specification_length--; - } - else - { - specification_length += (size_t)snprintf(specification + specification_length, - sizeof(specification) - specification_length, "%ld", precision); + left = TRUE; + width = width < -WIDE_FORMAT_MAXIMUM_WIDTH ? WIDE_FORMAT_MAXIMUM_WIDTH : -width; + if (flag_count < sizeof(flags) - 1) + flags[flag_count++] = '-'; } format++; } @@ -654,10 +636,44 @@ static int wide_format(struct wide_output *output, const wchar_t *format, va_lis { while (*format >= '0' && *format <= '9') { - precision = precision * 10 + (*format - '0'); - specification[specification_length++] = (char)*format++; + if (width <= WIDE_FORMAT_MAXIMUM_WIDTH) + width = width * 10 + (*format - '0'); + format++; } } + if (width > WIDE_FORMAT_MAXIMUM_WIDTH) + width = WIDE_FORMAT_MAXIMUM_WIDTH; + flags[flag_count] = '\0'; + /* precision */ + if (*format == '.') + { + precision = 0; + format++; + if (*format == '*') + { + precision = va_arg(arguments, int); + if (precision < 0) + precision = -1; + format++; + } + else + { + while (*format >= '0' && *format <= '9') + { + if (precision <= WIDE_FORMAT_MAXIMUM_WIDTH) + precision = precision * 10 + (*format - '0'); + format++; + } + } + if (precision > WIDE_FORMAT_MAXIMUM_WIDTH) + precision = WIDE_FORMAT_MAXIMUM_WIDTH; + } + if (precision >= 0) + specification_length = (size_t)snprintf(specification, sizeof(specification), "%%%s%ld.%ld", flags, + width, precision); + else + specification_length = (size_t)snprintf(specification, sizeof(specification), "%%%s%ld", flags, + width); } /* size */ if (*format == 'h') @@ -716,13 +732,10 @@ static int wide_format(struct wide_output *output, const wchar_t *format, va_lis break; } case 'n': - { - int *count = va_arg(arguments, int *); - - if (count) - *count = (int)output->length; + /* (never written: a format may be a map's text. Its argument + is passed over, as UCRT does with %n disabled) */ + (void)va_arg(arguments, int *); break; - } case 'd': case 'i': case 'u': case 'x': case 'X': case 'o': case 'e': case 'E': case 'f': case 'g': case 'G': case 'p': { From 33804e0fe6132ff60ab13703f7a945c65242bfd0 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:33:44 -0400 Subject: [PATCH 058/187] stb_vorbis: refuse code books of no dimensions, and compare large powers as floats lookup1_values divided by a code book's dimensions, which a stream may give as 0, and cast powers past an int's range to int; both are undefined. A code book of no dimensions or entries now has no values (the stream is refused), and the powers are compared without the cast. --- port/linux/game/stb_vorbis.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/port/linux/game/stb_vorbis.c b/port/linux/game/stb_vorbis.c index 89c3f497..202f0b78 100644 --- a/port/linux/game/stb_vorbis.c +++ b/port/linux/game/stb_vorbis.c @@ -1243,12 +1243,18 @@ static int vorbis_validate(uint8 *data) // (formula implied by specification) static int lookup1_values(int entries, int dim) { - int r = (int) floor(exp((float) log((float) entries) / dim)); - if ((int) floor(pow((float) r+1, dim)) <= entries) // (int) cast for MinGW warning; - ++r; // floor() to avoid _ftol() when non-CRT + int r; + // port: a code book of no dimensions or entries has no values (the + // division below is by zero) + if (dim <= 0 || entries <= 0) return -1; + r = (int) floor(exp((float) log((float) entries) / dim)); + // port: the powers compared as they are, not cast to int first (a large + // dim's are past an int, where the cast is undefined) + if (floor(pow((float) r+1, dim)) <= entries) + ++r; if (pow((float) r+1, dim) <= entries) return -1; - if ((int) floor(pow((float) r, dim)) > entries) + if (floor(pow((float) r, dim)) > entries) return -1; return r; } From 764044d4f2d56ecf7f2c42b1f017c1ce4c93c366 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:34:10 -0400 Subject: [PATCH 059/187] GL: vertex and index uploads read only their own bytes stream_upload and index_upload rounded the size up to 16 bytes and read that many from the caller's data, so a draw whose vertices or indices were not a multiple of 16 bytes read past their allocation (a quad list's indices on every frame with transparent geometry). Only the data's own bytes are uploaded now; the room it takes stays rounded. --- port/linux/src/d3d8_gl.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 912e3f1a..81539462 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -4019,15 +4019,17 @@ static void stream_reserve(unsigned long size) static unsigned long stream_upload(const void *data, unsigned long size) { unsigned long offset; + /* (as index_upload: the vertices' own bytes, in room rounded up to 16) */ + unsigned long length = size; size = (size + 15) & ~15UL; stream_reserve(size); offset = device.stream_offset; state_array_buffer(device.stream_buffer); #ifdef HALO_ANDROID - host_gl_buffer_write(GL_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)size, data); + host_gl_buffer_write(GL_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)length, data); #else - buffer_upload(GL_ARRAY_BUFFER, offset, size, data); + buffer_upload(GL_ARRAY_BUFFER, offset, length, data); #endif device.stream_offset += size; return offset; @@ -4078,6 +4080,10 @@ static unsigned long stream_upload_swizzled(const struct vertex_shader_object *d static unsigned long index_upload(const void *data, unsigned long size) { unsigned long offset; + /* (the indices' own bytes are written; the room they take is rounded up + to 16, for the next ones' alignment: rounding what was read too read + past the caller's indices) */ + unsigned long length = size; size = (size + 15) & ~15UL; state_element_array_buffer(device.index_buffer); @@ -4088,9 +4094,9 @@ static unsigned long index_upload(const void *data, unsigned long size) } offset = device.index_offset; #ifdef HALO_ANDROID - host_gl_buffer_write(GL_ELEMENT_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)size, data); + host_gl_buffer_write(GL_ELEMENT_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)length, data); #else - buffer_upload(GL_ELEMENT_ARRAY_BUFFER, offset, size, data); + buffer_upload(GL_ELEMENT_ARRAY_BUFFER, offset, length, data); #endif device.index_offset += size; return offset; From debf9ca7da75bbb5c75b6f38d7c255007fa91c24 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:34:36 -0400 Subject: [PATCH 060/187] Validator: tag names no longer than a tag path The validator let a tag's or a reference's name be any length, but the game copies and formats tag names into buffers of 256 bytes (the game state header's map name, objects.c's messages). A name that does not end within 256 bytes is now given the empty name, as one outside the tags is. --- port/linux/game/tag_validate.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/port/linux/game/tag_validate.c b/port/linux/game/tag_validate.c index 786f8f8b..dccfd832 100644 --- a/port/linux/game/tag_validate.c +++ b/port/linux/game/tag_validate.c @@ -61,6 +61,11 @@ enum /* how deep blocks go in a schema (the deepest are 6 or 7) */ MAXIMUM_VALIDATION_DEPTH = 32, + /* the longest a tag's name is, with its terminator (Halo's tools' paths: + the game's buffers for them, game_state.c's, objects.c's, are this + size) */ + MAXIMUM_TAG_NAME_LENGTH = 256, + /* how many corrections are logged one by one, for each map or bsp */ MAXIMUM_LOGGED_CORRECTIONS = 64, MAXIMUM_MESSAGE_LENGTH = 512, @@ -431,12 +436,18 @@ static boolean string_valid( /* (the name a nameless tag or reference is given) */ if (string == tag_validate_empty_name) return TRUE; + /* (and no longer than a tag's path is, MAXIMUM_TAG_NAME_LENGTH: the + game copies and formats names into buffers of that size) */ if (region_contains(validation, string, 1)) - return memchr(string, 0, validation->region_size - offset) != NULL; + { + return memchr(string, 0, MIN(validation->region_size - offset, (unsigned long)MAXIMUM_TAG_NAME_LENGTH)) != + NULL; + } if (tags && (unsigned long)string >= (unsigned long)tags && tags_offset < (unsigned long)tag_validate_globals.tag_data_size) { - return memchr(string, 0, tag_validate_globals.tag_data_size - tags_offset) != NULL; + return memchr(string, 0, MIN(tag_validate_globals.tag_data_size - tags_offset, + (unsigned long)MAXIMUM_TAG_NAME_LENGTH)) != NULL; } return FALSE; From c1694f607f1f051dfd4595421b0108177bcb8d5a Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:35:30 -0400 Subject: [PATCH 061/187] HUD item messages: map text formatted only when it takes the count, and bounded An item's pickup message is a map string that the HUD used as the format of an unbounded usprintf into a 256-character buffer. It is now used as a format only if its one conversion is the count the game passes (ustring_format_takes), with usnprintf, and shown as plain text otherwise. usnprintf now also terminates a string it cuts short, and ustrncpy_terminated is a bounded copy that always terminates. --- source/interface/hud_messaging.c | 10 +++-- source/text/unicode.c | 69 ++++++++++++++++++++++++++++++++ source/text/unicode.h | 8 ++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/source/interface/hud_messaging.c b/source/interface/hud_messaging.c index 5eb0af3e..b270195d 100644 --- a/source/interface/hud_messaging.c +++ b/source/interface/hud_messaging.c @@ -1875,10 +1875,12 @@ void hud_messaging_update( wchar_t formatted[256]; short value_scale = MAX(item->item.hud_message_value_scale, 1); - usprintf( - formatted, - item_text, - message->quantity / value_scale); + /* port: the item's text (the map's) is the format: as + one only if it takes the count alone, and bounded */ + if (item_text && ustring_format_takes(item_text, "d")) + usnprintf(formatted, NUMBEROF(formatted), item_text, message->quantity / value_scale); + else + ustrncpy_terminated(formatted, item_text, NUMBEROF(formatted)); rasterizer_draw_unicode_string( &message_bounds, NULL, diff --git a/source/text/unicode.c b/source/text/unicode.c index 8c616769..9e565562 100644 --- a/source/text/unicode.c +++ b/source/text/unicode.c @@ -1337,10 +1337,79 @@ int usnprintf( va_start(arglist, format); result = _vsnwprintf(string, size, format, arglist); va_end(arglist); + /* port: terminated when it is cut short, which MSVC's is not */ + if (size > 0) + string[size - 1] = 0; return result; } +/* port: whether a format (a map's text: its string lists' are the game's +formats) takes exactly the arguments conversions names, in order: 'd' an +integer (%d, %i, %u, %x, %X, %o, %c), 's' a string (%s, %S), and no +others. %% and the flags, widths and precisions are allowed; '*' (which +takes an argument) and %n are not. A format that does not is shown as text +by the callers, unformatted, so that a map cannot make the game read an +argument as something it is not */ +int ustring_format_takes( + wchar_t const *format, + char const *conversions) +{ + if (!format || !conversions) + return FALSE; + while (*format) + { + char kind; + + if (*format++ != L'%') + continue; + if (*format == L'%') + { + format++; + continue; + } + while (*format == L'-' || *format == L'+' || *format == L' ' || *format == L'#' || *format == L'0' || + *format == L'.' || (*format >= L'0' && *format <= L'9')) + { + format++; + } + /* (the size prefixes the game's own formats use) */ + if (*format == L'h' || *format == L'l' || *format == L'w') + format++; + switch (*format) + { + case L'd': case L'i': case L'u': case L'x': case L'X': case L'o': case L'c': + kind = 'd'; + break; + case L's': case L'S': + kind = 's'; + break; + default: + return FALSE; + } + format++; + if (*conversions++ != kind) + return FALSE; + } + + return *conversions == 0; +} + +/* port: at most size - 1 characters of src, always terminated */ +wchar_t *ustrncpy_terminated( + wchar_t *dest, + wchar_t const *src, + unsigned long size) +{ + if (size > 0) + { + wcsncpy(dest, src ? src : L"", size - 1); + dest[size - 1] = 0; + } + + return dest; +} + int usprintf( wchar_t *string, wchar_t const *format, diff --git a/source/text/unicode.h b/source/text/unicode.h index 2f3b88eb..d3ef9dd7 100644 --- a/source/text/unicode.h +++ b/source/text/unicode.h @@ -223,6 +223,14 @@ int usprintf( wchar_t *string, wchar_t const *format, ...); +/* port: a map's text used as a format (its string lists' are) */ +int ustring_format_takes( + wchar_t const *format, + char const *conversions); +wchar_t *ustrncpy_terminated( + wchar_t *dest, + wchar_t const *src, + unsigned long size); int uvsnprintf( wchar_t *string, unsigned long size, From bcb0e5e6321282f9d59a808855fbcdf432aa2b65 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:35:42 -0400 Subject: [PATCH 062/187] Scoreboard: bounded copies of map strings into its buffers The game types' score names (format_score_name) copied a map string into the callers' 256-character score_string with ustrcpy, and the scoreboard formatted map strings and player names into row_string with usprintf, neither bounded. They now use ustrncpy_terminated and usnprintf with the buffers' sizes, and the title string's copies are terminated. --- source/game/game_engine.c | 22 +++++++++++++--------- source/game/game_engine_ctf.c | 3 ++- source/game/game_engine_king.c | 3 ++- source/game/game_engine_oddball.c | 6 ++++-- source/game/game_engine_race.c | 3 ++- source/game/game_engine_slayer.c | 3 ++- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 0016e819..5dd39917 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -1195,7 +1195,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy(title_string, outcome_string, 80); + ustrncpy_terminated(title_string, outcome_string, 80); break; case FALSE: @@ -1211,7 +1211,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy(title_string, outcome_string, 80); + ustrncpy_terminated(title_string, outcome_string, 80); } else { @@ -1225,7 +1225,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy(title_string, outcome_string, 80); + ustrncpy_terminated(title_string, outcome_string, 80); } break; @@ -1242,7 +1242,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy(title_string, outcome_string, 80); + ustrncpy_terminated(title_string, outcome_string, 80); } else { @@ -1256,7 +1256,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy(title_string, outcome_string, 80); + ustrncpy_terminated(title_string, outcome_string, 80); } break; } @@ -2041,7 +2041,9 @@ static void game_engine_rasterize_scoreboard( score_string[0] = 0; if (!campaign) game_engine->format_score_name(score_string); - usprintf(row_string, L"\t%s\t%s\t%s\t%s", column_name, score_name, score_string, network ? L"Ping" : L""); + /* port: bounded (the map's column names) */ + usnprintf(row_string, NUMBEROF(row_string), L"\t%s\t%s\t%s\t%s", column_name, score_name, score_string, + network ? L"Ping" : L""); { long column; @@ -2108,8 +2110,9 @@ static void game_engine_rasterize_scoreboard( else usprintf(ping_string, L"%ld", ping); } - usprintf( + usnprintf( row_string, + NUMBEROF(row_string), L"\t%s\t%s\t%s\t%s", campaign ? L"" : get_place_string(entry), player->name, @@ -2200,7 +2203,7 @@ static void game_engine_rasterize_in_game_score( score_name = L""; game_engine->format_score_name(score_string); - usprintf(row_string, L"\t%s\t%s\t%s", column_name, score_name, score_string); + usnprintf(row_string, NUMBEROF(row_string), L"\t%s\t%s\t%s", column_name, score_name, score_string); rasterize_in_game_score_draw_line(row_string, FALSE, &color, 1); for (entry_index = 0; entry_index < entry_count; entry_index++) @@ -2257,8 +2260,9 @@ static void game_engine_rasterize_in_game_score( place_string = get_place_string(&entries[entry_index]); - usprintf( + usnprintf( row_string, + NUMBEROF(row_string), L"\t%s\t%s\t%s", place_string, player->name, diff --git a/source/game/game_engine_ctf.c b/source/game/game_engine_ctf.c index 33faa797..7f899e71 100644 --- a/source/game/game_engine_ctf.c +++ b/source/game/game_engine_ctf.c @@ -865,7 +865,8 @@ static wchar_t *ctf_get_score_header_string( else string = L""; - ustrcpy(buffer, string); + /* port: bounded (a map's text, into its callers' score_string[256]) */ + ustrncpy_terminated(buffer, string, 256); return buffer; } diff --git a/source/game/game_engine_king.c b/source/game/game_engine_king.c index 24a180d0..e1c9343b 100644 --- a/source/game/game_engine_king.c +++ b/source/game/game_engine_king.c @@ -762,7 +762,8 @@ static wchar_t *king_get_score_header_string( { string = L""; } - ustrcpy(buffer, string); + /* port: bounded (a map's text, into its callers' score_string[256]) */ + ustrncpy_terminated(buffer, string, 256); return buffer; } diff --git a/source/game/game_engine_oddball.c b/source/game/game_engine_oddball.c index f5f46118..8f2a050e 100644 --- a/source/game/game_engine_oddball.c +++ b/source/game/game_engine_oddball.c @@ -1013,11 +1013,13 @@ static wchar_t *oddball_get_score_header_string( string_index = _multiplayer_game_text_time; string_list_index = tag_loaded('ustr', "ui\\multiplayer_game_text"); - ustrcpy( + /* port: bounded (a map's text, into its callers' score_string[256]) */ + ustrncpy_terminated( buffer, string_list_index != NONE ? unicode_string_list_get_string(string_list_index, string_index) : - L""); + L"", + 256); return buffer; } diff --git a/source/game/game_engine_race.c b/source/game/game_engine_race.c index d940e080..b143af79 100644 --- a/source/game/game_engine_race.c +++ b/source/game/game_engine_race.c @@ -1205,7 +1205,8 @@ static wchar_t *race_get_score_header_string( else header_string = L""; - ustrcpy(string, header_string); + /* port: bounded (a map's text, into its callers' score_string[256]) */ + ustrncpy_terminated(string, header_string, 256); return string; } diff --git a/source/game/game_engine_slayer.c b/source/game/game_engine_slayer.c index 89bc5474..faddf1e5 100644 --- a/source/game/game_engine_slayer.c +++ b/source/game/game_engine_slayer.c @@ -307,7 +307,8 @@ static wchar_t *slayer_get_score_header_string( { long string_list_index; - ustrcpy(buffer, GET_MULTIPLAYER_GAME_TEXT(_string_score)); + /* port: bounded (a map's text, into its callers' score_string[256]) */ + ustrncpy_terminated(buffer, GET_MULTIPLAYER_GAME_TEXT(_string_score), 256); return buffer; } From ea791119947235d76346256f1442400cb24b2700 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:35:50 -0400 Subject: [PATCH 063/187] Bounded copies and formats of tag names The scenario's tag name was copied with strcpy into the game state header's 256-byte map_name, a tag's name was formatted with sprintf into the out of objects message's 512 bytes, and csprintf bounded its output at MAXIMUM_STRING_SIZE even when it formats into temporary, which is 256 bytes and where most callers put tag names. All three are bounded by their buffers' sizes now. --- source/cseries/cseries.c | 6 ++++-- source/objects/objects.c | 4 +++- source/saved games/game_state.c | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/source/cseries/cseries.c b/source/cseries/cseries.c index 87a713a0..a541e578 100644 --- a/source/cseries/cseries.c +++ b/source/cseries/cseries.c @@ -349,8 +349,10 @@ char *csprintf( va_start(arglist, format); /* port: no longer than the longest a string is (MAXIMUM_STRING_SIZE, - as csstrlen asserts): the caller's buffer's size isn't passed */ - vsnprintf(buffer, MAXIMUM_STRING_SIZE, format, arglist); + as csstrlen asserts): the caller's buffer's size isn't passed. But + temporary, which most callers format into (often with a map's tag + names), is only its own size */ + vsnprintf(buffer, buffer == temporary ? sizeof(temporary) : MAXIMUM_STRING_SIZE, format, arglist); va_end(arglist); return buffer; diff --git a/source/objects/objects.c b/source/objects/objects.c index c88154ff..45dc6699 100644 --- a/source/objects/objects.c +++ b/source/objects/objects.c @@ -3680,7 +3680,9 @@ long object_new( if (object_index==NONE && definition_index!=NONE) { char string[512]; - sprintf(string, "OUT OF OBJECTS: cannot create %s", tag_name_strip_path(tag_get_name(definition_index))); + /* port: snprintf (a map's tag's name may be any length) */ + snprintf(string, sizeof(string), "OUT OF OBJECTS: cannot create %s", + tag_name_strip_path(tag_get_name(definition_index))); console_printf(FALSE, "%s", string); error(_error_log, "%s", string); } diff --git a/source/saved games/game_state.c b/source/saved games/game_state.c index 4c9d1689..0b5a073b 100644 --- a/source/saved games/game_state.c +++ b/source/saved games/game_state.c @@ -300,7 +300,9 @@ void game_state_initialize_for_new_map( memset(game_state_globals.header, 0, sizeof(*game_state_globals.header)); name = tag_get_name(global_scenario_index); - strcpy(game_state_globals.header->map_name, name); + /* port: no longer than the header has room for (a map's scenario's + name is the map's) */ + strncpy(game_state_globals.header->map_name, name, sizeof(game_state_globals.header->map_name) - 1); strcpy(game_state_globals.header->build_number, "01.01.14.2342"); game_state_globals.header->player_count = player_spawn_count; From 5ec67a8db4079633d4df1795a4b48dc12b4ac1b9 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:36:10 -0400 Subject: [PATCH 064/187] String lists: a string of one byte is not terminated before its data unicode_string_list_get_string terminates a string at its last character, size / 2 - 1, so a string whose data is one byte long had its terminator written two bytes before its data. Strings shorter than one character are now not used. --- source/text/text_group.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/text/text_group.c b/source/text/text_group.c index 9b4ba50c..ecb05b5a 100644 --- a/source/text/text_group.c +++ b/source/text/text_group.c @@ -271,7 +271,9 @@ wchar_t *unicode_string_list_get_string(long tag_index, short string_index) string_index, struct string_list_entry); - if (entry->string.size > 0) + /* port: only a string of at least one character, terminated + within it (a size of 1 wrote the terminator before its data) */ + if (entry->string.size >= (long)sizeof(wchar_t)) { result = entry->string.address; result[entry->string.size / sizeof(wchar_t) - 1] = L'\0'; From 40bb5ea4a68387b8c12405b9663726e9be144d39 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:36:36 -0400 Subject: [PATCH 065/187] Map strings used as formats only with the arguments the game gives The multiplayer messages, scores and saved games' names are formatted with map strings, and one whose conversions differ from the arguments passed (a string where the game gives a number) made the game read an argument as a type it is not. Each site now passes its arguments' signature to ustring_format_checked: a format whose conversions are not a prefix of it gives an empty message, logged once. The shipped strings all pass. --- source/game/game_engine.c | 48 +++++++++++++-------------- source/game/game_engine_ctf.c | 8 ++--- source/game/game_engine_king.c | 6 ++-- source/game/game_engine_oddball.c | 10 +++--- source/game/game_engine_race.c | 20 +++++------ source/game/game_engine_slayer.c | 12 +++---- source/saved games/saved_game_files.c | 2 +- source/text/unicode.c | 42 ++++++++++++++++++----- source/text/unicode.h | 3 ++ 9 files changed, 89 insertions(+), 62 deletions(-) diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 5dd39917..c2cc5dd3 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -1150,7 +1150,7 @@ static void game_engine_generate_title_string( usnprintf( life_string, NUMBEROF(life_string), - format_string, + ustring_format_checked(format_string, "d"), remaining_lives); life_string[NUMBEROF(life_string) - 1] = 0; secondary_string = life_string; @@ -1292,7 +1292,7 @@ static void game_engine_generate_title_string( usnprintf( title_string, 80, - format_string, + ustring_format_checked(format_string, "sss"), team0_name, team1_name, secondary_string); @@ -1312,7 +1312,7 @@ static void game_engine_generate_title_string( usnprintf( title_string, 80, - format_string, + ustring_format_checked(format_string, "sss"), team1_name, team0_name, secondary_string); @@ -1332,7 +1332,7 @@ static void game_engine_generate_title_string( usnprintf( title_string, 80, - format_string, + ustring_format_checked(format_string, "ss"), team1_name, secondary_string); } @@ -1360,7 +1360,7 @@ static void game_engine_generate_title_string( usnprintf( title_string, 80, - format_string, + ustring_format_checked(format_string, "sss"), get_place_string(&entry), score_string, secondary_string); @@ -1381,7 +1381,7 @@ static void game_engine_generate_title_string( usnprintf( title_string, 80, - format_string, + ustring_format_checked(format_string, "sss"), get_place_string(&entry), score_string, secondary_string); @@ -2405,7 +2405,7 @@ void game_engine_post_rasterize_post_game( usnprintf( row_string, NUMBEROF(row_string), - team_formats[team_index], + ustring_format_checked(team_formats[team_index], "s"), score_string); row_string[NUMBEROF(row_string) - 1] = 0; drawline(row_string, team_row + 4, 0); @@ -7935,7 +7935,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "s"), player->name); break; case _game_engine_message_killed_by_unknown: @@ -7947,7 +7947,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "s"), player->name); break; case _game_engine_message_killed_by_biped: @@ -7959,7 +7959,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "s"), player->name); break; case _game_engine_message_killed_by_vehicle: @@ -7971,7 +7971,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "s"), player->name); break; case _game_engine_message_killed_by_player: @@ -7984,7 +7984,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "ss"), player->name, other_player->name); break; @@ -7998,7 +7998,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "ss"), player->name, other_player->name); break; @@ -8012,7 +8012,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "s"), other_player->name); break; case _game_engine_message_killed_by_self: @@ -8024,7 +8024,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "s"), player->name); break; case _game_engine_message_killed_friendly: @@ -8037,7 +8037,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "s"), other_player->name); break; case _game_engine_message_multi_kill: @@ -8110,7 +8110,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "s"), other_player->name); break; case _game_engine_message_multi_kill_with_score: @@ -8122,7 +8122,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "d"), score); game_engine_play_multiplayer_sound(_multiplayer_sound_killtacular_kill); break; @@ -8135,7 +8135,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "d"), score); game_engine_play_multiplayer_sound(_multiplayer_sound_triple_kill); break; @@ -8148,7 +8148,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "d"), score); game_engine_play_multiplayer_sound(_multiplayer_sound_double_kill); break; @@ -8161,7 +8161,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "d"), score); game_engine_play_multiplayer_sound(_multiplayer_sound_running_riot); break; @@ -8174,7 +8174,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "d"), score); game_engine_play_multiplayer_sound(_multiplayer_sound_killing_spree); break; @@ -8188,7 +8188,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "sd"), other_player->name, score); break; @@ -8223,7 +8223,7 @@ static boolean internal_rasterize_score( usnprintf( buffer, buffer_size, - format, + ustring_format_checked(format, "d"), message_data); break; case _game_engine_message_waiting_for_space_to_clear: diff --git a/source/game/game_engine_ctf.c b/source/game/game_engine_ctf.c index 7f899e71..9a5b204d 100644 --- a/source/game/game_engine_ctf.c +++ b/source/game/game_engine_ctf.c @@ -900,7 +900,7 @@ static boolean ctf_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "dd"), ctf_globals.scores[_team_red], ctf_globals.scores[_team_blue]); break; @@ -918,7 +918,7 @@ static boolean ctf_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "dd"), ctf_globals.scores[team_index], ctf_globals.scores[other_team_index]); break; @@ -936,7 +936,7 @@ static boolean ctf_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "dd"), ctf_globals.scores[team_index], ctf_globals.scores[other_team_index]); break; @@ -954,7 +954,7 @@ static boolean ctf_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "dd"), ctf_globals.scores[team_index], ctf_globals.scores[other_team_index]); break; diff --git a/source/game/game_engine_king.c b/source/game/game_engine_king.c index e1c9343b..8061c4e6 100644 --- a/source/game/game_engine_king.c +++ b/source/game/game_engine_king.c @@ -560,7 +560,7 @@ static boolean king_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, place_name, score); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "sd"), place_name, score); } else if (message == king_message_ally_on_the_hill) { @@ -573,7 +573,7 @@ static boolean king_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, other_player->name, score); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "sd"), other_player->name, score); } else if (message == king_message_enemy_on_the_hill) { @@ -586,7 +586,7 @@ static boolean king_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, other_player->name, score); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "sd"), other_player->name, score); } else { diff --git a/source/game/game_engine_oddball.c b/source/game/game_engine_oddball.c index 8f2a050e..dd141b34 100644 --- a/source/game/game_engine_oddball.c +++ b/source/game/game_engine_oddball.c @@ -1218,7 +1218,7 @@ static boolean oddball_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, other_player->name); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "s"), other_player->name); } break; @@ -1261,7 +1261,7 @@ static boolean oddball_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, other_player->name); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "s"), other_player->name); } break; @@ -1286,7 +1286,7 @@ static boolean oddball_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, place_name, score); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "sd"), place_name, score); } else if (message == _oddball_message_ally_has_the_ball_tick) { @@ -1299,7 +1299,7 @@ static boolean oddball_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, other_player->name, score); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "sd"), other_player->name, score); } else if (message == _oddball_message_enemy_has_the_ball_tick) { @@ -1312,7 +1312,7 @@ static boolean oddball_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, other_player->name, score); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "sd"), other_player->name, score); } } break; diff --git a/source/game/game_engine_race.c b/source/game/game_engine_race.c index b143af79..e74b346c 100644 --- a/source/game/game_engine_race.c +++ b/source/game/game_engine_race.c @@ -924,7 +924,7 @@ static boolean race_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, other_player->name); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "s"), other_player->name); } break; @@ -939,7 +939,7 @@ static boolean race_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, other_player->name); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "s"), other_player->name); } break; @@ -961,7 +961,7 @@ static boolean race_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "df"), other_player->statistics.multiplayer_statistics.race_statistics.laps + 1, lap_time); } @@ -980,7 +980,7 @@ static boolean race_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "sd"), other_player->name, other_player->statistics.multiplayer_statistics.race_statistics.laps + 1); break; @@ -998,7 +998,7 @@ static boolean race_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "sd"), other_player->name, other_player->statistics.multiplayer_statistics.race_statistics.laps); break; @@ -1018,7 +1018,7 @@ static boolean race_engine_display_score( } else string = L""; - usnprintf(buffer, buffer_size, string, best_lap_time); + usnprintf(buffer, buffer_size, ustring_format_checked(string, "f"), best_lap_time); } break; @@ -1039,7 +1039,7 @@ static boolean race_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "s"), get_place_name(game_engine_get_place(player_index, _get_score_team))); } else @@ -1056,7 +1056,7 @@ static boolean race_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "sd"), get_place_name(game_engine_get_place(player_index, _get_score_team)), other_player->statistics.multiplayer_statistics.race_statistics.laps); } @@ -1076,7 +1076,7 @@ static boolean race_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "s"), get_place_name(game_engine_get_place(player_index, _get_score_team))); } else @@ -1093,7 +1093,7 @@ static boolean race_engine_display_score( usnprintf( buffer, buffer_size, - string, + ustring_format_checked(string, "sdd"), get_place_name(game_engine_get_place(player_index, _get_score_team)), other_player->statistics.multiplayer_statistics.race_statistics.laps + 1, game_engine_get_variant()->universal_variant.score_to_win); diff --git a/source/game/game_engine_slayer.c b/source/game/game_engine_slayer.c index faddf1e5..5677d4d0 100644 --- a/source/game/game_engine_slayer.c +++ b/source/game/game_engine_slayer.c @@ -525,7 +525,7 @@ static boolean slayer_engine_display_score( usnprintf( string, NUMBEROF(string), - GET_MULTIPLAYER_GAME_TEXT(_string_n_team_n), + ustring_format_checked(GET_MULTIPLAYER_GAME_TEXT(_string_n_team_n), "dd"), slayer_get_score(player_index, _get_score_individual), slayer_get_score(player_index, _get_score_team)); } @@ -547,7 +547,7 @@ static boolean slayer_engine_display_score( usnprintf( buffer, buffer_character_count, - GET_MULTIPLAYER_GAME_TEXT(_string_new_target_name), + ustring_format_checked(GET_MULTIPLAYER_GAME_TEXT(_string_new_target_name), "s"), target_player->name); break; @@ -560,8 +560,8 @@ static boolean slayer_engine_display_score( usnprintf( buffer, buffer_character_count, - GET_MULTIPLAYER_GAME_TEXT( - _string_name_kills_score_n_team_score_of_max), + ustring_format_checked(GET_MULTIPLAYER_GAME_TEXT( + _string_name_kills_score_n_team_score_of_max), "sddd"), place_name, slayer_get_score(player_index, _get_score_individual), slayer_get_score(player_index, _get_score_team), @@ -575,8 +575,8 @@ static boolean slayer_engine_display_score( usnprintf( buffer, buffer_character_count, - GET_MULTIPLAYER_GAME_TEXT( - _string_name_kills_score_of_max), + ustring_format_checked(GET_MULTIPLAYER_GAME_TEXT( + _string_name_kills_score_of_max), "sdd"), place_name, slayer_get_score(player_index, _get_score_team), game_engine_get_variant()->universal_variant.score_to_win); diff --git a/source/saved games/saved_game_files.c b/source/saved games/saved_game_files.c index c179191e..d5a6356d 100644 --- a/source/saved games/saved_game_files.c +++ b/source/saved games/saved_game_files.c @@ -1593,7 +1593,7 @@ void saved_game_file_get_useable_untitled_profile_name( for (index = 0; index < MAXIMUM_UNTITLED_SAVED_GAMES; index++) { usnprintf(display_name, MAX_GAMENAME-1, - unicode_string_list_get_string(string_list_index, _saved_game_file_string_untitled_name_format), + ustring_format_checked(unicode_string_list_get_string(string_list_index, _saved_game_file_string_untitled_name_format), "d"), index+1); display_name[MAX_GAMENAME-1] = 0; diff --git a/source/text/unicode.c b/source/text/unicode.c index 9e565562..1d6bdcbf 100644 --- a/source/text/unicode.c +++ b/source/text/unicode.c @@ -247,6 +247,7 @@ symbols in this file: #include #include "cseries/cseries.h" +#include "cseries/errors.h" /* port: ustring_format_checked */ #include "unicode.h" /* ---------- constants */ @@ -1345,12 +1346,11 @@ int usnprintf( } /* port: whether a format (a map's text: its string lists' are the game's -formats) takes exactly the arguments conversions names, in order: 'd' an -integer (%d, %i, %u, %x, %X, %o, %c), 's' a string (%s, %S), and no -others. %% and the flags, widths and precisions are allowed; '*' (which -takes an argument) and %n are not. A format that does not is shown as text -by the callers, unformatted, so that a map cannot make the game read an -argument as something it is not */ +formats) takes no other arguments than conversions names, in order, if +fewer: 'd' an integer (%d, %i, %u, %x, %X, %o, %c), 'f' a real (%f, %e, +%g), 's' a string (%s, %S). %% and the flags, widths and precisions are +allowed; '*' (which takes an argument of its own) and %n are not. A format +that takes others would have the game read an argument as what it is not */ int ustring_format_takes( wchar_t const *format, char const *conversions) @@ -1368,8 +1368,8 @@ int ustring_format_takes( format++; continue; } - while (*format == L'-' || *format == L'+' || *format == L' ' || *format == L'#' || *format == L'0' || - *format == L'.' || (*format >= L'0' && *format <= L'9')) + while (*format == L'-' || *format == L'+' || *format == L' ' || *format == L'#' || *format == L'.' || + (*format >= L'0' && *format <= L'9')) { format++; } @@ -1381,6 +1381,9 @@ int ustring_format_takes( case L'd': case L'i': case L'u': case L'x': case L'X': case L'o': case L'c': kind = 'd'; break; + case L'f': case L'e': case L'E': case L'g': case L'G': + kind = 'f'; + break; case L's': case L'S': kind = 's'; break; @@ -1392,7 +1395,28 @@ int ustring_format_takes( return FALSE; } - return *conversions == 0; + return TRUE; +} + +/* port: a map's format, if it takes the arguments a caller gives it +(ustring_format_takes); else an empty one, whose message is then empty, and +which is logged once */ +wchar_t const *ustring_format_checked( + wchar_t const *format, + char const *conversions) +{ + static boolean logged = FALSE; + + if (ustring_format_takes(format, conversions)) + return format; + if (!logged) + { + logged = TRUE; + error(_error_silent, "a map's text is a format of other arguments than the game gives it (%s): it is not shown", + conversions); + } + + return L""; } /* port: at most size - 1 characters of src, always terminated */ diff --git a/source/text/unicode.h b/source/text/unicode.h index d3ef9dd7..1c5d9031 100644 --- a/source/text/unicode.h +++ b/source/text/unicode.h @@ -227,6 +227,9 @@ int usprintf( int ustring_format_takes( wchar_t const *format, char const *conversions); +wchar_t const *ustring_format_checked( + wchar_t const *format, + char const *conversions); wchar_t *ustrncpy_terminated( wchar_t *dest, wchar_t const *src, From 792711ca57de5610b6dc2adbde74e8e24fd566ab Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 11:15:26 -0400 Subject: [PATCH 066/187] A grenade's shock wave is dealt with its explosion A frag grenade's explosion deals two damage effects, its explosion and its shock wave (a plasma grenade's too), and a client reports each hit of each. The host took the first of them against the throw it had seen and spent the throw; the explosion's other hits came with it only when they were of the same damage, so every hit of the other damage found no throw left and was refused as damage the player could not deal. An explosion of the player's this tick is now one of the grenade, of any damage its tags deal, so its other damage takes no throw of its own (and is still paid for at the grenade's rate). In the network test (1v1 slayer on wizard, staged hits and the bots' own fire and grenades, 8 games each) the host refused 4.4% of the client's reports with no added latency (134 of 3072, 104 of them a grenade's other damage) and 4.9% at a 100 ms round trip (153 of 3107); now 0.4% (11 of 2959) and 0.8% (25 of 2986). What it still refuses it should: grenades it never saw thrown, targets gone before the report came, and hits on a player the client still had at the teleporter they had left. --- port/linux/NETCODE.md | 5 +++-- port/linux/game/network_damage.c | 35 ++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index 001ca220..602afcb0 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -379,8 +379,9 @@ a pregame keep-alive every five seconds from the host player; damage one of their weapons (a vehicle's a driver's or gunner's; now or in the last ten seconds), a grenade the host's own game saw them throw in the last ten seconds that has not gone off (each - throw's explosion is taken once, its other hits that tick with it: - holding grenades deals nothing) or the vehicle they drove (in the + throw's explosion is taken once, its other hits and its other damage + (a frag grenade's shock wave) that tick with it: holding grenades + deals nothing) or the vehicle they drove (in the last ten seconds: its collisions) can deal (its projectiles' impacts and detonations, followed through the tags), no harder than it can be (all of it, but an airborne melee blow's half again); of the shape the diff --git a/port/linux/game/network_damage.c b/port/linux/game/network_damage.c index 519722c7..3ed444a5 100644 --- a/port/linux/game/network_damage.c +++ b/port/linux/game/network_damage.c @@ -1163,20 +1163,28 @@ static short distributed_grenade_throw( return oldest; } -/* whether an explosion of the damage is among the player's this tick -(distributed_report_paid): at the epicenter, or (NULL) anywhere */ +/* whether an explosion of the type of grenade is among the player's this +tick (distributed_report_paid), of any damage the grenade deals (one +explosion deals more than one, each reported: a frag grenade's explosion +and its shock wave): at the epicenter, or (NULL) anywhere */ static boolean distributed_explosion_this_tick( short player_index, - long damage_index, + short grenade_type, real_point3d const *epicenter) { + struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&scenario_get_game_globals()->grenades, + grenade_type, struct game_globals_grenade); short index; - if (damage_explosions[player_index].time != game_time_get()) + if (damage_explosions[player_index].time != game_time_get() || grenade->projectile.index == NONE) return FALSE; for (index = 0; index < damage_explosions[player_index].count; index++) { - if (damage_explosions[player_index].explosions[index].definition_index == damage_index && + byte kinds; + struct damage_reach reach; + + if (distributed_source_deals(grenade->projectile.index, + damage_explosions[player_index].explosions[index].definition_index, TRUE, &kinds, &reach) > 0.0f && (!epicenter || (damage_explosions[player_index].explosions[index].epicenter.x == epicenter->x && damage_explosions[player_index].explosions[index].epicenter.y == epicenter->y && damage_explosions[player_index].explosions[index].epicenter.z == epicenter->z))) @@ -1189,10 +1197,11 @@ static boolean distributed_explosion_this_tick( /* how many hits a second the player could deal of the damage: their weapons, lately, a grenade the host saw them throw that has not gone off -(or one going off this tick, hitting more than one object), the vehicle -they drove lately; 0 when none of them deals it; how they deal it (the -_damage_source flags); how far from them what they fire deals it; and the -type of grenade when nothing but a grenade of theirs deals it, else NONE */ +(or one going off this tick, hitting more than one object, dealing more +than one damage), the vehicle they drove lately; 0 when none of them deals +it; how they deal it (the _damage_source flags); how far from them what +they fire deals it; and the type of grenade when nothing but a grenade of +theirs deals it, else NONE */ static real distributed_player_deals( short player_index, long damage_index, @@ -1255,7 +1264,7 @@ static real distributed_player_deals( struct game_globals_grenade); boolean thrown = distributed_grenade_throw(player_index, index) != NONE; - if ((thrown || distributed_explosion_this_tick(player_index, damage_index, NULL)) && + if ((thrown || distributed_explosion_this_tick(player_index, index, NULL)) && grenade->projectile.index != NONE) { byte this_kinds; @@ -1835,10 +1844,10 @@ static boolean distributed_report_valid( return FALSE; /* what only a grenade of theirs deals, from one the host saw them throw that has not gone off: its explosion once (the explosion's other objects - that tick come free, distributed_report_paid), what it hits at a point - while it is about */ + that tick come free, distributed_report_paid, and its other damage takes + no throw of its own), what it hits at a point while it is about */ if (grenade_type != NONE && - !distributed_explosion_this_tick(player_index, report->damage.definition_index, &report->damage.epicenter)) + !distributed_explosion_this_tick(player_index, grenade_type, &report->damage.epicenter)) { throw_index = distributed_grenade_throw(player_index, grenade_type); if (throw_index == NONE) From 22fe5628b60b6bf2ec396b13c280323e7433d641 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 13:23:09 -0400 Subject: [PATCH 067/187] A remote player the host teleported stays where they landed When the host's teleporter sent a player, a client's copy that had not stepped on the pad itself was put at the far end by the host's word, landing on the teleporter there with no note of it (the game's own teleporter notes the one it lands a player on). The client's teleporter sent it straight back, every tick the host's word put it there, until the player stepped half a world unit off: the copy stood on the pad left behind, 24 world units off, for 6 to 9 ticks after most such teleports and for as long as the player stood still (up to 143 ticks in earlier runs), and the client's hits on them there were refused. A player's unit the host's word moves further than a step at once is now on the teleporter it lands on, as one the teleporter sends is. 1v1 slayer on Wizard, two bots shooting, 16 runs of 12 minutes each way at 0 and 50 ms: the client's teleporter sending the host's player back 85 ticks in 18 episodes before, none after; the copy over 5 world units from the host's for 3 ticks or more 12 times (82 ticks, up to 9) before, none after. Hits refused 3.7% before and 4.0% after, the view error (p50 0.03, p95 0.08 world units) the same. --- port/linux/NETCODE.md | 4 +++- port/linux/game/network_distributed.c | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index 001ca220..93d9601d 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -90,7 +90,9 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick: - **Corrections.** The host sends each client the authoritative state of the players' units and the game's moving objects; a client moves its copies toward it, a small error half of the way each tick, a larger one - at once, drawn gliding from where they were. A client's own unit and + at once, drawn gliding from where they were (a player's moved at once + is on the teleporter it lands on, as a teleporter leaves one it sends, + so the client's does not send it back). A client's own unit and vehicle are only corrected past a tolerance, so prediction does not rubber-band: the host tells the client which of the client's ticks it has its player at (its prediction come back), and the client compares diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index 24cc1048..19a123b2 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -2245,6 +2245,8 @@ static void distributed_handle_unit_state( if (TEST_FLAG(state->flags, _distributed_unit_placed_bit) && object_get(unit_index)->object.parent_object_index == NONE) { + real_point3d before = object_get(unit_index)->object.position; + if (local) distributed_correct_own_unit(player, unit_index, state); else @@ -2252,6 +2254,16 @@ static void distributed_handle_unit_state( distributed_apply_state(unit_index, state, &state->position, REMOTE_CORRECTION_TOLERANCE, REMOTE_BLEND_DISTANCE, 0.0f, 0.0f); } + /* moved further than a step at once (the host's teleporter sent + it): on the teleporter it lands on, as a teleporter leaves the + player it sends, so that this machine's does not send it back each + time the host's word puts it there until it steps off + (game_engine_update_teleporter) */ + if (distance_squared3d(&before, &object_get(unit_index)->object.position) > 1.0f) + { + player->teleporter_index = find_netgame_flag(&object_get(unit_index)->object.position, 1.0f, 0.0f, + _netgame_flag_teleporter_source, NONE); + } } } From 1f87b4ff63883c2420186e7846b66a44f1d4f068 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 13:23:35 -0400 Subject: [PATCH 068/187] A client's copy of a unit carries no grenades until the host says A client makes its copy of a unit the host spawns with object_new, and unit_new gives it the unit tag's grenades (a cyborg's four frags), where the host's game engine gives a player's unit the game's own (none on wizard slayer). The host's inventory, which corrects them, comes a tick or two later, so a client's player spawning with the throw button held began a throw with a grenade its unit did not have: the client saw it thrown and go off, and reported its hits, while the host's copy, with no grenade, never threw, and refused every report as damage the player could not deal. A client's copy of a host's unit now starts with no grenades, and takes the host's when its inventory comes. In the network test (1v1 slayer on wizard, staged hits and the bots' own fire and grenades, with #209's shock wave fix in both builds), with no added latency over 8 games the client threw 41 grenades to the host copy's 39, and the host refused all 5 reports of the 2 explosions it never saw thrown (11 of 2959 reports refused in all); now 55 throws to 55, no explosion refused for want of a throw, and 2 of 2749 reports refused. At 50 ms a receiver (8 games before: 66 throws to 64, 7 reports of 2 explosions refused; 4 games after) 10 throws to 10 and none of 1396 reports refused. Every unseen throw was in the client's first tick or two alive, with four grenades there and none on the host. Kills and deaths stay balanced. --- port/linux/NETCODE.md | 4 +++- port/linux/game/network_objects.c | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index 001ca220..1f169107 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -325,7 +325,9 @@ a pregame keep-alive every five seconds from the host indices from the upper half of the object array, clear of the host's. - Ten times a second, what every unit carries (the host's weapons, slot for slot, their ammunition, the weapon in hand, the grenades); a - client moves the same weapon objects in and out of its units. A + client moves the same weapon objects in and out of its units, which + carry no grenades until the host says (not the unit tag's: a player + spawned with the button held threw one the host's copy had not). A change of weapons or grenades goes to every client at once; one of ammunition only to the unit's player's machine at once, and to the others as often as they are sent that player. A client takes its own diff --git a/port/linux/game/network_objects.c b/port/linux/game/network_objects.c index 8ee2020e..1da5e4d5 100644 --- a/port/linux/game/network_objects.c +++ b/port/linux/game/network_objects.c @@ -2316,6 +2316,12 @@ static void distributed_client_create( return; } objects_client_has[absolute_index] = object_index; + /* (a unit's grenades are the host's, which its inventories bring: none + until they come, not the tag's that unit_new gave it, which the host's + game engine replaces with the game's at a player's spawn: a player + spawned with the button held threw one here the host's copy had not) */ + if (TEST_FLAG(_object_mask_unit, object_get(object_index)->object.type)) + csmemset(unit_get(object_index)->unit.grenade_counts, 0, sizeof(unit_get(object_index)->unit.grenade_counts)); distributed_client_apply_change(object_index, change); } From 3b20ef879a093e341f7a43b3a3543371261057c5 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:07:51 +1300 Subject: [PATCH 069/187] Votes to kick Any player of a network game can start a vote to kick another, on any team, by picking their name on the scoreboard (right-click while it is open frees the mouse) or with the console's votekick; the host's own scoreboard also has Kick and Ban (Ban picked twice). The host alone counts (port/linux/game/network_votekick.c): votes only from each voter's stream, one per real address and again per hardware id (which only removes votes), time played on the host before starting (network.votekick_minutes) and voting, more than half of everyone with the target counted and at least two, one vote at a time with cooldowns for a failed vote's starter and target, and a passed vote keeps the player out for network.votekick_ban_minutes. Builds without it drop the new messages, so the network version stays. --- port/linux/NETCODE.md | 18 + port/linux/README.md | 33 + port/linux/game/network_distributed.c | 86 +- port/linux/game/network_distributed.h | 31 + port/linux/game/network_votekick.c | 1056 ++++++++++++++++++++ port/linux/game/network_votekick.h | 63 ++ port/linux/include/halo_ui_pointer.h | 7 + port/linux/src/d3d8_gl.c | 33 +- port/linux/src/port_config.c | 12 + port/linux/src/sdl_platform.c | 103 +- port/linux/src/sdl_platform.h | 3 + source/game/game_engine.c | 298 +++++- source/hs/hs.c | 22 + source/main/console.c | 20 +- source/networking/network_server_manager.c | 101 +- source/networking/network_server_manager.h | 5 + tools/harness/tests/test_votekick_count.py | 65 ++ tools/harness/tests/votekick_count.c | 164 +++ 18 files changed, 2083 insertions(+), 37 deletions(-) create mode 100644 port/linux/game/network_votekick.c create mode 100644 port/linux/game/network_votekick.h create mode 100644 tools/harness/tests/test_votekick_count.py create mode 100644 tools/harness/tests/votekick_count.c diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index 001ca220..ea3ef686 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -231,6 +231,24 @@ machine tells them: anyone with administrator or root access can change them, and players behind one address share it. The console's `kick ` drops a player as `ban` does (every machine told), but adds no line and keeps no address out: the player may join again at once. + +The players' votes to kick a player (`network_votekick.c`): a client sends +`_distributed_message_votekick` (the player's absolute index) reliably, and +the host takes it only from the client's stream. The host alone counts: +one vote per real address (an internet play peer's endpoint, not its +stand-in), and votes again deduplicated by hardware id, which only ever +removes votes (the players who may vote are counted by address alone, so a +copied hardware id cannot lower the votes needed). A vote needs more than +half of the players who may vote, the target counted, and at least two; +starting one needs `network.votekick_minutes` played on this host in its +own ticks, voting two minutes; one vote runs at a time, and a failed vote +cools its starter down and protects its target. A vote that passes kicks as +the `kick` command does, and the host refuses the address and hardware id +for `network.votekick_ban_minutes`. The host sends each client +`_distributed_message_votekick_status` every second while a vote runs (and +as it changes), for the scoreboard, whose right click frees the mouse to +pick a player; the host's own scoreboard adds Kick and Ban. Builds without +votes drop both kinds as unknown, so the network version is unchanged. A speed hack of less than a tenth is let be: the host's bounds on how far and how fast a client's player moves and fires hold it to the host's time anyway. diff --git a/port/linux/README.md b/port/linux/README.md index adc6f996..ae7ddf3d 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -271,6 +271,9 @@ the setting for one start of the game. It has priority over the file. | `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup writes its choice here. | | `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad), up to 8 times the squad however many players there are. PER PLAYER in co-op's Server Setup writes its choice here. | | `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup writes its choice here. | +| `network.votekick` | `true` | `HALO_NET_VOTEKICK` | When you host: `true`, the players can vote to kick a player. Refer to "Security". `false`: no votes. | +| `network.votekick_minutes` | `5` | `HALO_NET_VOTEKICK_MINUTES` | When you host: the minutes that a player must play on the server before the player can start a vote to kick (`0` to `60`). To vote, a player must play for 2 minutes, or for this time if it is less. | +| `network.votekick_ban_minutes` | `30` | `HALO_NET_VOTEKICK_BAN_MINUTES` | When you host: the minutes that a player who is kicked by a vote cannot join again (`1` to `1440`). | | `network.coop_public` | `false` | `HALO_NET_COOP_PUBLIC` | `true`: an online co-op game (Create Game > Internet, a SINGLEPLAYER map) starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in co-op's Server Setup writes its choice here. Refer to "Server browser". | | `network.brokers_file` | `"brokers.txt"` | `HALO_NET_BROKERS_FILE` | The file of the public MQTT brokers that let the machines of an invite find each other, and that carry the listings of the server browser: next to `config.toml`, unless a full path. One `host:port` on each line, up to 4; `#` starts a comment. | | `network.stun_servers` | Google and Cloudflare | `HALO_NET_STUN` | The public STUN servers (`host:port`, with commas between them) that give the internet address of a machine. | @@ -585,6 +588,36 @@ Only machines with the invite can find the game: Refer to `NETCODE.md`. `kick ` drops the player the same way, but keeps nothing: no line in `bans.txt`, and the player can join again at once. In co-op, `bringto` brings every player to the host. +- Players can vote to kick a player (`network.votekick`). Hold the + scoreboard key, right-click to show the pointer, and click the name of + the player. Then click **Start a vote to kick**. Other players vote in + the same way, and see the vote on the scoreboard. `votekick ` in the developer console does the same. The host also gets + **Kick** and **Ban** in this menu: these do the same as the `kick` and + `ban` commands (click **Ban** two times). The host counts the votes, and + these rules prevent abuse: + - The vote passes when more than half of the players vote for it, and at + least two players. The player of the vote is counted, but cannot vote. + Thus, in a game of two equal teams, one team cannot kick a player of + the other team without help. + - The host counts one vote for each internet address (for internet + play, the real address of the player, not the address of the tunnel). + Two machines at one address, or with one hardware id, have one vote. + - To start a vote, a player must have played for + `network.votekick_minutes` (5) on this server. To vote, a player must + have played for 2 minutes (or less, if that setting is less). The host + counts the time. When a player joins again, the time starts again. + Players who cannot vote yet are not counted. + - The host only accepts a vote that comes on the connection of the + player, not a datagram, which another machine can send with the + address of the player. + - One vote runs at a time, for 45 seconds, with 30 seconds before the + next vote. If a vote fails, the player who started it cannot start a + vote for 5 minutes, and nobody can start a vote against the same player + for 10 minutes. + - Nobody can vote to kick a player of the host. + - A player kicked by a vote cannot join again for + `network.votekick_ban_minutes` (30), by address and hardware id. So that every player can be named, the host trims the spaces around a name and removes characters that draw as nothing. A letter with a mark is typed as the plain letter (`ban jose` for "José"). diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index 24cc1048..983aed36 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -3299,6 +3299,7 @@ void network_distributed_new_game( network_damage_new_game(); network_actors_new_game(); network_coop_new_game(); + network_votekick_new_game(); } /* after each tick (game_time.c) */ @@ -3354,6 +3355,7 @@ void network_distributed_tick( distributed_send_pickups(); if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0) distributed_send_game_state(NONE); + network_votekick_host_tick(); } else if (connection == _game_connection_network_client) { @@ -3403,6 +3405,7 @@ static boolean distributed_message_stale( case _distributed_message_damage_animations: case _distributed_message_coop_screen_effect: case _distributed_message_coop_device_states: + case _distributed_message_votekick_status: break; default: return FALSE; @@ -3419,7 +3422,7 @@ static boolean distributed_message_stale( /* (the host) a text shown in red on every machine's console: its own, and every client's (_distributed_message_notice) */ -static void distributed_send_notice( +void distributed_send_notice( char const *text) { struct @@ -3439,9 +3442,34 @@ static void distributed_send_notice( _distributed_to_clients_reliably); } +/* (the host) ... on one client's console only, or (NONE) the host's own */ +void distributed_send_notice_to_machine( + long machine_index, + char const *text) +{ + struct + { + struct distributed_message_header header; + char text[MAXIMUM_NOTICE_LENGTH]; + } message; + long length = csstrlen(text); + + if (length > MAXIMUM_NOTICE_LENGTH - 1) + length = MAXIMUM_NOTICE_LENGTH - 1; + csmemset(&message, 0, sizeof(message)); + csmemcpy(message.text, text, length); + if (machine_index == NONE) + { + console_warning("%s", message.text); + return; + } + distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_notice, 0, + (word)(sizeof(message.header) + length + 1)); +} + /* (the host) the names of a client machine's players, in ASCII, for a notice ("?" for what is not ASCII) */ -static void distributed_machine_player_names( +void distributed_machine_player_names( long machine_index, char *names, long size) @@ -3500,6 +3528,31 @@ void distributed_client_send_identity( _distributed_to_host_reliably); } +/* (the host) a client machine's address (host byte order) as it really is: +an internet play peer's endpoint for its stand-in in 100.64.0.0/10 (p2p.c, +which numbers the stand-in from the peer's identifier: a peer makes a new +one at will), 0 if that is not known */ +unsigned long distributed_real_address( + unsigned long address) +{ + if ((address & 0xFFC00000) == 0x64400000) + { + /* (p2p.c's in network byte order: the first number the lowest byte) */ + unsigned long network = (address >> 24) | ((address >> 8) & 0xFF00) | ((address << 8) & 0xFF0000) | + (address << 24); + unsigned long real = p2p_peer_endpoint_address(network); + + return (real >> 24) | ((real >> 8) & 0xFF00) | ((real << 8) & 0xFF0000) | (real << 24); + } + return address; +} + +unsigned long distributed_machine_real_address( + long machine_index) +{ + return distributed_real_address(network_game_server_machine_address(machine_index)); +} + /* (the host) a client machine's address as text: its real one, for an internet play peer's stand-in (p2p.c) */ static void distributed_address_text( @@ -3507,21 +3560,9 @@ static void distributed_address_text( char *text, long size) { - /* 100.64.0.0/10: an internet play peer's, by its real address (network - byte order: its first number the lowest byte) */ - if ((address & 0xFFC00000) == 0x64400000) - { - unsigned long network = (address >> 24) | ((address >> 8) & 0xFF00) | ((address << 8) & 0xFF0000) | - (address << 24); - unsigned long real = p2p_peer_endpoint_address(network); - - if (real) - { - snprintf(text, size, "%lu.%lu.%lu.%lu", real & 255, (real >> 8) & 255, (real >> 16) & 255, - (real >> 24) & 255); - return; - } - } + /* 100.64.0.0/10: an internet play peer's, by its real address */ + if ((address & 0xFFC00000) == 0x64400000 && distributed_real_address(address)) + address = distributed_real_address(address); if (address) { snprintf(text, size, "%lu.%lu.%lu.%lu", (address >> 24) & 255, (address >> 16) & 255, (address >> 8) & 255, @@ -3919,6 +3960,8 @@ void network_distributed_handle_message( case _distributed_message_coop_screen_effect: entry_size = network_coop_screen_effect_entry_size(); break; case _distributed_message_coop_device_states: entry_size = network_coop_device_state_entry_size(); break; case _distributed_message_pickups: entry_size = sizeof(struct distributed_pickup); break; + case _distributed_message_votekick: entry_size = network_votekick_request_entry_size(); break; + case _distributed_message_votekick_status: entry_size = network_votekick_status_entry_size(); break; case _distributed_message_player_inputs: entry_size = sizeof(struct distributed_player_input); break; case _distributed_message_relayed_actions: entry_size = DISTRIBUTED_RELAYED_ACTION_MINIMUM_SIZE; break; case _distributed_message_game_state: @@ -3942,6 +3985,8 @@ void network_distributed_handle_message( case _distributed_message_coop_presentation: case _distributed_message_coop_object_names: case _distributed_message_coop_skip_vote: + case _distributed_message_votekick: + case _distributed_message_votekick_status: if (header.count < 1) return; break; @@ -3960,6 +4005,7 @@ void network_distributed_handle_message( case _distributed_message_vehicle_prediction: case _distributed_message_player_inputs: case _distributed_message_coop_skip_vote: + case _distributed_message_votekick: if (machine_index == NONE || game_connection() != _game_connection_network_server) return; break; @@ -4014,6 +4060,12 @@ void network_distributed_handle_message( case _distributed_message_coop_skip_vote: network_coop_handle_skip_vote(machine_index, entries); break; + case _distributed_message_votekick: + network_votekick_handle_request(machine_index, entries, distributed_handling_stream_message); + break; + case _distributed_message_votekick_status: + network_votekick_handle_status(entries); + break; case _distributed_message_coop_device_groups: network_coop_handle_device_groups(entries, header.count); break; diff --git a/port/linux/game/network_distributed.h b/port/linux/game/network_distributed.h index 6fccb7f5..be3aef10 100644 --- a/port/linux/game/network_distributed.h +++ b/port/linux/game/network_distributed.h @@ -106,6 +106,12 @@ enum /* co-op: where the host's devices are (doors, elevators), when they move (network_coop.c) */ _distributed_message_coop_device_states = 77, + /* a player's vote to kick a player (network_votekick.c): to the host, + reliably, which takes it only from the voter's own stream */ + _distributed_message_votekick = 78, + /* the vote running as the host counts it, to each client, every second + and when it changes (unreliable: network_votekick.c) */ + _distributed_message_votekick_status = 79, NUMBER_OF_DISTRIBUTED_MESSAGES }; @@ -250,6 +256,31 @@ real distributed_angle_unpack(short value, boolean signed_angle); word distributed_vitality_pack(real value); real distributed_vitality_unpack(word value); +/* (the host) a text in red on every machine's console, or on one client's +(machine_index; NONE: the host's own) */ +void distributed_send_notice(char const *text); +void distributed_send_notice_to_machine(long machine_index, char const *text); +/* (the host) the names of a client machine's players, in ASCII */ +void distributed_machine_player_names(long machine_index, char *names, long size); +/* (the host) a client machine's real IPv4 address (host byte order): an +internet play peer's endpoint, not its stand-in; 0 if not known */ +unsigned long distributed_machine_real_address(long machine_index); +/* ... and the real address of a machine's address, as it joined from */ +unsigned long distributed_real_address(unsigned long address); + +/* ---------- prototypes/NETWORK_VOTEKICK.C */ + +void network_votekick_new_game(void); +/* after each tick, on the host */ +void network_votekick_host_tick(void); +/* (the host) a client's vote; from_stream: it came over the machine's own +stream (anything else is refused) */ +void network_votekick_handle_request(long machine_index, void const *entries, boolean from_stream); +word network_votekick_request_entry_size(void); +/* (a client) the host's count */ +void network_votekick_handle_status(void const *entries); +word network_votekick_status_entry_size(void); + /* ---------- prototypes/NETWORK_ACTORS.C */ void network_actors_new_game(void); diff --git a/port/linux/game/network_votekick.c b/port/linux/game/network_votekick.c new file mode 100644 index 00000000..9e4eb067 --- /dev/null +++ b/port/linux/game/network_votekick.c @@ -0,0 +1,1056 @@ +/* +NETWORK_VOTEKICK.C + +The players' vote to kick a player (port/linux/NETCODE.md). A player starts +one by picking another's name on the scoreboard (game_engine.c) or with the +console's "votekick "; the others vote the same way. The host alone +counts, and kicks the machine of the player once enough have voted. A +vote's power is against the game's other players, so the host decides +every part of it and trusts nothing a client says but which player it +votes against: + +- A vote counts only from its machine's own stream: a datagram's source + address is all that names its machine, and anyone can send one as from + another's. +- One vote a person. The votes and the players who may vote are counted by + their machines' real addresses (an internet play peer's endpoint, not its + stand-in, which the peer can make anew), and the votes again by hardware + id: two machines of one address or one hardware id are one vote. The + hardware id a machine tells the host joining (anyone's it likes), so it + only ever takes votes away: the players who may vote are counted by their + addresses alone, or a machine could copy another's id and so lower the + votes needed. +- Time played: on this host, counted in its own ticks of game, not told + by the client: votekick_minutes (config.toml) to start a vote, and a + part of that to vote, so that a player who has only just joined (or a + cheater's second machine) neither starts nor counts. Joining again starts + over. +- More than half of the players who may vote, the player voted against + counted among them (who never votes for it), and at least two: one team + alone kicks nobody from the other in an even game, and two people never + kick each other. +- One vote at a time, for VOTE_SECONDS, then a pause before the next. A + player whose vote failed starts none for STARTER_COOLDOWN_SECONDS, and a + player a vote failed against is not voted against again for + TARGET_PROTECTION_SECONDS, both by address and hardware id. +- The host's own players are never voted against; the host kicks and bans + as it likes (network_server_manager.c), and its machine votes as any + other. +- Only the host speaks: what a client did wrong is told to it alone, at + most once a second; the votes to everyone, only as they change. +- A machine kicked by a vote is kept out of the host's games, by address + and hardware id, for votekick_ban_minutes. + +A client hears the host's count every second (and as it changes), for the +scoreboard. A build without votes drops both messages as kinds it does not +know: its players cannot vote, and count as players who did not. +*/ + +#include "cseries.h" +#include "cseries/errors.h" +#include "memory/data.h" +#include "game/game.h" +#include "game/players.h" +#include "networking/network_game_globals.h" +#include "network_distributed.h" +#include "network_votekick.h" + +#include + +/* port_config.c's, network_server_manager.c's, cseries_windows.c's */ +int config_boolean(const char *name); +long config_integer(const char *name); +void network_game_server_kick_machine(long machine_index, boolean kept_out); +boolean network_game_server_kick_machine_of_player(long machine_index, boolean ban); +char const *network_game_server_machine_hardware_id(long machine_index); +void p2p_hardware_id_sanitize(char *destination, int size, const char *source); +unsigned long system_milliseconds(void); +void console_warning(const char *format, ...); + +/* ---------- constants */ + +enum +{ + /* p2p.h's P2P_HARDWARE_ID_SIZE */ + VOTEKICK_HARDWARE_ID_SIZE = 33, + VOTE_SECONDS = 45, + /* after a vote, before the next */ + VOTE_GAP_SECONDS = 30, + STARTER_COOLDOWN_SECONDS = 5 * 60, + TARGET_PROTECTION_SECONDS = 10 * 60, + /* the time played to vote, at most (and no more than to start one) */ + VOTER_MINIMUM_SECONDS = 2 * 60, + /* a machine's requests the host takes, at most one this often */ + REQUEST_INTERVAL_MILLISECONDS = 1000, + MINIMUM_VOTES = 2, + /* the addresses (and hardware ids) remembered: kept out, cooling down, + protected; the oldest forgotten first */ + MAXIMUM_REMEMBERED = 32, + /* a client forgets the host's count it has not heard again this long */ + STATUS_TIMEOUT_MILLISECONDS = 3000, + MAXIMUM_VOTEKICK_MACHINES = HALO_PORT_MAXIMUM_NETWORK_MACHINES, +}; + +/* the status's flags */ +enum +{ + /* this machine (the one told) has voted */ + _votekick_status_voted_bit, + /* ... may vote */ + _votekick_status_may_vote_bit, +}; + +/* ---------- structures */ + +struct distributed_votekick_request +{ + /* the player voted against (absolute index) */ + byte player_index; + byte pad[3]; +}; + +struct distributed_votekick_status +{ + /* the player voted against (absolute index), NO_PLAYER for no vote */ + byte player_index; + byte votes; + byte needed; + byte seconds_left; + byte flags; + byte pad[3]; +}; + +typedef char distributed_votekick_request_size_assert[sizeof(struct distributed_votekick_request) == 4 ? 1 : -1]; +typedef char distributed_votekick_status_size_assert[sizeof(struct distributed_votekick_status) == 8 ? 1 : -1]; + +/* a machine as the count sees it (votekick_count) */ +struct votekick_voter +{ + /* its real address (host byte order; 0: not known) */ + unsigned long address; + char hardware_id[VOTEKICK_HARDWARE_ID_SIZE]; + /* the host's own machine */ + boolean host; + /* the machine of the player voted against */ + boolean target; + /* has played long enough to vote (the host's, always) */ + boolean eligible; + boolean voted; +}; + +/* an address and hardware id remembered until a time (system_milliseconds) */ +struct votekick_identity +{ + unsigned long address; + char hardware_id[VOTEKICK_HARDWARE_ID_SIZE]; + unsigned long until; + boolean valid; +}; + +/* ---------- globals */ + +/* the host: each machine slot's time in game, in the host's ticks, since it +joined (network_votekick_machine_joined), and how many machines joined at +it (a vote names the one it was) */ +static long votekick_played_ticks[MAXIMUM_VOTEKICK_MACHINES]; +static long votekick_generations[MAXIMUM_VOTEKICK_MACHINES]; +/* ... when it last asked something (system_milliseconds), and whether it +has */ +static unsigned long votekick_request_times[MAXIMUM_VOTEKICK_MACHINES]; +static boolean votekick_requested[MAXIMUM_VOTEKICK_MACHINES]; + +/* the host: the vote */ +static struct +{ + boolean active; + /* the machine voted against, and its players' first (for the clients) */ + long target_machine; + long target_generation; + short target_player; + unsigned long target_address; + char target_hardware_id[VOTEKICK_HARDWARE_ID_SIZE]; + char target_names[64]; + /* who started it, cooling down if it fails */ + unsigned long starter_address; + char starter_hardware_id[VOTEKICK_HARDWARE_ID_SIZE]; + unsigned long started_at; + /* the machines that voted (as they were at their slots), the host's */ + boolean voted[MAXIMUM_VOTEKICK_MACHINES]; + long voted_generations[MAXIMUM_VOTEKICK_MACHINES]; + boolean host_voted; + /* as last counted, and told */ + short votes; + short needed; +} votekick; + +/* the host: no vote before this (a vote's end and the gap after) */ +static unsigned long votekick_next_allowed; +static boolean votekick_gap; + +static struct votekick_identity votekick_kept_out_identities[MAXIMUM_REMEMBERED]; +static struct votekick_identity votekick_cooling_starters[MAXIMUM_REMEMBERED]; +static struct votekick_identity votekick_protected_targets[MAXIMUM_REMEMBERED]; + +/* a client: the host's count, as last heard (the time: 0, none) */ +static struct distributed_votekick_status votekick_status; +static unsigned long votekick_status_heard; + +/* ---------- the count (pure: tools/harness/tests/votekick_count.c) */ + +/* whether two machines are one person's by address: the host's own is +only itself; an address not known, no other's */ +static boolean votekick_same_address( + struct votekick_voter const *a, + struct votekick_voter const *b) +{ + if (a->host || b->host) + return a->host && b->host; + return a->address != 0 && a->address == b->address; +} + +/* ... or by hardware id too (none told: no other's) */ +static boolean votekick_same_person( + struct votekick_voter const *a, + struct votekick_voter const *b) +{ + if (votekick_same_address(a, b)) + return TRUE; + return !a->host && !b->host && a->hardware_id[0] && !csstrcmp(a->hardware_id, b->hardware_id); +} + +/* whether a machine is one of those who may vote: played long enough, and +its address known (else it could not be told from another) */ +static boolean votekick_may_vote( + struct votekick_voter const *voter) +{ + return voter->eligible && !voter->target && (voter->host || voter->address != 0); +} + +/* the players who may vote (by address: the target's among them, once) and +the votes for (by address and hardware id, none of the target's person) */ +static void votekick_count( + struct votekick_voter const *voters, + short count, + short *votes, + short *electorate) +{ + short index; + short other; + + *votes = 0; + *electorate = 0; + for (index = 0; index < count; index++) + { + boolean counted = votekick_may_vote(&voters[index]) || voters[index].target; + + /* (an address counted once: the first of its machines) */ + for (other = 0; counted && other < index; other++) + { + if ((votekick_may_vote(&voters[other]) || voters[other].target) && + votekick_same_address(&voters[index], &voters[other])) + { + counted = FALSE; + } + } + if (counted) + (*electorate)++; + } + for (index = 0; index < count; index++) + { + boolean counted = voters[index].voted && votekick_may_vote(&voters[index]); + + for (other = 0; counted && other < count; other++) + { + /* (not the target's person, whatever it says; a person once) */ + if (voters[other].target && votekick_same_person(&voters[index], &voters[other])) + counted = FALSE; + if (other < index && voters[other].voted && votekick_may_vote(&voters[other]) && + votekick_same_person(&voters[index], &voters[other])) + { + counted = FALSE; + } + } + if (counted) + (*votes)++; + } +} + +/* the votes that kick: more than half of those who may vote, and at least +MINIMUM_VOTES */ +static short votekick_votes_needed( + short electorate) +{ + short needed = (short)(electorate / 2 + 1); + + return needed < MINIMUM_VOTES ? MINIMUM_VOTES : needed; +} + +/* whether a time (system_milliseconds) has come; times wrap, so by their +difference */ +static boolean votekick_time_reached( + unsigned long now, + unsigned long time) +{ + return (long)(now - time) >= 0; +} + +/* ---------- remembered identities */ + +static void votekick_remember( + struct votekick_identity *identities, + unsigned long address, + char const *hardware_id, + unsigned long milliseconds) +{ + unsigned long now = system_milliseconds(); + short index; + short oldest = 0; + + /* (a free entry, one gone by, else the one that ends soonest) */ + for (index = 0; index < MAXIMUM_REMEMBERED; index++) + { + if (!identities[index].valid || votekick_time_reached(now, identities[index].until)) + { + oldest = index; + break; + } + if ((long)(identities[index].until - identities[oldest].until) < 0) + oldest = index; + } + identities[oldest].valid = TRUE; + identities[oldest].address = address; + p2p_hardware_id_sanitize(identities[oldest].hardware_id, VOTEKICK_HARDWARE_ID_SIZE, hardware_id); + identities[oldest].until = now + milliseconds; +} + +/* whether an address or hardware id is remembered (not gone by); the +milliseconds left */ +static boolean votekick_remembered( + struct votekick_identity const *identities, + unsigned long address, + char const *hardware_id, + unsigned long *milliseconds_left) +{ + unsigned long now = system_milliseconds(); + short index; + + for (index = 0; index < MAXIMUM_REMEMBERED; index++) + { + struct votekick_identity const *identity = &identities[index]; + + if (!identity->valid || votekick_time_reached(now, identity->until)) + continue; + if ((address && identity->address == address) || + (hardware_id && hardware_id[0] && identity->hardware_id[0] && + !csstrcmp(hardware_id, identity->hardware_id))) + { + if (milliseconds_left) + *milliseconds_left = identity->until - now; + return TRUE; + } + } + return FALSE; +} + +/* ---------- the host */ + +static boolean votekick_host( + void) +{ + return game_connection() == _game_connection_network_server; +} + +/* the time played to start a vote, and to vote (in ticks) */ +static long votekick_starter_ticks( + void) +{ + long minutes = config_integer("network.votekick_minutes"); + + return PIN(minutes, 0, 60) * 60 * TICKS_PER_SECOND; +} + +static long votekick_voter_ticks( + void) +{ + return MIN(votekick_starter_ticks(), VOTER_MINIMUM_SECONDS * TICKS_PER_SECOND); +} + +static boolean votekick_machine_valid( + long machine_index) +{ + return machine_index >= 0 && machine_index < MAXIMUM_VOTEKICK_MACHINES; +} + +/* (network_server_manager.c) a machine joined the host at the slot: its +time played starts over, and no vote of the machine before is its */ +void network_votekick_machine_joined( + long machine_index) +{ + if (!votekick_machine_valid(machine_index)) + return; + votekick_played_ticks[machine_index] = 0; + votekick_generations[machine_index]++; + votekick_requested[machine_index] = FALSE; + votekick.voted[machine_index] = FALSE; +} + +/* (network_server_manager.c) whether a machine of the address (as it +joins from: host byte order) and hardware id is kept out, kicked by a vote */ +boolean network_votekick_kept_out( + unsigned long address, + char const *hardware_id) +{ + return votekick_remembered(votekick_kept_out_identities, distributed_real_address(address), hardware_id, NULL); +} + +/* the client machines in the game with a player who has not quit, and the +host's own if it has one (NONE); their count */ +static short votekick_machines( + long *machine_indices, + short maximum) +{ + struct data_iterator iterator; + struct player_datum *player; + short count = 0; + + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL) + { + long machine_index = distributed_player_machine((short)DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)); + short index; + + if (player->quit_out_of_game || (machine_index != NONE && !votekick_machine_valid(machine_index))) + continue; + for (index = 0; index < count && machine_indices[index] != machine_index; index++) + ; + if (index == count && count < maximum) + machine_indices[count++] = machine_index; + } + return count; +} + +/* a machine (NONE: the host's own) as the count sees it */ +static void votekick_voter_get( + long machine_index, + struct votekick_voter *voter) +{ + csmemset(voter, 0, sizeof(*voter)); + if (machine_index == NONE) + { + voter->host = TRUE; + voter->eligible = TRUE; + voter->voted = votekick.active && votekick.host_voted; + return; + } + voter->address = distributed_machine_real_address(machine_index); + p2p_hardware_id_sanitize(voter->hardware_id, VOTEKICK_HARDWARE_ID_SIZE, + network_game_server_machine_hardware_id(machine_index)); + voter->eligible = votekick_played_ticks[machine_index] >= votekick_voter_ticks(); + voter->target = votekick.active && machine_index == votekick.target_machine; + voter->voted = votekick.active && votekick.voted[machine_index] && + votekick.voted_generations[machine_index] == votekick_generations[machine_index]; +} + +/* the target's machine still the one voted against, in the game */ +static boolean votekick_target_present( + void) +{ + long machine_indices[MAXIMUM_VOTEKICK_MACHINES + 1]; + short count = votekick_machines(machine_indices, (short)NUMBEROF(machine_indices)); + short index; + + if (votekick_generations[votekick.target_machine] != votekick.target_generation) + return FALSE; + for (index = 0; index < count; index++) + { + if (machine_indices[index] == votekick.target_machine) + return TRUE; + } + return FALSE; +} + +/* the count now, of the machines in the game (the target's among them: +votekick_target_present) */ +static void votekick_recount( + void) +{ + struct votekick_voter voters[MAXIMUM_VOTEKICK_MACHINES + 1]; + long machine_indices[MAXIMUM_VOTEKICK_MACHINES + 1]; + short count = votekick_machines(machine_indices, (short)NUMBEROF(machine_indices)); + short electorate; + short index; + + for (index = 0; index < count; index++) + votekick_voter_get(machine_indices[index], &voters[index]); + votekick_count(voters, count, &votekick.votes, &electorate); + votekick.needed = votekick_votes_needed(electorate); +} + +/* the name of a machine's players for a notice (NONE: the host's own) */ +static void votekick_names( + long machine_index, + char *names, + long size) +{ + distributed_machine_player_names(machine_index, names, size); +} + +/* the count to every client: what it may do, and has */ +static void votekick_send_status( + void) +{ + long machine_indices[MAXIMUM_VOTEKICK_MACHINES]; + short count = distributed_client_machines(machine_indices, MAXIMUM_VOTEKICK_MACHINES); + unsigned long now = system_milliseconds(); + short index; + + for (index = 0; index < count; index++) + { + struct + { + struct distributed_message_header header; + struct distributed_votekick_status status; + } message; + struct votekick_voter voter; + + csmemset(&message, 0, sizeof(message)); + message.status.player_index = NO_PLAYER; + if (votekick.active) + { + unsigned long elapsed = now - votekick.started_at; + long left = VOTE_SECONDS - (long)(elapsed / 1000); + + votekick_voter_get(machine_indices[index], &voter); + message.status.player_index = distributed_player_to_byte(votekick.target_player); + message.status.votes = (byte)PIN(votekick.votes, 0, 255); + message.status.needed = (byte)PIN(votekick.needed, 0, 255); + message.status.seconds_left = (byte)PIN(left, 0, 255); + if (voter.voted) + message.status.flags |= FLAG(_votekick_status_voted_bit); + if (votekick_may_vote(&voter)) + message.status.flags |= FLAG(_votekick_status_may_vote_bit); + } + distributed_send_to_machine(machine_indices[index], &message, _distributed_message_votekick_status, 1, + (word)sizeof(message)); + } +} + +/* the vote over (passed or not: its notice said), and none for a while */ +static void votekick_end( + void) +{ + votekick.active = FALSE; + votekick_next_allowed = system_milliseconds() + VOTE_GAP_SECONDS * 1000; + votekick_gap = TRUE; + votekick_send_status(); +} + +static void votekick_pass( + void) +{ + char notice[160]; + long ban_minutes = PIN(config_integer("network.votekick_ban_minutes"), 1, 24 * 60); + + snprintf(notice, sizeof(notice), "%s kicked by vote (%d of %d), kept out for %ld minute%s", + votekick.target_names, votekick.votes, votekick.needed, ban_minutes, ban_minutes == 1 ? "" : "s"); + distributed_send_notice(notice); + votekick_remember(votekick_kept_out_identities, votekick.target_address, votekick.target_hardware_id, + (unsigned long)ban_minutes * 60 * 1000); + network_game_server_kick_machine(votekick.target_machine, FALSE); + votekick_end(); +} + +static void votekick_fail( + void) +{ + char notice[160]; + + snprintf(notice, sizeof(notice), "The vote to kick %s failed (%d of %d)", votekick.target_names, votekick.votes, + votekick.needed); + distributed_send_notice(notice); + votekick_remember(votekick_cooling_starters, votekick.starter_address, votekick.starter_hardware_id, + STARTER_COOLDOWN_SECONDS * 1000UL); + votekick_remember(votekick_protected_targets, votekick.target_address, votekick.target_hardware_id, + TARGET_PROTECTION_SECONDS * 1000UL); + votekick_end(); +} + +/* a refusal, to the asking machine alone */ +static void votekick_refuse( + long machine_index, + char const *text) +{ + distributed_send_notice_to_machine(machine_index, text); +} + +/* a machine's vote against a player (NONE: the host's own machine): a new +vote, or one for the vote running */ +static void votekick_host_request( + long machine_index, + short player_index) +{ + struct player_datum *player = distributed_player(player_index); + long target_machine = player ? distributed_player_machine(player_index) : NONE; + struct votekick_voter voter; + struct votekick_voter target; + char names[64]; + char notice[160]; + unsigned long now = system_milliseconds(); + unsigned long left; + + if (!config_boolean("network.votekick")) + { + votekick_refuse(machine_index, "votekick: the host has turned votes off"); + return; + } + if (machine_index == NONE && local_player_get_next(NONE) == NONE) + return; + if (!player || player->quit_out_of_game) + { + votekick_refuse(machine_index, "votekick: no such player"); + return; + } + if (target_machine == NONE) + { + votekick_refuse(machine_index, "votekick: the host's players cannot be voted out"); + return; + } + if (!votekick_machine_valid(target_machine) || target_machine == machine_index) + { + votekick_refuse(machine_index, "votekick: not against your own players"); + return; + } + votekick_voter_get(machine_index, &voter); + votekick_voter_get(target_machine, &target); + target.target = TRUE; + if (votekick_same_person(&voter, &target)) + { + votekick_refuse(machine_index, "votekick: not against your own machine"); + return; + } + if (!voter.host && !voter.address) + { + votekick_refuse(machine_index, "votekick: the host does not know your address"); + return; + } + /* a vote for the vote running */ + if (votekick.active) + { + if (target_machine != votekick.target_machine) + { + snprintf(notice, sizeof(notice), "votekick: wait for the vote to kick %s to end", votekick.target_names); + votekick_refuse(machine_index, notice); + return; + } + if (!votekick_may_vote(&voter)) + { + votekick_refuse(machine_index, "votekick: you have not played long enough on this server to vote"); + return; + } + if (voter.voted) + return; + if (machine_index == NONE) + votekick.host_voted = TRUE; + else + { + votekick.voted[machine_index] = TRUE; + votekick.voted_generations[machine_index] = votekick_generations[machine_index]; + } + { + short votes = votekick.votes; + + votekick_recount(); + /* (told only as the count changes: a second machine of one + person's adds nothing) */ + if (votekick.votes != votes && votekick.votes < votekick.needed) + { + votekick_names(machine_index, names, sizeof(names)); + snprintf(notice, sizeof(notice), "%s voted to kick %s (%d of %d)", names, votekick.target_names, + votekick.votes, votekick.needed); + distributed_send_notice(notice); + } + } + if (votekick.votes >= votekick.needed) + votekick_pass(); + else + votekick_send_status(); + return; + } + /* a new vote */ + if (votekick_gap && !votekick_time_reached(now, votekick_next_allowed)) + { + snprintf(notice, sizeof(notice), "votekick: the next vote can start in %lu seconds", + (votekick_next_allowed - now + 999) / 1000); + votekick_refuse(machine_index, notice); + return; + } + if (!voter.host && votekick_played_ticks[machine_index] < votekick_starter_ticks()) + { + long minutes = votekick_starter_ticks() / (60 * TICKS_PER_SECOND); + + snprintf(notice, sizeof(notice), + "votekick: play %ld minute%s on this server to start a vote (%ld played)", minutes, + minutes == 1 ? "" : "s", votekick_played_ticks[machine_index] / (60 * TICKS_PER_SECOND)); + votekick_refuse(machine_index, notice); + return; + } + if (!voter.host && votekick_remembered(votekick_cooling_starters, voter.address, voter.hardware_id, &left)) + { + snprintf(notice, sizeof(notice), "votekick: your last vote failed: wait %lu seconds", (left + 999) / 1000); + votekick_refuse(machine_index, notice); + return; + } + if (votekick_remembered(votekick_protected_targets, target.address, target.hardware_id, &left)) + { + snprintf(notice, sizeof(notice), "votekick: a vote against that player failed lately: wait %lu seconds", + (left + 999) / 1000); + votekick_refuse(machine_index, notice); + return; + } + csmemset(&votekick, 0, sizeof(votekick)); + votekick.active = TRUE; + votekick.target_machine = target_machine; + votekick.target_generation = votekick_generations[target_machine]; + votekick.target_player = player_index; + votekick.target_address = target.address; + csstrcpy(votekick.target_hardware_id, target.hardware_id); + votekick_names(target_machine, votekick.target_names, sizeof(votekick.target_names)); + votekick.starter_address = voter.address; + csstrcpy(votekick.starter_hardware_id, voter.hardware_id); + votekick.started_at = now; + if (machine_index == NONE) + votekick.host_voted = TRUE; + else + { + votekick.voted[machine_index] = TRUE; + votekick.voted_generations[machine_index] = votekick_generations[machine_index]; + } + votekick_recount(); + votekick_names(machine_index, names, sizeof(names)); + snprintf(notice, sizeof(notice), "%s started a vote to kick %s (%d of %d): open the scores and pick the name to vote", + names, votekick.target_names, votekick.votes, votekick.needed); + distributed_send_notice(notice); + error(_error_log, "votekick: started by machine %ld against machine %ld", machine_index, target_machine); + if (votekick.votes >= votekick.needed) + votekick_pass(); + else + votekick_send_status(); +} + +void network_votekick_handle_request( + long machine_index, + void const *entries, + boolean from_stream) +{ + struct distributed_votekick_request request; + unsigned long now = system_milliseconds(); + + if (!votekick_host() || !votekick_machine_valid(machine_index) || !from_stream) + return; + /* (at most one a second: more are dropped, untold) */ + if (votekick_requested[machine_index] && + !votekick_time_reached(now, votekick_request_times[machine_index] + REQUEST_INTERVAL_MILLISECONDS)) + { + return; + } + votekick_requested[machine_index] = TRUE; + votekick_request_times[machine_index] = now; + csmemcpy(&request, entries, sizeof(request)); + if (request.player_index >= MAXIMUM_TRACKED_PLAYERS) + return; + votekick_host_request(machine_index, request.player_index); +} + +word network_votekick_request_entry_size( + void) +{ + return sizeof(struct distributed_votekick_request); +} + +void network_votekick_new_game( + void) +{ + /* (a vote ends with its game, failing nobody; the time played stays) */ + if (votekick.active && votekick_host()) + distributed_send_notice("The vote ended with the game"); + votekick.active = FALSE; + csmemset(&votekick_status, 0, sizeof(votekick_status)); + votekick_status_heard = 0; +} + +void network_votekick_host_tick( + void) +{ + long machine_indices[MAXIMUM_VOTEKICK_MACHINES]; + short count = distributed_client_machines(machine_indices, MAXIMUM_VOTEKICK_MACHINES); + short index; + + for (index = 0; index < count; index++) + { + if (votekick_machine_valid(machine_indices[index])) + votekick_played_ticks[machine_indices[index]]++; + } + if (!votekick.active) + return; + if (!votekick_target_present()) + { + char notice[160]; + + snprintf(notice, sizeof(notice), "%s left: the vote ended", votekick.target_names); + distributed_send_notice(notice); + votekick_end(); + return; + } + /* (those gone counted no more, and those who have since played long + enough to vote among those who may) */ + if (game_time_get() % TICKS_PER_SECOND == 0) + { + votekick_recount(); + if (votekick.votes >= votekick.needed) + { + votekick_pass(); + return; + } + if (votekick_time_reached(system_milliseconds(), votekick.started_at + VOTE_SECONDS * 1000)) + { + votekick_fail(); + return; + } + votekick_send_status(); + } +} + +/* ---------- a client */ + +void network_votekick_handle_status( + void const *entries) +{ + if (game_connection() != _game_connection_network_client) + return; + csmemcpy(&votekick_status, entries, sizeof(votekick_status)); + votekick_status_heard = system_milliseconds(); + if (votekick_status.player_index != NO_PLAYER && !distributed_player(votekick_status.player_index)) + votekick_status.player_index = NO_PLAYER; + /* (0: none heard) */ + if (!votekick_status_heard) + votekick_status_heard = 1; +} + +word network_votekick_status_entry_size( + void) +{ + return sizeof(struct distributed_votekick_status); +} + +/* ---------- every machine (network_votekick.h) */ + +boolean network_votekick_get_status( + struct network_votekick_status *status) +{ + csmemset(status, 0, sizeof(*status)); + status->player_index = NONE; + if (votekick_host()) + { + struct votekick_voter voter; + unsigned long elapsed; + + if (!votekick.active) + return FALSE; + votekick_voter_get(NONE, &voter); + elapsed = system_milliseconds() - votekick.started_at; + status->player_index = votekick.target_player; + status->votes = votekick.votes; + status->needed = votekick.needed; + status->seconds_left = (short)MAX(0, VOTE_SECONDS - (long)(elapsed / 1000)); + status->voted = votekick.host_voted; + status->may_vote = TRUE; + return TRUE; + } + if (game_connection() != _game_connection_network_client || !votekick_status_heard || + votekick_time_reached(system_milliseconds(), votekick_status_heard + STATUS_TIMEOUT_MILLISECONDS) || + votekick_status.player_index == NO_PLAYER) + { + return FALSE; + } + status->player_index = distributed_player_from_byte(votekick_status.player_index) == NONE ? NONE : + votekick_status.player_index; + status->votes = votekick_status.votes; + status->needed = votekick_status.needed; + status->seconds_left = votekick_status.seconds_left; + status->voted = TEST_FLAG(votekick_status.flags, _votekick_status_voted_bit); + status->may_vote = TEST_FLAG(votekick_status.flags, _votekick_status_may_vote_bit); + return status->player_index != NONE; +} + +/* a player's name in ASCII (player_name_character_ascii), at most size */ +static void votekick_player_name( + struct player_datum const *player, + char *text, + long size) +{ + long index; + + for (index = 0; index < (long)NUMBEROF(player->name) && player->name[index] && index < size - 1; index++) + text[index] = player_name_character_ascii(player->name[index]); + text[index] = 0; +} + +/* whether a name begins with the text, in either case; and is it */ +static boolean votekick_name_begins_with( + char const *name, + char const *text, + boolean whole) +{ + for (; *text; name++, text++) + { + char a = *name >= 'A' && *name <= 'Z' ? *name - 'A' + 'a' : *name; + char b = *text >= 'A' && *text <= 'Z' ? *text - 'A' + 'a' : *text; + + if (!*name || a != b) + return FALSE; + } + return !whole || !*name; +} + +short network_votekick_matching_player_names( + char const *text, + char (*names)[VOTEKICK_NAME_TEXT_SIZE], + short maximum_count) +{ + struct data_iterator iterator; + struct player_datum *player; + short count = 0; + + if (!network_votekick_available()) + return 0; + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL && count < maximum_count) + { + if (player->quit_out_of_game || player->local_player_index != NONE) + continue; + votekick_player_name(player, names[count], VOTEKICK_NAME_TEXT_SIZE); + if (votekick_name_begins_with(names[count], text, FALSE)) + count++; + } + return count; +} + +boolean network_votekick_player_named( + char const *text) +{ + struct data_iterator iterator; + struct player_datum *player; + long found = NONE; + short matches = 0; + short exact = 0; + long exact_found = NONE; + + if (!network_votekick_available()) + { + console_warning("votekick: only in a network game"); + return FALSE; + } + if (!text[0]) + { + console_warning("votekick: give a player's name (Tab completes it)"); + return FALSE; + } + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL) + { + char name[VOTEKICK_NAME_TEXT_SIZE]; + + if (player->quit_out_of_game) + continue; + votekick_player_name(player, name, sizeof(name)); + if (votekick_name_begins_with(name, text, TRUE)) + { + exact++; + exact_found = iterator.datum_index; + } + if (votekick_name_begins_with(name, text, FALSE)) + { + matches++; + found = iterator.datum_index; + } + } + if (exact == 1) + { + found = exact_found; + matches = 1; + } + if (matches > 1) + { + console_warning("votekick: %d players' names begin with \"%s\": give more of it", matches, text); + return FALSE; + } + if (matches == 0) + { + console_warning("votekick: no player's name begins with \"%s\"", text); + return FALSE; + } + return network_votekick_request((short)DATUM_INDEX_TO_ABSOLUTE_INDEX(found)); +} + +/* (the host) its Kick and Ban on the scoreboard: the player's machine */ +boolean network_votekick_host_kick( + short player_index, + boolean ban) +{ + long machine_index; + + if (!votekick_host() || !distributed_player(player_index)) + return FALSE; + machine_index = distributed_player_machine(player_index); + if (machine_index == NONE) + { + console_warning("%s: not a player of the host's own machine", ban ? "ban" : "kick"); + return FALSE; + } + return network_game_server_kick_machine_of_player(machine_index, ban); +} + +boolean network_votekick_host( + void) +{ + return votekick_host(); +} + +boolean network_votekick_available( + void) +{ + return game_connection() == _game_connection_network_client || + game_connection() == _game_connection_network_server; +} + +boolean network_votekick_request( + short player_index) +{ + struct + { + struct distributed_message_header header; + struct distributed_votekick_request request; + } message; + + if (!distributed_player(player_index)) + return FALSE; + if (distributed_player_is_local(player_index)) + { + console_warning("votekick: not against your own players"); + return FALSE; + } + if (votekick_host()) + { + votekick_host_request(NONE, player_index); + return TRUE; + } + if (game_connection() != _game_connection_network_client) + { + console_warning("votekick: only in a network game"); + return FALSE; + } + csmemset(&message, 0, sizeof(message)); + message.request.player_index = distributed_player_to_byte(player_index); + distributed_send(&message, _distributed_message_votekick, 1, (word)sizeof(message), _distributed_to_host_reliably); + return TRUE; +} diff --git a/port/linux/game/network_votekick.h b/port/linux/game/network_votekick.h new file mode 100644 index 00000000..72e0a679 --- /dev/null +++ b/port/linux/game/network_votekick.h @@ -0,0 +1,63 @@ +/* +NETWORK_VOTEKICK.H + +The players' vote to kick a player (network_votekick.c): what the +scoreboard (game_engine.c), the console (hs.c) and the host's server +(network_server_manager.c) ask of it. +*/ + +#ifndef __NETWORK_VOTEKICK_H +#define __NETWORK_VOTEKICK_H +#pragma once + +/* ---------- constants */ + +enum +{ + /* a player's name in ASCII, with its end */ + VOTEKICK_NAME_TEXT_SIZE = 16, +}; + +/* ---------- structures */ + +/* the vote running, as the host counts it */ +struct network_votekick_status +{ + /* the player voted against (absolute index) */ + short player_index; + short votes; + short needed; + short seconds_left; + /* this machine has voted; may vote */ + boolean voted; + boolean may_vote; +}; + +/* ---------- prototypes/NETWORK_VOTEKICK.C */ + +/* whether this machine is in a network game, where votes are */ +boolean network_votekick_available(void); +/* the vote running; FALSE for none */ +boolean network_votekick_get_status(struct network_votekick_status *status); +/* this machine votes against the player (absolute index): starts a vote, +or votes in the one running; the host answers on the console */ +boolean network_votekick_request(short player_index); +/* whether this machine hosts the game (its scoreboard offers Kick and Ban) */ +boolean network_votekick_host(void); +/* (the host) kicks, or bans, the machine of the player (absolute index): +its kick and ban commands' (network_server_manager.c) */ +boolean network_votekick_host_kick(short player_index, boolean ban); +/* the console's "votekick ": the player of that name (or the one +whose name begins with it) voted against */ +boolean network_votekick_player_named(char const *text); +/* the names of the game's players but this machine's that begin with the +text (the command's completion: console.c); how many */ +short network_votekick_matching_player_names(char const *text, char (*names)[VOTEKICK_NAME_TEXT_SIZE], + short maximum_count); +/* (the host's server) a machine joined at the slot */ +void network_votekick_machine_joined(long machine_index); +/* (the host's server) whether a machine of the address (host byte order) +and hardware id is kept out, kicked by a vote */ +boolean network_votekick_kept_out(unsigned long address, char const *hardware_id); + +#endif // __NETWORK_VOTEKICK_H diff --git a/port/linux/include/halo_ui_pointer.h b/port/linux/include/halo_ui_pointer.h index bc7035a1..a0b4bb93 100644 --- a/port/linux/include/halo_ui_pointer.h +++ b/port/linux/include/halo_ui_pointer.h @@ -28,4 +28,11 @@ for aiming when not; while menus are active returns nonzero and what the pointer did since the last call. Always 0 on Android. */ int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer); +/* the open scoreboard's pointer (game_engine.c), offered (a network game's) +or not: 1 while a right click has freed it, with where it is and what it +did since the last call, in the screen's coordinates (the game's drawing, +not the menus' centered 640); 0 while it is not; -1 where there is none +(Android) */ +int halo_scoreboard_pointer_update(int offered, struct halo_ui_pointer *pointer); + #endif diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 912e3f1a..886ab1e1 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -1801,11 +1801,19 @@ int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer) platform_menus_set_active(menus_active != 0); return 0; } + +int halo_scoreboard_pointer_update(int offered, struct halo_ui_pointer *pointer) +{ + (void)offered; + (void)pointer; + return -1; +} #else /* a point in the window, as SDL reports it, in the menus' coordinates: the inverse of the letterboxed display blit at presentation, the screen's -width and the menus' centering (halo_screen_ui_offset) */ -static void ui_point_from_window(float window_x, float window_y, short *x, short *y) +width and the menus' centering (halo_screen_ui_offset); or, not centered, +the screen's (the game's drawing: the scoreboard's) */ +static void ui_point_from_window_on(float window_x, float window_y, int centered, short *x, short *y) { struct render_target_entry *back_buffer = render_target_get(&device.back_buffer); int window_width, window_height, pixel_width, pixel_height, width, height, left, top; @@ -1829,10 +1837,29 @@ static void ui_point_from_window(float window_x, float window_y, short *x, short top = (pixel_height - height) / 2; screen_x = (window_x * pixel_width / window_width - left) * (float)back_buffer->target.width / (float)width; screen_y = (window_y * pixel_height / window_height - top) * (float)back_buffer->target.height / (float)height; - *x = (short)floorf(screen_x - (float)(halo_screen_width() - 640) / 2.0f); + *x = (short)floorf(screen_x - (centered ? (float)(halo_screen_width() - 640) / 2.0f : 0.0f)); *y = (short)floorf(screen_y); } +static void ui_point_from_window(float window_x, float window_y, short *x, short *y) +{ + ui_point_from_window_on(window_x, window_y, TRUE, x, y); +} + +int halo_scoreboard_pointer_update(int offered, struct halo_ui_pointer *pointer) +{ + struct platform_ui_pointer state; + + memset(pointer, 0, sizeof(*pointer)); + if (!platform_scoreboard_pointer(offered != 0, &state) || !device.gl_ready) + return 0; + ui_point_from_window_on(state.x, state.y, FALSE, &pointer->x, &pointer->y); + ui_point_from_window_on(state.click_x, state.click_y, FALSE, &pointer->click_x, &pointer->click_y); + pointer->moved = state.moved != FALSE; + pointer->left_clicks = (unsigned char)(state.left_clicks < 255 ? state.left_clicks : 255); + return 1; +} + int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer) { struct platform_ui_pointer state; diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 2d0d5f58..66be9af5 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -295,6 +295,18 @@ static const struct config_setting config_settings[] = "in everyone's server browser: anyone can see and join it) or, false,\n" "PRIVATE (only players with its invite link can join). Server Setup's\n" "LISTING changes it for each game." }, + { "network.votekick", _config_boolean, "true", "HALO_NET_VOTEKICK", _environment_value, _platform_all, + "Hosting: let the players vote to kick a player (the scoreboard's\n" + "right-click, or the console's votekick). More than half of the players\n" + "must vote, counted once per address." }, + { "network.votekick_minutes", _config_integer, "5", "HALO_NET_VOTEKICK_MINUTES", _environment_value, + _platform_all, + "Hosting: the minutes a player must have played on this server to start\n" + "a vote to kick (0 to 60); to vote, 2 minutes or this, the less." }, + { "network.votekick_ban_minutes", _config_integer, "30", "HALO_NET_VOTEKICK_BAN_MINUTES", _environment_value, + _platform_all, + "Hosting: the minutes a player kicked by a vote cannot join again (1 to\n" + "1440)." }, { "network.coop_public", _config_boolean, "false", "HALO_NET_COOP_PUBLIC", _environment_value, _platform_all, "Whether an online co-op game (Create Game > Internet, a SINGLEPLAYER\n" "map) starts as PUBLIC or, false, PRIVATE: Server Setup's LISTING in\n" diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 58130ee4..17a68bdb 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -73,6 +73,16 @@ static Uint64 scoreboard_open_until_ms; static float scoreboard_wheel; static long scoreboard_notches; static long scoreboard_pages; +#ifndef HALO_ANDROID +/* the scoreboard's pointer (platform_scoreboard_pointer): while the game +offers it (a network game's scoreboard is open), a right click frees the +mouse, whose pointer then picks a player; its motion and clicks go to it, +not to the aim and the triggers. Another right click, or the scoreboard +closing, takes the mouse back for the aim. */ +static BOOL scoreboard_pointer_offered; +static struct platform_ui_pointer scoreboard_pointer; +#endif +static BOOL scoreboard_pointer_active; /* debug keyboard queue */ #define KEYSTROKE_QUEUE_SIZE 64 @@ -1153,6 +1163,52 @@ void platform_request_quit(void) #endif } +#ifndef HALO_ANDROID +/* (under input_lock, on the event thread) the scoreboard's pointer on: the +mouse freed, at the window's middle, and nothing held for the triggers */ +static void scoreboard_pointer_start(void) +{ + int width, height; + + scoreboard_pointer_active = TRUE; + memset(&scoreboard_pointer, 0, sizeof(scoreboard_pointer)); + memset(input_state.mouse_buttons, 0, sizeof(input_state.mouse_buttons)); + memset(mouse_buttons_pressed, 0, sizeof(mouse_buttons_pressed)); + input_state.mouse_dx = input_state.mouse_dy = 0.0f; + platform_mouse_capture(FALSE); + SDL_GetWindowSize(platform_window, &width, &height); + SDL_WarpMouseInWindow(platform_window, width * 0.5f, height * 0.5f); + scoreboard_pointer.x = width * 0.5f; + scoreboard_pointer.y = height * 0.5f; +} + +/* ... off: the mouse the aim's again (unless freed: F12, or the menus) */ +static void scoreboard_pointer_stop(void) +{ + if (!scoreboard_pointer_active) + return; + scoreboard_pointer_active = FALSE; + memset(&scoreboard_pointer, 0, sizeof(scoreboard_pointer)); + platform_mouse_capture(!input_state.mouse_released && !input_state.ui_pointer); +} + +BOOL platform_scoreboard_pointer(BOOL offered, struct platform_ui_pointer *pointer) +{ + BOOL active; + + pthread_mutex_lock(&input_lock); + scoreboard_pointer_offered = offered; + active = scoreboard_pointer_active && offered; + *pointer = scoreboard_pointer; + scoreboard_pointer.moved = FALSE; + scoreboard_pointer.left_clicks = 0; + scoreboard_pointer.right_clicks = 0; + scoreboard_pointer.wheel_steps = 0; + pthread_mutex_unlock(&input_lock); + return active; +} +#endif + void platform_scoreboard_scroll(int open, long *notches, long *pages) { Uint64 now = SDL_GetTicks(); @@ -1165,6 +1221,10 @@ void platform_scoreboard_scroll(int open, long *notches, long *pages) scoreboard_pages = 0; } scoreboard_open_until_ms = open ? now + SCOREBOARD_OPEN_MS : 0; +#ifndef HALO_ANDROID + if (!open) + scoreboard_pointer_offered = FALSE; +#endif if (notches) *notches = scoreboard_notches; if (pages) @@ -1201,6 +1261,14 @@ void platform_pump_events(void) updater_poll(platform_window); #endif pthread_mutex_lock(&input_lock); +#ifndef HALO_ANDROID + /* (the scoreboard closed, or no longer offering it: the pointer goes) */ + if (scoreboard_pointer_active && (SDL_GetTicks() >= scoreboard_open_until_ms || !scoreboard_pointer_offered || + input_state.ui_pointer)) + { + scoreboard_pointer_stop(); + } +#endif while (SDL_PollEvent(&event)) { switch (event.type) @@ -1237,7 +1305,8 @@ void platform_pump_events(void) if (event.key.down && !event.key.repeat && event.key.scancode == SDL_SCANCODE_F12) { input_state.mouse_released = !input_state.mouse_released; - platform_mouse_capture(!input_state.mouse_released && !input_state.ui_pointer); + platform_mouse_capture(!input_state.mouse_released && !input_state.ui_pointer && + !scoreboard_pointer_active); } #ifndef HALO_ANDROID /* F11 switches between fullscreen and the window (SDL keeps the @@ -1250,6 +1319,13 @@ void platform_pump_events(void) break; case SDL_EVENT_MOUSE_MOTION: #ifndef HALO_ANDROID + if (scoreboard_pointer_active) + { + scoreboard_pointer.x = event.motion.x; + scoreboard_pointer.y = event.motion.y; + scoreboard_pointer.moved = TRUE; + break; + } /* in the menus the mouse moves the pointer, not the view */ if (input_state.ui_pointer) { @@ -1281,6 +1357,26 @@ void platform_pump_events(void) break; } #ifndef HALO_ANDROID + /* the open scoreboard's pointer: a right click frees it (and + fires nothing), and another takes it back; its clicks pick */ + if (!input_state.ui_pointer && SDL_GetTicks() < scoreboard_open_until_ms && scoreboard_pointer_offered && + (scoreboard_pointer_active || (event.button.down && event.button.button == SDL_BUTTON_RIGHT))) + { + if (event.button.down && event.button.button == SDL_BUTTON_RIGHT) + { + if (scoreboard_pointer_active) + scoreboard_pointer_stop(); + else + scoreboard_pointer_start(); + } + else if (event.button.down && event.button.button == SDL_BUTTON_LEFT) + { + scoreboard_pointer.left_clicks++; + scoreboard_pointer.click_x = event.button.x; + scoreboard_pointer.click_y = event.button.y; + } + break; + } /* clicks in the menus go to the pointer; a button held down when the menu closes stays up until pressed again, so the click that resumes the game does not also fire */ @@ -1356,12 +1452,15 @@ void platform_pump_events(void) memset(input_state.mouse_buttons, 0, sizeof(input_state.mouse_buttons)); memset(mouse_buttons_pressed, 0, sizeof(mouse_buttons_pressed)); input_state.focused = FALSE; + /* (the scoreboard's pointer goes; the mouse is taken back for + the aim as the window has the focus again) */ + scoreboard_pointer_active = FALSE; break; case SDL_EVENT_WINDOW_FOCUS_GAINED: input_state.focused = TRUE; look_at_clipboard = TRUE; #ifndef HALO_ANDROID - if (!input_state.mouse_released && !input_state.ui_pointer) + if (!input_state.mouse_released && !input_state.ui_pointer && !scoreboard_pointer_active) platform_mouse_capture(TRUE); #endif break; diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index 1c4978be..6e342994 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -98,6 +98,9 @@ struct platform_ui_pointer }; void platform_ui_pointer_set_active(BOOL active); BOOL platform_ui_pointer_read(struct platform_ui_pointer *pointer); +/* the open scoreboard's pointer, offered (a network game's) or not: TRUE +while a right click has freed it, with what it did since the last call */ +BOOL platform_scoreboard_pointer(BOOL offered, struct platform_ui_pointer *pointer); void platform_video_window_size(int *width, int *height); #endif BOOL platform_next_keystroke(struct platform_keystroke *keystroke); diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 0016e819..e35fe662 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -569,6 +569,7 @@ symbols in this file: #include "networking/network_server_manager.h" #include "networking/network_game_manager.h" #include "network_coop.h" /* port: port/linux/game/network_coop.c */ +#include "network_votekick.h" /* port: port/linux/game/network_votekick.c */ /* (network_server_manager_internal.h's: the host's game record) */ struct network_game *network_game_server_get_game(struct network_game_server *server); #include "objects.h" @@ -1862,15 +1863,148 @@ static boolean scoreboard_team_columns( return (boolean)setting; } +/* a network game's scoreboard picks players with the mouse +(halo_scoreboard_pointer_update: a right click frees it): a player's name +picked opens a menu of what may be done to them, the players' vote to kick +them (network_votekick.c), and the host's Kick and Ban, Ban picked twice */ +enum +{ + _scoreboard_item_vote, + _scoreboard_item_kick, + _scoreboard_item_ban, + _scoreboard_item_cancel, + NUMBER_OF_SCOREBOARD_ITEMS, + + /* the menu's width (the scoreboard's text, before it is scaled) */ + SCOREBOARD_MENU_WIDTH = 220, +}; + +/* the menu: the player it is for (NONE: none open), the row under whose +name it opens (from the title's), that name's left, and whether Ban was +picked once */ +static struct +{ + long player_index; + long row_index; + short left; + boolean ban_picked; +} scoreboard_menu = { NONE, 0, 0, FALSE }; + /* the scoreboard closed: its scroll forgotten, the wheel the weapons' again */ static void game_engine_scoreboard_closed( void) { if (scoreboard_open) { + struct halo_ui_pointer pointer; + scoreboard_open = FALSE; platform_scoreboard_scroll(FALSE, NULL, NULL); + halo_scoreboard_pointer_update(FALSE, &pointer); } + scoreboard_menu.player_index = NONE; +} + +/* the menu's items for the player: whether each is offered, and what it says */ +static void scoreboard_menu_items( + wchar_t (*labels)[48], + boolean *offered) +{ + struct network_votekick_status status; + short item; + + for (item = 0; item < NUMBER_OF_SCOREBOARD_ITEMS; item++) + { + offered[item] = FALSE; + labels[item][0] = 0; + } + offered[_scoreboard_item_vote] = TRUE; + if (network_votekick_get_status(&status) && + status.player_index == DATUM_INDEX_TO_ABSOLUTE_INDEX(scoreboard_menu.player_index)) + { + usprintf(labels[_scoreboard_item_vote], status.voted ? L"Voted to kick (%d of %d)" : + L"Vote to kick (%d of %d)", status.votes, status.needed); + } + else + { + usprintf(labels[_scoreboard_item_vote], L"Start a vote to kick"); + } + if (network_votekick_host()) + { + offered[_scoreboard_item_kick] = TRUE; + offered[_scoreboard_item_ban] = TRUE; + usprintf(labels[_scoreboard_item_kick], L"Kick"); + usprintf(labels[_scoreboard_item_ban], scoreboard_menu.ban_picked ? L"Ban: pick again to ban" : L"Ban"); + } + offered[_scoreboard_item_cancel] = TRUE; + usprintf(labels[_scoreboard_item_cancel], L"Cancel"); +} + +/* the menu's first row (from the title's): under the name, or above it when +there is no room below (last_row: the screen's) */ +static long scoreboard_menu_first_row( + short item_count, + long last_row) +{ + if (scoreboard_menu.row_index + 1 + item_count <= last_row) + return scoreboard_menu.row_index + 1; + return MAX(scoreboard_menu.row_index - item_count, 0); +} + +/* a rectangle of the scoreboard's text (laid out at full size, from left +and the row from the title's) as it is drawn: scaled about the title's top +left (bounds_left, top) */ +static void scoreboard_rectangle( + rectangle2d *rectangle, + short bounds_left, + short top, + long line_height, + short left, + short width, + long row_index, + long row_count) +{ + rectangle->x0 = (short)(bounds_left + (left - bounds_left) * SCOREBOARD_SCALE); + rectangle->x1 = (short)(rectangle->x0 + width * SCOREBOARD_SCALE); + rectangle->y0 = (short)(top + row_index * line_height * SCOREBOARD_SCALE); + rectangle->y1 = (short)(top + (row_index + row_count) * line_height * SCOREBOARD_SCALE); +} + +static boolean scoreboard_point_in( + rectangle2d const *rectangle, + short x, + short y) +{ + return x >= rectangle->x0 && x < rectangle->x1 && y >= rectangle->y0 && y < rectangle->y1; +} + +/* the item picked, done */ +static void scoreboard_menu_pick( + short item) +{ + short player_index = (short)DATUM_INDEX_TO_ABSOLUTE_INDEX(scoreboard_menu.player_index); + + switch (item) + { + case _scoreboard_item_vote: + network_votekick_request(player_index); + break; + case _scoreboard_item_kick: + network_votekick_host_kick(player_index, FALSE); + break; + case _scoreboard_item_ban: + /* (a ban lasts: picked twice) */ + if (!scoreboard_menu.ban_picked) + { + scoreboard_menu.ban_picked = TRUE; + return; + } + network_votekick_host_kick(player_index, TRUE); + break; + default: + break; + } + scoreboard_menu.player_index = NONE; } static void game_engine_rasterize_scoreboard( @@ -1910,6 +2044,24 @@ static void game_engine_rasterize_scoreboard( short top; wchar_t *column_name; wchar_t *score_name; + /* port: the pointer (1: picking; -1: none on this platform), where it + is in the drawing's coordinates, and whether its click was taken */ + struct halo_ui_pointer pointer; + int pointer_state; + short pointer_x; + short pointer_y; + short click_x; + short click_y; + boolean clicked; + struct network_votekick_status vote; + boolean voting; + long extra_rows; + long next_row; + wchar_t menu_labels[NUMBER_OF_SCOREBOARD_ITEMS][48]; + boolean menu_offered[NUMBER_OF_SCOREBOARD_ITEMS]; + short menu_item_count = 0; + long menu_first_row = 0; + long last_row; if (font_index == NONE) return; @@ -1924,8 +2076,13 @@ static void game_engine_rasterize_scoreboard( /* (laid out at full size, then drawn scaled about the title's top left: the screen holds 1/SCOREBOARD_SCALE as much) */ width = (short)((bounds.x1 - bounds.x0) / SCOREBOARD_SCALE); + /* (port: a network game's lines beneath, the vote running and how to + pick a player, have their rows) */ + pointer_state = halo_scoreboard_pointer_update(network && network_votekick_available(), &pointer); + voting = network && network_votekick_get_status(&vote); + extra_rows = (voting ? 1 : 0) + (network && pointer_state >= 0 ? 1 : 0); rows = (long)((bounds.y1 - SCOREBOARD_LAYOUT_TOP_ROWS * line_height) / SCOREBOARD_SCALE / line_height) - 2 - - SCOREBOARD_BOTTOM_ROWS; + SCOREBOARD_BOTTOM_ROWS - extra_rows; rows = MAX(rows, 1); if (campaign) { @@ -1990,6 +2147,7 @@ static void game_engine_rasterize_scoreboard( shown_rows = MIN(rows, total); if (total > page) shown_rows++; + shown_rows += extra_rows; { real height = (2 + shown_rows) * line_height * SCOREBOARD_SCALE; @@ -2018,6 +2176,50 @@ static void game_engine_rasterize_scoreboard( } rasterizer_text_set_scale(SCOREBOARD_SCALE, (real)bounds.x0, (real)top); + /* port: the pointer's click, on the menu's items (where they are + drawn), else on a player's row (below); one elsewhere closes the menu */ + pointer_x = (short)(pointer.x - render.camera.viewport_bounds.x0); + pointer_y = (short)(pointer.y - render.camera.viewport_bounds.y0); + click_x = (short)(pointer.click_x - render.camera.viewport_bounds.x0); + click_y = (short)(pointer.click_y - render.camera.viewport_bounds.y0); + clicked = pointer_state > 0 && pointer.left_clicks > 0; + /* (the menu opens below its row, beyond the scoreboard if need be, but + above the motion sensor's rows) */ + last_row = (long)((bounds.y1 - top) / (line_height * SCOREBOARD_SCALE)) - SCOREBOARD_BOTTOM_ROWS; + if (pointer_state <= 0 || (scoreboard_menu.player_index != NONE && !player_try_and_get(scoreboard_menu.player_index))) + scoreboard_menu.player_index = NONE; + if (scoreboard_menu.player_index != NONE) + { + short item; + + scoreboard_menu_items(menu_labels, menu_offered); + for (item = 0; item < NUMBER_OF_SCOREBOARD_ITEMS; item++) + menu_item_count += menu_offered[item] ? 1 : 0; + menu_first_row = scoreboard_menu_first_row(menu_item_count, last_row); + if (clicked) + { + rectangle2d rectangle; + short shown = 0; + + for (item = 0; item < NUMBER_OF_SCOREBOARD_ITEMS && clicked; item++) + { + if (!menu_offered[item]) + continue; + scoreboard_rectangle(&rectangle, bounds.x0, top, line_height, scoreboard_menu.left, + SCOREBOARD_MENU_WIDTH, menu_first_row + shown++, 1); + if (scoreboard_point_in(&rectangle, click_x, click_y)) + { + scoreboard_menu_pick(item); + clicked = FALSE; + } + } + if (clicked) + scoreboard_menu.player_index = NONE; + else if (scoreboard_menu.player_index != NONE) + scoreboard_menu_items(menu_labels, menu_offered); + } + } + team_colors[0].alpha = alpha; team_colors[0].red = 0.6f; team_colors[0].green = 0.3f; @@ -2116,6 +2318,32 @@ static void game_engine_rasterize_scoreboard( status_string, ping_string); row_color = has_teams ? &team_colors[PIN(player->team_index, 0, 1)] : &color; + /* port: another machine's player, picked with the pointer: lit + under it (not under the menu), and its menu opened when clicked */ + if (pointer_state > 0 && player->local_player_index == NONE) + { + rectangle2d rectangle; + rectangle2d menu_rectangle; + short row_left = (short)(left + column * (SCOREBOARD_COLUMN_WIDTH + SCOREBOARD_COLUMN_GAP)); + + scoreboard_rectangle(&rectangle, bounds.x0, top, line_height, row_left, SCOREBOARD_COLUMN_WIDTH, + 2 + row, 1); + scoreboard_rectangle(&menu_rectangle, bounds.x0, top, line_height, scoreboard_menu.left, + SCOREBOARD_MENU_WIDTH, menu_first_row, menu_item_count); + if (scoreboard_point_in(&rectangle, pointer_x, pointer_y) && + (scoreboard_menu.player_index == NONE || !scoreboard_point_in(&menu_rectangle, pointer_x, pointer_y))) + { + draw_quad(&rectangle, ((pixel32)(long)(80 * PIN(alpha, 0.0f, 1.0f)) << 24) | 0x00FFFFFF); + } + if (clicked && scoreboard_point_in(&rectangle, click_x, click_y)) + { + scoreboard_menu.player_index = entry->player_index; + scoreboard_menu.row_index = 2 + row; + scoreboard_menu.left = (short)(row_left + SCOREBOARD_PLACE_WIDTH); + scoreboard_menu.ban_picked = FALSE; + clicked = FALSE; + } + } scoreboard_draw_row( row_string, player_index == entry->player_index, @@ -2136,6 +2364,74 @@ static void game_engine_rasterize_scoreboard( usprintf(row_string, L"%ld-%ld of %ld (Page Up / Page Down, mouse wheel)", first, last, total); scoreboard_draw_row(row_string, FALSE, &color, 2 + rows, top, left, FALSE); } + /* port: the vote running, and how to pick a player */ + next_row = 2 + MIN(rows, total) + (total > page ? 1 : 0); + if (voting) + { + struct data_iterator iterator; + struct player_datum *target; + wchar_t const *name = L"?"; + wchar_t const *note = vote.voted ? L" (you voted)" : vote.may_vote ? L"" : L" (too new to vote)"; + + data_iterator_new(&iterator, player_data); + while ((target = (struct player_datum *)data_iterator_next(&iterator)) != NULL) + { + if (DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index) == vote.player_index) + { + name = target->name; + if (target->local_player_index != NONE) + note = L" (against you)"; + } + } + color.alpha = alpha; + color.red = 1.0f; + color.green = 0.75f; + color.blue = 0.3f; + usprintf(row_string, L"Vote to kick %s: %d of %d, %d s left%s", name, vote.votes, vote.needed, + vote.seconds_left, note); + scoreboard_draw_row(row_string, FALSE, &color, next_row++, top, left, FALSE); + } + if (network && pointer_state >= 0) + { + color.alpha = alpha; + color.red = color.green = color.blue = 0.6f; + if (pointer_state == 0) + usprintf(row_string, L"Right-click to pick a player"); + else + usprintf(row_string, L"Pick a player (right-click to aim)"); + scoreboard_draw_row(row_string, FALSE, &color, next_row++, top, left, FALSE); + } + /* port: the picked player's menu, over the rows, its item under the + pointer lit */ + if (scoreboard_menu.player_index != NONE && menu_item_count > 0) + { + rectangle2d rectangle; + short item; + short shown = 0; + long panel_alpha = (long)(255 * PIN(alpha, 0.0f, 1.0f)); + + scoreboard_rectangle(&rectangle, bounds.x0, top, line_height, scoreboard_menu.left, SCOREBOARD_MENU_WIDTH, + menu_first_row, menu_item_count); + draw_quad(&rectangle, ((pixel32)panel_alpha << 24) | 0x00202830); + for (item = 0; item < NUMBER_OF_SCOREBOARD_ITEMS; item++) + { + if (!menu_offered[item]) + continue; + scoreboard_rectangle(&rectangle, bounds.x0, top, line_height, scoreboard_menu.left, + SCOREBOARD_MENU_WIDTH, menu_first_row + shown, 1); + if (scoreboard_point_in(&rectangle, pointer_x, pointer_y)) + draw_quad(&rectangle, ((pixel32)(long)(90 * PIN(alpha, 0.0f, 1.0f)) << 24) | 0x00FFFFFF); + color.alpha = alpha; + color.red = color.green = color.blue = 0.9f; + if (item == _scoreboard_item_ban) + { + color.green = color.blue = 0.45f; + } + scoreboard_draw_row(menu_labels[item], FALSE, &color, menu_first_row + shown, top, + (short)(scoreboard_menu.left + 6), FALSE); + shown++; + } + } rasterizer_text_set_scale(1.0f, 0.0f, 0.0f); return; diff --git a/source/hs/hs.c b/source/hs/hs.c index e1f6fde6..465b42a4 100644 --- a/source/hs/hs.c +++ b/source/hs/hs.c @@ -2805,6 +2805,7 @@ symbols in this file: #include "networking/network_game_globals.h" #include "networking/network_game_manager.h" #include "networking/network_server_manager.h" +#include "network_votekick.h" /* port: port/linux/game/network_votekick.c */ #include "objects/damage.h" #include "objects/object_lights.h" #include "objects/scenery.h" @@ -15361,6 +15362,27 @@ static boolean hs_compile_and_evaluate_command( (players.c; a client is told it is the host's) */ if (hs_host_player_command(expression, "bringto")) return players_coop_bring_to_host(); + /* port: the players' vote to kick a player ("votekick ", + or the name's start: Tab completes it), on any machine of a network + game: the host counts (network_votekick.c) */ + { + char const *text = hs_host_player_command(expression, "votekick"); + + if (text) + { + char name[64]; + long length = 0; + + while (*text == ' ' || *text == '\t' || *text == '"') + text++; + while (*text && *text != '"' && *text != ')' && length < (long)sizeof(name) - 1) + name[length++] = *text++; + while (length > 0 && (name[length - 1] == ' ' || name[length - 1] == '\t')) + length--; + name[length] = 0; + return network_votekick_player_named(name); + } + } /* port: playing in another's game, the host decides the game: no cheats, no game speed, nothing else a command changes of the game (the game run each tick also puts back what was changed before joining, diff --git a/source/main/console.c b/source/main/console.c index 4b8a7103..6bfdd7f1 100644 --- a/source/main/console.c +++ b/source/main/console.c @@ -59,6 +59,7 @@ symbols in this file: #include "editor/editor_stubs.h" #include "hs/hs.h" #include "networking/network_server_manager.h" +#include "network_votekick.h" /* port: port/linux/game/network_votekick.c */ #include "input/input.h" #include "interface/terminal.h" #include "math/real_math.h" @@ -260,12 +261,13 @@ static char *console_get_text_to_autocomplete( } /* port: the text after the host's ban or kick command ("ban ", "kick "), -which completes as a player's name (network_game_server_matching_player_names); -NULL if the input is not one */ +or any player's votekick ("votekick "), which completes as a player's name +(network_game_server_matching_player_names, network_votekick_matching_player_names; +the command's index: votekick's, 2); NULL if the input is not one */ static char *console_player_command_name( - void) + short *command_index) { - static char const *const commands[] = { "ban", "kick" }; + static char const *const commands[] = { "ban", "kick", "votekick" }; char *text = console_globals.input_state.result; short command; @@ -286,6 +288,7 @@ static char *console_player_command_name( } if (index == length && text[length] == ' ') { + *command_index = command; text += length; while (*text == ' ' || *text == '"') text++; @@ -303,14 +306,19 @@ static void console_complete( /* (port: the players' names the ban and kick commands complete) */ static char player_names[64][NETWORK_GAME_SERVER_NAME_TEXT_SIZE]; - char *token = console_player_command_name(); + short command_index = NONE; + char *token = console_player_command_name(&command_index); short count; if (token) { short index; - count = network_game_server_matching_player_names(token, player_names, NUMBEROF(player_names)); + /* (the votekick's: any machine's, from the game's players) */ + if (command_index == 2) + count = network_votekick_matching_player_names(token, player_names, NUMBEROF(player_names)); + else + count = network_game_server_matching_player_names(token, player_names, NUMBEROF(player_names)); for (index = 0; index < count; index++) matching_items[index] = player_names[index]; } diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index 1a7c2217..3ec3511a 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -804,6 +804,9 @@ static boolean network_game_server_countdown_slowed[MAXIMUM_NETWORK_MACHINE_COUN boolean network_distributed_banned(unsigned long address, char const *hardware_id); void network_distributed_ban(long machine_index, unsigned long address, char const *names); void network_distributed_kick(char const *names); +/* port/linux/game/network_votekick.c's (the players' votes to kick) */ +void network_votekick_machine_joined(long machine_index); +boolean network_votekick_kept_out(unsigned long address, char const *hardware_id); /* port/linux/src/p2p.c's */ enum { @@ -1045,9 +1048,53 @@ static boolean network_game_server_named_machine( return TRUE; } +/* port: the names of a machine's players (each command's notice) */ +static void network_game_server_machine_names( + struct network_game_server *server, + long machine_index, + char *names, + long names_size) +{ + long index; + + names[0] = 0; + for (index = 0; index < MAXIMUM_NETWORK_PLAYER_COUNT; index++) + { + struct network_player const *player = &server->game.players[index]; + char name[NETWORK_GAME_SERVER_NAME_TEXT_SIZE]; + + if (!network_player_is_valid(player) || player->machine_index != machine_index) + continue; + network_game_server_player_name_text(player, name, sizeof(name)); + if (names[0] && csstrlen(names) + 2 < names_size) + csstrcat(names, ", "); + if (csstrlen(names) + csstrlen(name) < names_size) + csstrcat(names, name); + } +} + +/* port: the host's ban: the machine dropped, its address in bans.txt +(network_distributed_ban), and kept out */ +static void network_game_server_ban_machine( + long machine_index, + char const *names) +{ + network_distributed_ban(machine_index, network_game_server_client_machine_addresses[machine_index], names); + network_game_server_kick_pending[machine_index] = _kick_kept_out; +} + +/* port: the host's kick: the machine dropped, every machine told, and +nothing kept: it may join again at once */ +static void network_game_server_kick_named_machine( + long machine_index, + char const *names) +{ + network_distributed_kick(names); + network_game_server_kick_pending[machine_index] = _kick_rejoinable; +} + /* port: the host's ban command: the player's machine (as -network_game_server_named_machine finds it) dropped, its address in -bans.txt (network_distributed_ban), and kept out */ +network_game_server_named_machine finds it) banned */ boolean network_game_server_ban_player( char const *text) { @@ -1056,14 +1103,12 @@ boolean network_game_server_ban_player( if (!network_game_server_named_machine("ban", text, &machine_index, names, sizeof(names))) return FALSE; - network_distributed_ban(machine_index, network_game_server_client_machine_addresses[machine_index], names); - network_game_server_kick_pending[machine_index] = _kick_kept_out; + network_game_server_ban_machine(machine_index, names); return TRUE; } /* port: the host's kick command: the player's machine (as the ban -command's) dropped, every machine told, and nothing kept: it may join again -at once */ +command's) kicked */ boolean network_game_server_kick_player( char const *text) { @@ -1072,8 +1117,37 @@ boolean network_game_server_kick_player( if (!network_game_server_named_machine("kick", text, &machine_index, names, sizeof(names))) return FALSE; - network_distributed_kick(names); - network_game_server_kick_pending[machine_index] = _kick_rejoinable; + network_game_server_kick_named_machine(machine_index, names); + return TRUE; +} + +/* port: the host's Kick and Ban on the scoreboard (game_engine.c): the +machine of the player at the network machine index it has, kicked or +banned as the commands do; FALSE (said) for none, or the host's own */ +boolean network_game_server_kick_machine_of_player( + long machine_index, + boolean ban) +{ + struct network_game_server *server = global_network_game_server_get(); + char names[96]; + + if (!server) + { + console_warning("%s: only the host of a game does this", ban ? "ban" : "kick"); + return FALSE; + } + if (!VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) || + !network_game_server_client_machine_is_joined_to_game(server, &server->client_machines[machine_index]) || + network_game_server_client_machine_is_local(server, &server->client_machines[machine_index])) + { + console_warning("%s: not a player of the host's own machine, nor one not joined", ban ? "ban" : "kick"); + return FALSE; + } + network_game_server_machine_names(server, machine_index, names, sizeof(names)); + if (ban) + network_game_server_ban_machine(machine_index, names); + else + network_game_server_kick_named_machine(machine_index, names); return TRUE; } @@ -1983,6 +2057,15 @@ boolean network_game_server_accept_client_machine_into_game( network_event("refusing a machine @ %s: banned (bans.txt)", transport_address_to_string(&address)); return FALSE; } + /* (nor one kicked by a vote, for a while: network_votekick.c) */ + if (!network_game_server_client_machine_is_local(server, machine) && + network_votekick_kept_out(address.address.long_words[0], + VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) ? + network_game_server_hardware_ids[machine_index] : "")) + { + network_event("refusing a machine @ %s: kicked by a vote", transport_address_to_string(&address)); + return FALSE; + } } /* (a kick asked for the slot's machine before is not this one's) */ if (VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT)) @@ -2047,6 +2130,8 @@ boolean network_game_server_accept_client_machine_into_game( } network_game_server_client_machine_addresses[machine_index] = address.address.long_words[0]; network_game_server_client_machine_join_times[machine_index] = system_milliseconds(); + /* (its time played for votes starts over) */ + network_votekick_machine_joined(machine_index); } else { diff --git a/source/networking/network_server_manager.h b/source/networking/network_server_manager.h index c5b7eec6..52fe417d 100644 --- a/source/networking/network_server_manager.h +++ b/source/networking/network_server_manager.h @@ -83,6 +83,11 @@ boolean network_game_server_ban_player( bans.txt line, the address not kept out): the player may join again at once */ boolean network_game_server_kick_player( char const *text); +/* port: the host's Kick and Ban on the scoreboard: the client machine +(its index), kicked or banned as those commands do */ +boolean network_game_server_kick_machine_of_player( + long machine_index, + boolean ban); short network_game_server_matching_player_names( char const *text, char (*names)[NETWORK_GAME_SERVER_NAME_TEXT_SIZE], diff --git a/tools/harness/tests/test_votekick_count.py b/tools/harness/tests/test_votekick_count.py new file mode 100644 index 00000000..c2e769f5 --- /dev/null +++ b/tools/harness/tests/test_votekick_count.py @@ -0,0 +1,65 @@ +"""The real count of network_votekick.c: who may vote, whose votes are one person's (by address, and by a hardware id +that only ever takes votes away), and how many kick.""" + +import re +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, constant, function, mutated, read, run # noqa: E402 + +CASES = ["majority", "even-teams", "two-never-kick", "same-address-one-vote", "same-hardware-id-one-vote", + "copied-hardware-id-lowers-nothing", "target-never-votes", "eligibility"] +FUNCTIONS = ("votekick_same_address", "votekick_same_person", "votekick_may_vote", "votekick_count", + "votekick_votes_needed") + +# a fault in network_votekick.c, the function it is in, and the case that must catch it +NEGATIVE_CONTROLS = { + "half-is-enough": (("electorate / 2 + 1", "(electorate + 1) / 2"), "votekick_votes_needed", "majority"), + "no-minimum": (("needed < MINIMUM_VOTES ? MINIMUM_VOTES : needed", "needed"), "votekick_votes_needed", + "two-never-kick"), + "target-not-counted": (("votekick_may_vote(&voters[index]) || voters[index].target;", + "votekick_may_vote(&voters[index]);"), "votekick_count", "even-teams"), + "votes-by-address-only": (("votekick_may_vote(&voters[other]) &&\n\t\t\t\tvotekick_same_person(", + "votekick_may_vote(&voters[other]) &&\n\t\t\t\tvotekick_same_address("), + "votekick_count", "same-hardware-id-one-vote"), + "electorate-by-hardware-id": (("votekick_same_address(&voters[index], &voters[other]))\n\t\t\t{\n\t\t\t\tcounted = FALSE;\n\t\t\t}\n\t\t}\n\t\tif (counted)\n\t\t\t(*electorate)++;", + "votekick_same_person(&voters[index], &voters[other]))\n\t\t\t{\n\t\t\t\tcounted = FALSE;\n\t\t\t}\n\t\t}\n\t\tif (counted)\n\t\t\t(*electorate)++;"), + "votekick_count", "copied-hardware-id-lowers-nothing"), + "target-votes": (("if (voters[other].target && votekick_same_person(", "if (0 && votekick_same_person("), + "votekick_count", "target-never-votes"), + "newcomers-vote": (("return voter->eligible && !voter->target", "return !voter->target"), "votekick_may_vote", + "eligibility"), +} + + +def generated(fault=None, faulty_function=None): + source = read("port/linux/game/network_votekick.c") + config = "" + for name in ("VOTEKICK_HARDWARE_ID_SIZE", "MINIMUM_VOTES"): + config += f"#define {name} {constant(source, name)}\n" + match = re.search(r"^struct votekick_voter\n\{.*?^\};\n", source, re.M | re.S) + assert match, "struct votekick_voter not found in network_votekick.c" + config += match.group(0) + text = "" + for name in FUNCTIONS: + code = function(source, name) + if fault and name == faulty_function: + code = mutated(code, *fault) + text += code + "\n" + return (("config.inc", config), ("under_test.inc", text)) + + +@pytest.mark.parametrize("case", CASES) +def test_case(case): + status, output = run(build("votekick_count", generated()), case) + assert status == 0, output + + +@pytest.mark.parametrize("control", NEGATIVE_CONTROLS) +def test_negative_control(control): + fault, faulty_function, case = NEGATIVE_CONTROLS[control] + status, output = run(build("votekick_count", generated(fault, faulty_function)), case) + assert status == CHECK_FAILED, f"network_votekick.c with a fault ({control}) passed '{case}': the test cannot see it" diff --git a/tools/harness/tests/votekick_count.c b/tools/harness/tests/votekick_count.c new file mode 100644 index 00000000..110fb963 --- /dev/null +++ b/tools/harness/tests/votekick_count.c @@ -0,0 +1,164 @@ +/* +VOTEKICK_COUNT.C (test) + +The real count of port/linux/game/network_votekick.c, which stands between +the game's players and a kick: who may vote, whose votes are one person's, +and how many kick. test_votekick_count.py takes the constants and the +voter's structure (config.inc) and the code (under_test.inc). +*/ + +#include "harness.h" + +#define csstrcmp strcmp + +#include "config.inc" +#include "under_test.inc" + +static struct votekick_voter voters[16]; +static short voter_count; + +static struct votekick_voter *add( + unsigned long address, + char const *hardware_id, + boolean eligible, + boolean voted) +{ + struct votekick_voter *voter = &voters[voter_count++]; + + memset(voter, 0, sizeof(*voter)); + voter->address = address; + snprintf(voter->hardware_id, sizeof(voter->hardware_id), "%s", hardware_id); + voter->eligible = eligible; + voter->voted = voted; + return voter; +} + +static void target( + unsigned long address, + char const *hardware_id) +{ + add(address, hardware_id, TRUE, FALSE)->target = TRUE; +} + +/* the count: votes, those who may vote, and the votes needed */ +static void count( + short *votes, + short *electorate, + short *needed) +{ + votekick_count(voters, voter_count, votes, electorate); + *needed = votekick_votes_needed(*electorate); +} + +int main(int argc, char **argv) +{ + char const *case_name = argc > 1 ? argv[1] : ""; + short votes, electorate, needed; + + /* more than half of everyone, the target counted */ + CASE("majority") + { + target(0x0A000001, "t"); + add(0x0A000002, "a", TRUE, TRUE); + add(0x0A000003, "b", TRUE, TRUE); + add(0x0A000004, "c", TRUE, TRUE); + add(0x0A000005, "d", TRUE, FALSE); + add(0x0A000006, "e", TRUE, FALSE); + count(&votes, &electorate, &needed); + CHECK(electorate == 6, "%d may vote, not 6", electorate); + CHECK(needed == 4, "%d needed of 6, not 4", needed); + CHECK(votes == 3, "%d votes, not 3", votes); + return 0; + } + /* one team of an even game kicks nobody from the other */ + CASE("even-teams") + { + unsigned long index; + + for (index = 0; index < 4; index++) + add(0x0A000010 + index, "", TRUE, TRUE); + target(0x0A000020, ""); + for (index = 1; index < 4; index++) + add(0x0A000020 + index, "", TRUE, FALSE); + count(&votes, &electorate, &needed); + CHECK(votes == 4 && electorate == 8, "%d votes of %d", votes, electorate); + CHECK(votes < needed, "one team of four kicked from the other (%d needed)", needed); + return 0; + } + /* two people never kick each other, nor one alone */ + CASE("two-never-kick") + { + target(0x0A000001, ""); + add(0x0A000002, "", TRUE, TRUE); + count(&votes, &electorate, &needed); + CHECK(votes < needed, "one of two kicked the other (%d of %d)", votes, needed); + CHECK(votekick_votes_needed(0) >= 2 && votekick_votes_needed(1) >= 2, "one vote kicked"); + return 0; + } + /* machines of one address: one vote, and counted once */ + CASE("same-address-one-vote") + { + target(0x0A000001, ""); + add(0x0A000002, "a", TRUE, TRUE); + add(0x0A000002, "b", TRUE, TRUE); + add(0x0A000002, "c", TRUE, TRUE); + add(0x0A000003, "d", TRUE, FALSE); + count(&votes, &electorate, &needed); + CHECK(votes == 1, "one address voted %d times", votes); + CHECK(electorate == 3, "%d may vote, not 3 addresses", electorate); + return 0; + } + /* machines of one hardware id: one vote */ + CASE("same-hardware-id-one-vote") + { + target(0x0A000001, ""); + add(0x0A000002, "abc", TRUE, TRUE); + add(0x0A000003, "abc", TRUE, TRUE); + count(&votes, &electorate, &needed); + CHECK(votes == 1, "one hardware id voted %d times", votes); + return 0; + } + /* a hardware id copied from another (anyone may tell any) never lowers + the votes needed: those who may vote are counted by address */ + CASE("copied-hardware-id-lowers-nothing") + { + target(0x0A000001, "t"); + add(0x0A000002, "a", TRUE, TRUE); + add(0x0A000003, "b", TRUE, FALSE); + add(0x0A000004, "c", TRUE, FALSE); + add(0x0A000005, "a", TRUE, FALSE); + add(0x0A000006, "b", TRUE, FALSE); + count(&votes, &electorate, &needed); + CHECK(electorate == 6, "copied hardware ids made %d who may vote, not 6", electorate); + CHECK(needed == 4, "%d needed, not 4", needed); + return 0; + } + /* the target's person never votes against it, by address or id */ + CASE("target-never-votes") + { + target(0x0A000001, "t"); + add(0x0A000001, "x", TRUE, TRUE); + add(0x0A000002, "t", TRUE, TRUE); + add(0x0A000003, "y", TRUE, TRUE); + count(&votes, &electorate, &needed); + CHECK(votes == 1, "the target's address or id voted (%d votes)", votes); + return 0; + } + /* those who have not played long enough neither vote nor count; an + address not known neither; the host's machine does */ + CASE("eligibility") + { + target(0x0A000001, ""); + add(0x0A000002, "", FALSE, TRUE); + add(0x0A000003, "", FALSE, TRUE); + add(0, "z", TRUE, TRUE); + add(0, "", TRUE, TRUE)->host = TRUE; + add(0x0A000004, "", TRUE, TRUE); + count(&votes, &electorate, &needed); + CHECK(votes == 2, "%d votes, not the host's and one other", votes); + CHECK(electorate == 3, "%d may vote, not 3", electorate); + return 0; + } + fprintf(stderr, "unknown case: %s\n", case_name); + return 2; +} From 3a6e146fdbcb00f27003bb4daa701f0e9a989856 Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Thu, 8 Oct 2026 17:25:37 -0400 Subject: [PATCH 070/187] Remove redundant 1000ms hang on Create Game options --- source/networking/network_server_manager.c | 4 + tools/harness/tests/server_teardown.c | 151 ++++++++++++++++++++ tools/harness/tests/test_server_teardown.py | 66 +++++++++ 3 files changed, 221 insertions(+) create mode 100644 tools/harness/tests/server_teardown.c create mode 100644 tools/harness/tests/test_server_teardown.py diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index 1a7c2217..3fd306c3 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -1317,7 +1317,11 @@ void network_game_server_dispose( network_connection_delete(server->connection); #ifdef xbox + /* port: native transports flush before closing sockets, and unregistering + an XNet key is a stub. Preserve the original grace period on other builds. */ +#if !defined(HALO_WINDOWS) && !defined(HALO_ANDROID) && !defined(__linux__) SleepEx(MILLISECONDS_PER_SECOND, FALSE); +#endif transport_server_terminate(); #endif diff --git a/tools/harness/tests/server_teardown.c b/tools/harness/tests/server_teardown.c new file mode 100644 index 00000000..20f26d08 --- /dev/null +++ b/tools/harness/tests/server_teardown.c @@ -0,0 +1,151 @@ +/* Actual server disposal and menu callbacks; the network/transport are ordered + recorders. No sockets, assets, user profile or wall-clock sleeps. */ +#include "harness.h" +#include "config.inc" +#define NETWORK_SERVER_MANAGER_FILE "server" +#define _error_silent 0 +#define csmemset memset +#define SET_FLAG(flags, bit, value) ((value) ? ((flags) |= FLAG(bit)) : ((flags) &= ~FLAG(bit))) + +struct network_connection { int unused; }; +struct network_game_server +{ + short state; + unsigned flags; + struct network_connection *connection; + unsigned long time_of_first_client_loading_completion; + boolean sent_start_game_message; +}; +struct network_game_client { int unused; }; +struct network_message { int unused; }; +struct widget_instance { int unused; }; +struct event_record { int unused; }; +static struct +{ + struct network_game_server *server; + struct network_game_client *client; + boolean quickstart_local, client_started; +} bss_004566dc; +static char network_game_server_cooperative_next_map[128]; +static boolean network_game_server_memory_do_not_use_directly_in_use; +static struct network_message message; +static struct network_game_server *disposing; +static char order[64]; +static int slept, messages, broadcasts, handled, closed, terminated, errors; +static int new_servers, clients_closed, joins_cleared, variants_cleared; +static short expected_message; +static boolean message_fails, send_fails, machines_fail; + +static void note(char event) +{ + size_t length = strlen(order); + CHECK(length + 1 < sizeof(order), "event recorder overflow"); + order[length] = event; order[length + 1] = 0; +} +static void network_event(const char *text, ...) { } +static void error(int level, const char *text, ...) { errors++; } +static struct network_message *create_network_game_message(short kind, const void *packet, long size) +{ + CHECK(kind == expected_message && size == sizeof(long) && *(const long *)packet == 0, + "graceful-exit packet changed or contained uninitialized data"); + messages++; note('M'); + return message_fails ? NULL : &message; +} +static boolean network_game_server_send_message_to_all_machines(struct network_game_server *server, struct network_message *packet) +{ + CHECK(server == disposing && packet == &message && !closed && !terminated, "exit notice sent after cleanup"); + broadcasts++; note('B'); return !send_fails; +} +static boolean network_game_server_handle_client_machines(struct network_game_server *server) +{ + CHECK(server == disposing && !server->time_of_first_client_loading_completion && !server->sent_start_game_message && + !TEST_FLAG(server->flags, _network_game_server_game_open_bit), "teardown could start another game or accept clients"); + CHECK(!network_game_server_cooperative_next_map[0], "next co-op map survived teardown"); + handled++; note('H'); return !machines_fail; +} +static void network_connection_delete(struct network_connection *connection) +{ + CHECK(connection == disposing->connection && handled == 1 && !terminated, "socket cleanup order changed"); + closed++; note('C'); +} +static void SleepEx(unsigned long milliseconds, boolean alertable) +{ + CHECK(closed == 1 && !terminated && !alertable, "legacy wait moved before connection cleanup"); + slept += milliseconds; note('S'); +} +static short transport_server_terminate(void) +{ + CHECK(closed == 1 && slept == EXPECTED_SLEEP, "transport terminated before cleanup/required legacy grace"); + terminated++; note('T'); return 0; +} +static void p2p_set_game_player_counts(int count, int maximum) +{ + struct network_game_server empty = { 0 }; + CHECK(count == 0 && maximum == 0 && terminated == 1 && + !network_game_server_memory_do_not_use_directly_in_use && !memcmp(disposing, &empty, sizeof(empty)), + "server state/counts not cleared after transport cleanup"); + note('P'); +} +static void network_game_client_dispose(struct network_game_client *client) +{ + CHECK(client == bss_004566dc.client && !messages && !handled, "local client disposal order changed"); + clients_closed++; note('L'); +} +static void player_ui_clear_multiplayer_joins(void) { joins_cleared++; note('J'); } +static void player_ui_clear_multiplayer_variant(void) { variants_cleared++; note('V'); } +static boolean network_game_start_new_server(struct widget_instance *widget, struct event_record *event, boolean *deleted) +{ + CHECK(!bss_004566dc.client && !bss_004566dc.server, "new host started before old host/client disposed"); + new_servers++; note('N'); return TRUE; +} + +#include "under_test.inc" + +int main(int argc, char **argv) +{ + const char *case_name = argc > 1 ? argv[1] : ""; + struct network_connection connection = { 0 }; + struct network_game_client client = { 0 }; + struct network_game_server host = { _network_game_server_state_pregame, + FLAG(_network_game_server_game_open_bit) | FLAG(_network_game_server_game_valid_bit), &connection, 123, TRUE }; + boolean deleted = FALSE; + disposing = &host; + bss_004566dc.server = &host; bss_004566dc.client = &client; + bss_004566dc.quickstart_local = bss_004566dc.client_started = TRUE; + network_game_server_memory_do_not_use_directly_in_use = TRUE; + strcpy(network_game_server_cooperative_next_map, "next-level"); + expected_message = _message_server_graceful_game_exit_pregame; + boolean back = !strcmp(case_name, "back"), reopen = !strcmp(case_name, "reopen"); + CASE("postgame") { host.state = _network_game_server_state_postgame; expected_message = _message_server_graceful_game_exit_postgame; } + CASE("ingame") { host.state = _network_game_server_state_ingame; } + CASE("message-failure") { message_fails = TRUE; } + CASE("send-failure") { send_fails = TRUE; } + CASE("machine-failure") { machines_fail = TRUE; } + CASE("no-server") + { + bss_004566dc.server = NULL; + CHECK(ui_widget_port_host(NULL, NULL, &deleted), "new host failed"); + CHECK(!slept && !closed && !handled && !terminated && clients_closed == 1 && new_servers == 1, + "first host creation performed nonexistent server teardown"); + return 0; + } + if (back) CHECK(clear_multiplayer_player_joins(NULL, NULL, &deleted), "back callback failed"); + else if (reopen) CHECK(ui_widget_port_host(NULL, NULL, &deleted), "reopen callback failed"); + else network_game_server_dispose(&host); + CHECK(slept == EXPECTED_SLEEP && closed == 1 && handled == 1 && terminated == 1, "wrong sleep or incomplete teardown: %s", order); + CHECK(errors == (machines_fail ? 1 : 0), "error handling changed"); + int expected_messages = strcmp(case_name, "ingame") ? 1 : 0; + CHECK(messages == expected_messages && broadcasts == (message_fails ? 0 : expected_messages), "graceful-exit notice lost"); + char expected[64] = ""; + if (back || reopen) strcat(expected, "L"); + if (expected_messages) strcat(expected, message_fails ? "M" : "MB"); + strcat(expected, EXPECTED_SLEEP ? "HCSTP" : "HCTP"); + if (back) strcat(expected, "JV"); + if (reopen) strcat(expected, "N"); + CHECK(!strcmp(order, expected), "cleanup sequence changed: %s != %s", order, expected); + if (back || reopen) + CHECK(!bss_004566dc.server && !bss_004566dc.client && !bss_004566dc.quickstart_local && !bss_004566dc.client_started && + clients_closed == 1 && new_servers == reopen && joins_cleared == back && variants_cleared == back, + "menu lifecycle state changed"); + return 0; +} diff --git a/tools/harness/tests/test_server_teardown.py b/tools/harness/tests/test_server_teardown.py new file mode 100644 index 00000000..c4423b16 --- /dev/null +++ b/tools/harness/tests/test_server_teardown.py @@ -0,0 +1,66 @@ +"""Real menu and server teardown, including native/legacy platform guards.""" +import re +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, constant, enum_with, function, mutated, read, run # noqa: E402 + +PLATFORMS = { + 'windows': ('#define HALO_WINDOWS 1', 0), + 'linux': ('#define __linux__ 1', 0), + 'android': ('#define HALO_ANDROID 1', 0), + 'xbox': ('', 1000), + 'other-port': ('#define HALO_UNEXAMINED_PORT 1', 1000), +} +CASES = [(platform, case) for platform in PLATFORMS for case in ('back', 'reopen')] +CASES += [('windows', case) for case in ('postgame', 'ingame', 'message-failure', + 'send-failure', 'machine-failure', 'no-server')] +CASES += [('xbox', case) for case in ('postgame', 'ingame')] +GUARD = '#if !defined(HALO_WINDOWS) && !defined(HALO_ANDROID) && !defined(__linux__)' +CONTROLS = { + 'native-sleeps': (GUARD, '#if 1', 'windows', 'back'), + 'xbox-loses-grace': ('SleepEx(MILLISECONDS_PER_SECOND, FALSE);', ';', 'xbox', 'back'), + 'transport-not-terminated': ('transport_server_terminate();', ';', 'windows', 'back'), + 'connections-not-closed': ('network_connection_delete(server->connection);', ';', 'windows', 'reopen'), + 'server-state-not-cleared': ('csmemset(server, 0, sizeof(*server));', ';', 'windows', 'back'), +} + + +def generated(platform, control=None): + server = read('source/networking/network_server_manager.c') + code = function(server, 'network_game_server_dispose') + if control: + before, after, _, _ = CONTROLS[control] + code = mutated(code, before, after) + globals_source = read('source/networking/network_game_globals.c') + for name in ('dispose_global_network_game_server', 'dispose_global_network_game_client'): + code += '\n' + function(globals_source, name) + widgets = read('source/interface/ui_widget_event_handler_functions.c') + for name in ('clear_multiplayer_player_joins', 'ui_widget_port_host'): + code += '\n' + function(widgets, name) + defines, sleep = PLATFORMS[platform] + # The harness runs on Linux; each compiled variant selects its own target. + config = '#undef __linux__\n#define xbox 1\n' + defines + f'\n#define EXPECTED_SLEEP {sleep}\n' + config += f'#define MILLISECONDS_PER_SECOND {constant(read("source/cseries/cseries.h"), "MILLISECONDS_PER_SECOND")}\n' + for member in ('_network_game_server_state_pregame', '_network_game_server_game_open_bit'): + config += enum_with(server, member) + '\n' + config += enum_with(read('source/networking/network_messages.h'), '_message_server_graceful_game_exit_pregame') + '\n' + for name in ('message_server_graceful_game_exit_pregame', 'message_server_graceful_game_exit_postgame'): + config += re.search(r'struct ' + name + r'\s*\{[^}]+\};', server).group() + '\n' + return (('config.inc', config), ('under_test.inc', code)) + + +@pytest.mark.parametrize('platform,case', CASES) +def test_case(platform, case): + status, output = run(build('server_teardown', generated(platform)), case) + assert status == 0, output + + +@pytest.mark.parametrize('control', CONTROLS) +def test_negative_control(control): + _, _, platform, case = CONTROLS[control] + status, output = run(build('server_teardown', generated(platform, control)), case) + assert status == CHECK_FAILED, f'{control} was not caught: {output}' From f97b33391bfcb69b38006c1ffe93740f2d19c70d Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:20:57 +1300 Subject: [PATCH 071/187] Voice chat Opus voice chat (port/third_party/opus 1.5.2, port/linux/src/voice_audio.c, port/linux/game/network_voice.c), relayed by the host, in the lobby too. The host decides in Server Setup > Teamplay Options: lobby voice (on by default), the game's mode (off, team near, anyone near, team, team and enemies near: the default), the near distance, and the quality, 8 to 64 kbps (24). Each player picks push to talk (V), open mic or off, the voices' volume, and the output and input devices in Settings > Audio, and mutes others from the scoreboard's menu. A speaker icon marks who talks in the lobby, on the scoreboard and in a list in game. The host takes a machine's frames only with the key it gave it over its stream, within the quality's size, 50 a second and 8 talkers at once, and sends voices only to machines that asked for them, so the network version stays. --- port/android/app/src/main/AndroidManifest.xml | 2 + port/android/guest/runtime/guest_host.h | 4 + port/android/guest/runtime/guest_sdl.c | 15 + port/android/host/host_sdl.c | 41 +- port/android/host_imports.list | 3 + port/assets/menus/UNWIRED.md | 2 +- ...layer_setup.teamplay_options_edit.port.xml | 68 + ...ultiplayer_setup.teamplay_options_edit.xml | 4 + ...tup.player_profile_edit.audio_settings.xml | 75 +- port/assets/menus/ce/strings.xml | 25 +- port/assets/menus/menus.json | 1 + port/linux/NETCODE.md | 21 + port/linux/README.md | 50 +- port/linux/game/menu_functions.c | 124 +- port/linux/game/menu_tags.c | 41 +- port/linux/game/network_distributed.c | 7 + port/linux/game/network_distributed.h | 6 + port/linux/game/network_voice.c | 1047 ++++++ port/linux/game/network_voice.h | 59 + port/linux/include/halo_keyboard.h | 6 + port/linux/src/dsound_sdl.c | 59 +- port/linux/src/port_config.c | 37 + port/linux/src/sdl_platform.c | 71 + port/linux/src/sdl_platform.h | 9 + port/linux/src/voice_audio.c | 548 +++ port/linux/src/voice_audio.h | 42 + port/linux/src/xinput_sdl.c | 18 +- port/third_party/opus/AUTHORS | 6 + port/third_party/opus/COPYING | 44 + port/third_party/opus/PORT.md | 11 + port/third_party/opus/README | 189 + port/third_party/opus/celt/_kiss_fft_guts.h | 182 + port/third_party/opus/celt/arch.h | 291 ++ port/third_party/opus/celt/bands.c | 1673 +++++++++ port/third_party/opus/celt/bands.h | 123 + port/third_party/opus/celt/celt.c | 316 ++ port/third_party/opus/celt/celt.h | 252 ++ port/third_party/opus/celt/celt_decoder.c | 1591 +++++++++ port/third_party/opus/celt/celt_encoder.c | 2639 ++++++++++++++ port/third_party/opus/celt/celt_lpc.c | 363 ++ port/third_party/opus/celt/celt_lpc.h | 66 + port/third_party/opus/celt/cpu_support.h | 72 + port/third_party/opus/celt/cwrs.c | 715 ++++ port/third_party/opus/celt/cwrs.h | 48 + port/third_party/opus/celt/ecintrin.h | 91 + port/third_party/opus/celt/entcode.c | 153 + port/third_party/opus/celt/entcode.h | 152 + port/third_party/opus/celt/entdec.c | 266 ++ port/third_party/opus/celt/entdec.h | 110 + port/third_party/opus/celt/entenc.c | 305 ++ port/third_party/opus/celt/entenc.h | 119 + port/third_party/opus/celt/fixed_debug.h | 836 +++++ port/third_party/opus/celt/fixed_generic.h | 188 + port/third_party/opus/celt/float_cast.h | 152 + port/third_party/opus/celt/kiss_fft.c | 604 ++++ port/third_party/opus/celt/kiss_fft.h | 210 ++ port/third_party/opus/celt/laplace.c | 235 ++ port/third_party/opus/celt/laplace.h | 57 + port/third_party/opus/celt/mathops.c | 209 ++ port/third_party/opus/celt/mathops.h | 296 ++ port/third_party/opus/celt/mdct.c | 343 ++ port/third_party/opus/celt/mdct.h | 112 + port/third_party/opus/celt/mfrngcod.h | 48 + port/third_party/opus/celt/modes.c | 445 +++ port/third_party/opus/celt/modes.h | 75 + port/third_party/opus/celt/os_support.h | 99 + port/third_party/opus/celt/pitch.c | 555 +++ port/third_party/opus/celt/pitch.h | 203 ++ port/third_party/opus/celt/quant_bands.c | 563 +++ port/third_party/opus/celt/quant_bands.h | 66 + port/third_party/opus/celt/rate.c | 646 ++++ port/third_party/opus/celt/rate.h | 101 + port/third_party/opus/celt/stack_alloc.h | 184 + .../opus/celt/static_modes_fixed.h | 892 +++++ .../opus/celt/static_modes_fixed_arm_ne10.h | 388 +++ .../opus/celt/static_modes_float.h | 888 +++++ .../opus/celt/static_modes_float_arm_ne10.h | 404 +++ port/third_party/opus/celt/vq.c | 442 +++ port/third_party/opus/celt/vq.h | 79 + port/third_party/opus/include/opus.h | 1099 ++++++ port/third_party/opus/include/opus_custom.h | 343 ++ port/third_party/opus/include/opus_defines.h | 830 +++++ .../opus/include/opus_multistream.h | 660 ++++ .../opus/include/opus_projection.h | 568 +++ port/third_party/opus/include/opus_types.h | 166 + port/third_party/opus/silk/A2NLSF.c | 267 ++ port/third_party/opus/silk/API.h | 156 + port/third_party/opus/silk/CNG.c | 188 + .../opus/silk/HP_variable_cutoff.c | 77 + port/third_party/opus/silk/Inlines.h | 188 + .../opus/silk/LPC_analysis_filter.c | 111 + port/third_party/opus/silk/LPC_fit.c | 82 + .../third_party/opus/silk/LPC_inv_pred_gain.c | 141 + .../opus/silk/LP_variable_cutoff.c | 135 + port/third_party/opus/silk/MacroCount.h | 710 ++++ port/third_party/opus/silk/MacroDebug.h | 945 +++++ port/third_party/opus/silk/NLSF2A.c | 141 + port/third_party/opus/silk/NLSF_VQ.c | 76 + .../opus/silk/NLSF_VQ_weights_laroia.c | 80 + port/third_party/opus/silk/NLSF_decode.c | 93 + .../opus/silk/NLSF_del_dec_quant.c | 215 ++ port/third_party/opus/silk/NLSF_encode.c | 124 + port/third_party/opus/silk/NLSF_stabilize.c | 142 + port/third_party/opus/silk/NLSF_unpack.c | 55 + port/third_party/opus/silk/NSQ.c | 437 +++ port/third_party/opus/silk/NSQ.h | 101 + port/third_party/opus/silk/NSQ_del_dec.c | 733 ++++ port/third_party/opus/silk/PLC.c | 493 +++ port/third_party/opus/silk/PLC.h | 65 + port/third_party/opus/silk/SigProc_FIX.h | 643 ++++ port/third_party/opus/silk/VAD.c | 360 ++ port/third_party/opus/silk/VQ_WMat_EC.c | 131 + port/third_party/opus/silk/ana_filt_bank_1.c | 74 + port/third_party/opus/silk/biquad_alt.c | 121 + port/third_party/opus/silk/bwexpander.c | 51 + port/third_party/opus/silk/bwexpander_32.c | 51 + .../opus/silk/check_control_input.c | 106 + port/third_party/opus/silk/code_signs.c | 115 + port/third_party/opus/silk/control.h | 161 + port/third_party/opus/silk/control_SNR.c | 113 + .../opus/silk/control_audio_bandwidth.c | 132 + port/third_party/opus/silk/control_codec.c | 423 +++ port/third_party/opus/silk/debug.c | 174 + port/third_party/opus/silk/debug.h | 267 ++ port/third_party/opus/silk/dec_API.c | 486 +++ port/third_party/opus/silk/decode_core.c | 237 ++ port/third_party/opus/silk/decode_frame.c | 169 + port/third_party/opus/silk/decode_indices.c | 151 + .../third_party/opus/silk/decode_parameters.c | 115 + port/third_party/opus/silk/decode_pitch.c | 77 + port/third_party/opus/silk/decode_pulses.c | 115 + port/third_party/opus/silk/decoder_set_fs.c | 108 + port/third_party/opus/silk/define.h | 235 ++ port/third_party/opus/silk/enc_API.c | 591 ++++ port/third_party/opus/silk/encode_indices.c | 181 + port/third_party/opus/silk/encode_pulses.c | 206 ++ port/third_party/opus/silk/errors.h | 98 + .../opus/silk/float/LPC_analysis_filter_FLP.c | 249 ++ .../opus/silk/float/LPC_inv_pred_gain_FLP.c | 73 + .../opus/silk/float/LTP_analysis_filter_FLP.c | 75 + .../opus/silk/float/LTP_scale_ctrl_FLP.c | 58 + .../third_party/opus/silk/float/SigProc_FLP.h | 205 ++ .../opus/silk/float/apply_sine_window_FLP.c | 81 + .../opus/silk/float/autocorrelation_FLP.c | 53 + .../opus/silk/float/burg_modified_FLP.c | 187 + .../opus/silk/float/bwexpander_FLP.c | 49 + .../opus/silk/float/corrMatrix_FLP.c | 95 + .../opus/silk/float/encode_frame_FLP.c | 432 +++ port/third_party/opus/silk/float/energy_FLP.c | 59 + .../opus/silk/float/find_LPC_FLP.c | 105 + .../opus/silk/float/find_LTP_FLP.c | 65 + .../opus/silk/float/find_pitch_lags_FLP.c | 132 + .../opus/silk/float/find_pred_coefs_FLP.c | 117 + .../opus/silk/float/inner_product_FLP.c | 59 + port/third_party/opus/silk/float/k2a_FLP.c | 54 + port/third_party/opus/silk/float/main_FLP.h | 290 ++ .../silk/float/noise_shape_analysis_FLP.c | 350 ++ .../opus/silk/float/pitch_analysis_core_FLP.c | 630 ++++ .../opus/silk/float/process_gains_FLP.c | 103 + .../silk/float/regularize_correlations_FLP.c | 48 + .../opus/silk/float/residual_energy_FLP.c | 117 + .../opus/silk/float/scale_copy_vector_FLP.c | 57 + .../opus/silk/float/scale_vector_FLP.c | 56 + port/third_party/opus/silk/float/schur_FLP.c | 70 + port/third_party/opus/silk/float/sort_FLP.c | 83 + .../third_party/opus/silk/float/structs_FLP.h | 112 + .../silk/float/warped_autocorrelation_FLP.c | 75 + .../opus/silk/float/wrappers_FLP.c | 209 ++ port/third_party/opus/silk/gain_quant.c | 142 + port/third_party/opus/silk/init_decoder.c | 84 + port/third_party/opus/silk/init_encoder.c | 68 + .../opus/silk/inner_prod_aligned.c | 47 + port/third_party/opus/silk/interpolate.c | 51 + port/third_party/opus/silk/lin2log.c | 46 + port/third_party/opus/silk/log2lin.c | 58 + port/third_party/opus/silk/macros.h | 151 + port/third_party/opus/silk/main.h | 486 +++ .../third_party/opus/silk/pitch_est_defines.h | 88 + port/third_party/opus/silk/pitch_est_tables.c | 99 + port/third_party/opus/silk/process_NLSFs.c | 107 + port/third_party/opus/silk/quant_LTP_gains.c | 132 + port/third_party/opus/silk/resampler.c | 215 ++ port/third_party/opus/silk/resampler_down2.c | 74 + .../third_party/opus/silk/resampler_down2_3.c | 103 + .../third_party/opus/silk/resampler_private.h | 88 + .../opus/silk/resampler_private_AR2.c | 55 + .../opus/silk/resampler_private_IIR_FIR.c | 107 + .../opus/silk/resampler_private_down_FIR.c | 194 ++ .../opus/silk/resampler_private_up2_HQ.c | 113 + port/third_party/opus/silk/resampler_rom.c | 96 + port/third_party/opus/silk/resampler_rom.h | 68 + .../third_party/opus/silk/resampler_structs.h | 60 + port/third_party/opus/silk/shell_coder.c | 151 + port/third_party/opus/silk/sigm_Q15.c | 76 + port/third_party/opus/silk/sort.c | 154 + port/third_party/opus/silk/stereo_LR_to_MS.c | 229 ++ port/third_party/opus/silk/stereo_MS_to_LR.c | 85 + .../opus/silk/stereo_decode_pred.c | 73 + .../opus/silk/stereo_encode_pred.c | 62 + .../opus/silk/stereo_find_predictor.c | 79 + .../third_party/opus/silk/stereo_quant_pred.c | 73 + port/third_party/opus/silk/structs.h | 357 ++ port/third_party/opus/silk/sum_sqr_shift.c | 83 + port/third_party/opus/silk/table_LSF_cos.c | 70 + port/third_party/opus/silk/tables.h | 114 + port/third_party/opus/silk/tables_LTP.c | 294 ++ .../opus/silk/tables_NLSF_CB_NB_MB.c | 195 ++ .../third_party/opus/silk/tables_NLSF_CB_WB.c | 234 ++ port/third_party/opus/silk/tables_gain.c | 63 + port/third_party/opus/silk/tables_other.c | 124 + port/third_party/opus/silk/tables_pitch_lag.c | 69 + .../opus/silk/tables_pulses_per_block.c | 264 ++ .../third_party/opus/silk/tuning_parameters.h | 155 + port/third_party/opus/silk/typedef.h | 81 + port/third_party/opus/sources.txt | 131 + port/third_party/opus/src/analysis.c | 983 ++++++ port/third_party/opus/src/analysis.h | 103 + port/third_party/opus/src/extensions.c | 315 ++ port/third_party/opus/src/mlp.c | 132 + port/third_party/opus/src/mlp.h | 60 + port/third_party/opus/src/mlp_data.c | 672 ++++ port/third_party/opus/src/opus.c | 362 ++ port/third_party/opus/src/opus_decoder.c | 1508 ++++++++ port/third_party/opus/src/opus_encoder.c | 3089 +++++++++++++++++ port/third_party/opus/src/opus_private.h | 223 ++ port/third_party/opus/src/repacketizer.c | 464 +++ source/game/game_engine.c | 110 +- source/interface/ui_widget.c | 16 + source/main/main.c | 3 + source/networking/network_server_manager.c | 4 + .../network_server_message_handler.c | 28 + tools/android_build.py | 6 +- tools/ci_build.py | 3 + tools/harness/tests/test_voice_route.py | 59 + tools/harness/tests/voice_route.c | 104 + tools/linux_build.py | 20 + tools/port_settings.py | 109 +- tools/windows_build.py | 7 +- 238 files changed, 55240 insertions(+), 30 deletions(-) create mode 100644 port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml create mode 100644 port/linux/game/network_voice.c create mode 100644 port/linux/game/network_voice.h create mode 100644 port/linux/src/voice_audio.c create mode 100644 port/linux/src/voice_audio.h create mode 100644 port/third_party/opus/AUTHORS create mode 100644 port/third_party/opus/COPYING create mode 100644 port/third_party/opus/PORT.md create mode 100644 port/third_party/opus/README create mode 100644 port/third_party/opus/celt/_kiss_fft_guts.h create mode 100644 port/third_party/opus/celt/arch.h create mode 100644 port/third_party/opus/celt/bands.c create mode 100644 port/third_party/opus/celt/bands.h create mode 100644 port/third_party/opus/celt/celt.c create mode 100644 port/third_party/opus/celt/celt.h create mode 100644 port/third_party/opus/celt/celt_decoder.c create mode 100644 port/third_party/opus/celt/celt_encoder.c create mode 100644 port/third_party/opus/celt/celt_lpc.c create mode 100644 port/third_party/opus/celt/celt_lpc.h create mode 100644 port/third_party/opus/celt/cpu_support.h create mode 100644 port/third_party/opus/celt/cwrs.c create mode 100644 port/third_party/opus/celt/cwrs.h create mode 100644 port/third_party/opus/celt/ecintrin.h create mode 100644 port/third_party/opus/celt/entcode.c create mode 100644 port/third_party/opus/celt/entcode.h create mode 100644 port/third_party/opus/celt/entdec.c create mode 100644 port/third_party/opus/celt/entdec.h create mode 100644 port/third_party/opus/celt/entenc.c create mode 100644 port/third_party/opus/celt/entenc.h create mode 100644 port/third_party/opus/celt/fixed_debug.h create mode 100644 port/third_party/opus/celt/fixed_generic.h create mode 100644 port/third_party/opus/celt/float_cast.h create mode 100644 port/third_party/opus/celt/kiss_fft.c create mode 100644 port/third_party/opus/celt/kiss_fft.h create mode 100644 port/third_party/opus/celt/laplace.c create mode 100644 port/third_party/opus/celt/laplace.h create mode 100644 port/third_party/opus/celt/mathops.c create mode 100644 port/third_party/opus/celt/mathops.h create mode 100644 port/third_party/opus/celt/mdct.c create mode 100644 port/third_party/opus/celt/mdct.h create mode 100644 port/third_party/opus/celt/mfrngcod.h create mode 100644 port/third_party/opus/celt/modes.c create mode 100644 port/third_party/opus/celt/modes.h create mode 100644 port/third_party/opus/celt/os_support.h create mode 100644 port/third_party/opus/celt/pitch.c create mode 100644 port/third_party/opus/celt/pitch.h create mode 100644 port/third_party/opus/celt/quant_bands.c create mode 100644 port/third_party/opus/celt/quant_bands.h create mode 100644 port/third_party/opus/celt/rate.c create mode 100644 port/third_party/opus/celt/rate.h create mode 100644 port/third_party/opus/celt/stack_alloc.h create mode 100644 port/third_party/opus/celt/static_modes_fixed.h create mode 100644 port/third_party/opus/celt/static_modes_fixed_arm_ne10.h create mode 100644 port/third_party/opus/celt/static_modes_float.h create mode 100644 port/third_party/opus/celt/static_modes_float_arm_ne10.h create mode 100644 port/third_party/opus/celt/vq.c create mode 100644 port/third_party/opus/celt/vq.h create mode 100644 port/third_party/opus/include/opus.h create mode 100644 port/third_party/opus/include/opus_custom.h create mode 100644 port/third_party/opus/include/opus_defines.h create mode 100644 port/third_party/opus/include/opus_multistream.h create mode 100644 port/third_party/opus/include/opus_projection.h create mode 100644 port/third_party/opus/include/opus_types.h create mode 100644 port/third_party/opus/silk/A2NLSF.c create mode 100644 port/third_party/opus/silk/API.h create mode 100644 port/third_party/opus/silk/CNG.c create mode 100644 port/third_party/opus/silk/HP_variable_cutoff.c create mode 100644 port/third_party/opus/silk/Inlines.h create mode 100644 port/third_party/opus/silk/LPC_analysis_filter.c create mode 100644 port/third_party/opus/silk/LPC_fit.c create mode 100644 port/third_party/opus/silk/LPC_inv_pred_gain.c create mode 100644 port/third_party/opus/silk/LP_variable_cutoff.c create mode 100644 port/third_party/opus/silk/MacroCount.h create mode 100644 port/third_party/opus/silk/MacroDebug.h create mode 100644 port/third_party/opus/silk/NLSF2A.c create mode 100644 port/third_party/opus/silk/NLSF_VQ.c create mode 100644 port/third_party/opus/silk/NLSF_VQ_weights_laroia.c create mode 100644 port/third_party/opus/silk/NLSF_decode.c create mode 100644 port/third_party/opus/silk/NLSF_del_dec_quant.c create mode 100644 port/third_party/opus/silk/NLSF_encode.c create mode 100644 port/third_party/opus/silk/NLSF_stabilize.c create mode 100644 port/third_party/opus/silk/NLSF_unpack.c create mode 100644 port/third_party/opus/silk/NSQ.c create mode 100644 port/third_party/opus/silk/NSQ.h create mode 100644 port/third_party/opus/silk/NSQ_del_dec.c create mode 100644 port/third_party/opus/silk/PLC.c create mode 100644 port/third_party/opus/silk/PLC.h create mode 100644 port/third_party/opus/silk/SigProc_FIX.h create mode 100644 port/third_party/opus/silk/VAD.c create mode 100644 port/third_party/opus/silk/VQ_WMat_EC.c create mode 100644 port/third_party/opus/silk/ana_filt_bank_1.c create mode 100644 port/third_party/opus/silk/biquad_alt.c create mode 100644 port/third_party/opus/silk/bwexpander.c create mode 100644 port/third_party/opus/silk/bwexpander_32.c create mode 100644 port/third_party/opus/silk/check_control_input.c create mode 100644 port/third_party/opus/silk/code_signs.c create mode 100644 port/third_party/opus/silk/control.h create mode 100644 port/third_party/opus/silk/control_SNR.c create mode 100644 port/third_party/opus/silk/control_audio_bandwidth.c create mode 100644 port/third_party/opus/silk/control_codec.c create mode 100644 port/third_party/opus/silk/debug.c create mode 100644 port/third_party/opus/silk/debug.h create mode 100644 port/third_party/opus/silk/dec_API.c create mode 100644 port/third_party/opus/silk/decode_core.c create mode 100644 port/third_party/opus/silk/decode_frame.c create mode 100644 port/third_party/opus/silk/decode_indices.c create mode 100644 port/third_party/opus/silk/decode_parameters.c create mode 100644 port/third_party/opus/silk/decode_pitch.c create mode 100644 port/third_party/opus/silk/decode_pulses.c create mode 100644 port/third_party/opus/silk/decoder_set_fs.c create mode 100644 port/third_party/opus/silk/define.h create mode 100644 port/third_party/opus/silk/enc_API.c create mode 100644 port/third_party/opus/silk/encode_indices.c create mode 100644 port/third_party/opus/silk/encode_pulses.c create mode 100644 port/third_party/opus/silk/errors.h create mode 100644 port/third_party/opus/silk/float/LPC_analysis_filter_FLP.c create mode 100644 port/third_party/opus/silk/float/LPC_inv_pred_gain_FLP.c create mode 100644 port/third_party/opus/silk/float/LTP_analysis_filter_FLP.c create mode 100644 port/third_party/opus/silk/float/LTP_scale_ctrl_FLP.c create mode 100644 port/third_party/opus/silk/float/SigProc_FLP.h create mode 100644 port/third_party/opus/silk/float/apply_sine_window_FLP.c create mode 100644 port/third_party/opus/silk/float/autocorrelation_FLP.c create mode 100644 port/third_party/opus/silk/float/burg_modified_FLP.c create mode 100644 port/third_party/opus/silk/float/bwexpander_FLP.c create mode 100644 port/third_party/opus/silk/float/corrMatrix_FLP.c create mode 100644 port/third_party/opus/silk/float/encode_frame_FLP.c create mode 100644 port/third_party/opus/silk/float/energy_FLP.c create mode 100644 port/third_party/opus/silk/float/find_LPC_FLP.c create mode 100644 port/third_party/opus/silk/float/find_LTP_FLP.c create mode 100644 port/third_party/opus/silk/float/find_pitch_lags_FLP.c create mode 100644 port/third_party/opus/silk/float/find_pred_coefs_FLP.c create mode 100644 port/third_party/opus/silk/float/inner_product_FLP.c create mode 100644 port/third_party/opus/silk/float/k2a_FLP.c create mode 100644 port/third_party/opus/silk/float/main_FLP.h create mode 100644 port/third_party/opus/silk/float/noise_shape_analysis_FLP.c create mode 100644 port/third_party/opus/silk/float/pitch_analysis_core_FLP.c create mode 100644 port/third_party/opus/silk/float/process_gains_FLP.c create mode 100644 port/third_party/opus/silk/float/regularize_correlations_FLP.c create mode 100644 port/third_party/opus/silk/float/residual_energy_FLP.c create mode 100644 port/third_party/opus/silk/float/scale_copy_vector_FLP.c create mode 100644 port/third_party/opus/silk/float/scale_vector_FLP.c create mode 100644 port/third_party/opus/silk/float/schur_FLP.c create mode 100644 port/third_party/opus/silk/float/sort_FLP.c create mode 100644 port/third_party/opus/silk/float/structs_FLP.h create mode 100644 port/third_party/opus/silk/float/warped_autocorrelation_FLP.c create mode 100644 port/third_party/opus/silk/float/wrappers_FLP.c create mode 100644 port/third_party/opus/silk/gain_quant.c create mode 100644 port/third_party/opus/silk/init_decoder.c create mode 100644 port/third_party/opus/silk/init_encoder.c create mode 100644 port/third_party/opus/silk/inner_prod_aligned.c create mode 100644 port/third_party/opus/silk/interpolate.c create mode 100644 port/third_party/opus/silk/lin2log.c create mode 100644 port/third_party/opus/silk/log2lin.c create mode 100644 port/third_party/opus/silk/macros.h create mode 100644 port/third_party/opus/silk/main.h create mode 100644 port/third_party/opus/silk/pitch_est_defines.h create mode 100644 port/third_party/opus/silk/pitch_est_tables.c create mode 100644 port/third_party/opus/silk/process_NLSFs.c create mode 100644 port/third_party/opus/silk/quant_LTP_gains.c create mode 100644 port/third_party/opus/silk/resampler.c create mode 100644 port/third_party/opus/silk/resampler_down2.c create mode 100644 port/third_party/opus/silk/resampler_down2_3.c create mode 100644 port/third_party/opus/silk/resampler_private.h create mode 100644 port/third_party/opus/silk/resampler_private_AR2.c create mode 100644 port/third_party/opus/silk/resampler_private_IIR_FIR.c create mode 100644 port/third_party/opus/silk/resampler_private_down_FIR.c create mode 100644 port/third_party/opus/silk/resampler_private_up2_HQ.c create mode 100644 port/third_party/opus/silk/resampler_rom.c create mode 100644 port/third_party/opus/silk/resampler_rom.h create mode 100644 port/third_party/opus/silk/resampler_structs.h create mode 100644 port/third_party/opus/silk/shell_coder.c create mode 100644 port/third_party/opus/silk/sigm_Q15.c create mode 100644 port/third_party/opus/silk/sort.c create mode 100644 port/third_party/opus/silk/stereo_LR_to_MS.c create mode 100644 port/third_party/opus/silk/stereo_MS_to_LR.c create mode 100644 port/third_party/opus/silk/stereo_decode_pred.c create mode 100644 port/third_party/opus/silk/stereo_encode_pred.c create mode 100644 port/third_party/opus/silk/stereo_find_predictor.c create mode 100644 port/third_party/opus/silk/stereo_quant_pred.c create mode 100644 port/third_party/opus/silk/structs.h create mode 100644 port/third_party/opus/silk/sum_sqr_shift.c create mode 100644 port/third_party/opus/silk/table_LSF_cos.c create mode 100644 port/third_party/opus/silk/tables.h create mode 100644 port/third_party/opus/silk/tables_LTP.c create mode 100644 port/third_party/opus/silk/tables_NLSF_CB_NB_MB.c create mode 100644 port/third_party/opus/silk/tables_NLSF_CB_WB.c create mode 100644 port/third_party/opus/silk/tables_gain.c create mode 100644 port/third_party/opus/silk/tables_other.c create mode 100644 port/third_party/opus/silk/tables_pitch_lag.c create mode 100644 port/third_party/opus/silk/tables_pulses_per_block.c create mode 100644 port/third_party/opus/silk/tuning_parameters.h create mode 100644 port/third_party/opus/silk/typedef.h create mode 100644 port/third_party/opus/sources.txt create mode 100644 port/third_party/opus/src/analysis.c create mode 100644 port/third_party/opus/src/analysis.h create mode 100644 port/third_party/opus/src/extensions.c create mode 100644 port/third_party/opus/src/mlp.c create mode 100644 port/third_party/opus/src/mlp.h create mode 100644 port/third_party/opus/src/mlp_data.c create mode 100644 port/third_party/opus/src/opus.c create mode 100644 port/third_party/opus/src/opus_decoder.c create mode 100644 port/third_party/opus/src/opus_encoder.c create mode 100644 port/third_party/opus/src/opus_private.h create mode 100644 port/third_party/opus/src/repacketizer.c create mode 100644 tools/harness/tests/test_voice_route.py create mode 100644 tools/harness/tests/voice_route.c diff --git a/port/android/app/src/main/AndroidManifest.xml b/port/android/app/src/main/AndroidManifest.xml index 08d788e9..cccdd89c 100644 --- a/port/android/app/src/main/AndroidManifest.xml +++ b/port/android/app/src/main/AndroidManifest.xml @@ -15,6 +15,8 @@ + + diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h index e77403c2..ca863654 100644 --- a/port/android/guest/runtime/guest_host.h +++ b/port/android/guest/runtime/guest_host.h @@ -85,6 +85,10 @@ called on the audio thread */ unsigned int host_sdl_open_audio_stream(unsigned int device, const void *spec, unsigned int callback, unsigned int userdata); int host_sdl_put_audio_stream_data(unsigned int stream, const void *data, int length); int host_sdl_resume_audio_stream_device(unsigned int stream); +/* (voice chat's microphone, a stream without a callback) */ +int host_sdl_get_audio_stream_data(unsigned int stream, void *data, int length); +int host_sdl_get_audio_stream_available(unsigned int stream); +void host_sdl_destroy_audio_stream(unsigned int stream); /* ---------- OpenGL ES */ diff --git a/port/android/guest/runtime/guest_sdl.c b/port/android/guest/runtime/guest_sdl.c index 7bd89689..21197ba1 100644 --- a/port/android/guest/runtime/guest_sdl.c +++ b/port/android/guest/runtime/guest_sdl.c @@ -234,3 +234,18 @@ bool SDL_ResumeAudioStreamDevice(SDL_AudioStream *stream) { return host_sdl_resume_audio_stream_device((unsigned int)stream) != 0; } + +int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *data, int length) +{ + return host_sdl_get_audio_stream_data((unsigned int)stream, data, length); +} + +int SDL_GetAudioStreamAvailable(SDL_AudioStream *stream) +{ + return host_sdl_get_audio_stream_available((unsigned int)stream); +} + +void SDL_DestroyAudioStream(SDL_AudioStream *stream) +{ + host_sdl_destroy_audio_stream((unsigned int)stream); +} diff --git a/port/android/host/host_sdl.c b/port/android/host/host_sdl.c index 0167e453..e994539d 100644 --- a/port/android/host/host_sdl.c +++ b/port/android/host/host_sdl.c @@ -339,9 +339,18 @@ static int audio_keep(struct audio_binding *binding, const void *data, int lengt uint32_t host_sdl_open_audio_stream(uint32_t device, const void *spec, uint32_t callback, uint32_t userdata) { - struct audio_binding *binding = SDL_calloc(1, sizeof(*binding)); + struct audio_binding *binding; SDL_AudioStream *stream; + /* (one without a callback, the microphone's, which the guest reads: + nothing to bind) */ + if (!callback) + { + stream = SDL_OpenAudioDeviceStream((SDL_AudioDeviceID)device, spec, NULL, NULL); + return stream ? handle_new(_handle_audio, stream) : 0; + } + binding = SDL_calloc(1, sizeof(*binding)); + binding->callback = callback; binding->userdata = userdata; pthread_mutex_init(&binding->lock, NULL); @@ -379,6 +388,36 @@ int host_sdl_resume_audio_stream_device(uint32_t stream) return object ? SDL_ResumeAudioStreamDevice(object) : 0; } +/* (voice chat's microphone: port/linux/src/voice_audio.c) */ +int host_sdl_get_audio_stream_data(uint32_t stream, void *data, int length) +{ + SDL_AudioStream *object = handle_get(stream, _handle_audio); + + return object ? SDL_GetAudioStreamData(object, data, length) : -1; +} + +int host_sdl_get_audio_stream_available(uint32_t stream) +{ + SDL_AudioStream *object = handle_get(stream, _handle_audio); + + return object ? SDL_GetAudioStreamAvailable(object) : -1; +} + +/* (a stream opened without a callback only: one with a callback has a +thread the guest's callbacks run on) */ +void host_sdl_destroy_audio_stream(uint32_t stream) +{ + SDL_AudioStream *object = handle_get(stream, _handle_audio); + + if (!object) + return; + pthread_mutex_lock(&handle_lock); + handles[stream].type = _handle_free; + handles[stream].object = NULL; + pthread_mutex_unlock(&handle_lock); + SDL_DestroyAudioStream(object); +} + /* ---------- the clipboard (internet play's invite links) */ int host_sdl_set_clipboard_text(const char *text) diff --git a/port/android/host_imports.list b/port/android/host_imports.list index e7a81fb1..7f2d97c6 100644 --- a/port/android/host_imports.list +++ b/port/android/host_imports.list @@ -43,6 +43,9 @@ host_sdl_scancode_from_name host_sdl_open_audio_stream host_sdl_put_audio_stream_data host_sdl_resume_audio_stream_device +host_sdl_get_audio_stream_data +host_sdl_get_audio_stream_available +host_sdl_destroy_audio_stream host_gl_get_string host_gl_has_extension host_gl_read_buffer diff --git a/port/assets/menus/UNWIRED.md b/port/assets/menus/UNWIRED.md index 3b20f26a..a57a6a1e 100644 --- a/port/assets/menus/UNWIRED.md +++ b/port/assets/menus/UNWIRED.md @@ -21,7 +21,7 @@ do (save a setting, join a game) does not happen; the screens open and close as | `gamespy update filter settings` | 2 | PC function | | `gt edit list update` | 1 | PC function | | `gt select list update` | 1 | PC function | -| `mouse spinner 1wide click` | 72 | PC function | +| `mouse spinner 1wide click` | 76 | PC function | | `mp map list update` | 1 | PC function | | `mp prof init teamplay options` | 1 | PC function | | `mp prof init vehicle options` | 1 | PC function | diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml new file mode 100644 index 00000000..389e9277 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml index 898b5c7e..08713f89 100644 --- a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml @@ -48,6 +48,10 @@ + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml index 55a92dd3..3ad3251d 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml @@ -13,11 +13,22 @@ - - - - - + + + + + + + + + + + + + + + + @@ -25,11 +36,29 @@ + + + + + + + + + + + + + + + + + + - + @@ -38,7 +67,7 @@ - + @@ -47,7 +76,7 @@ - + @@ -56,7 +85,7 @@ - + @@ -65,11 +94,29 @@ - + + + + + + + + + + + + + + + + + + + @@ -81,18 +128,26 @@ + + + + + + + + diff --git a/port/assets/menus/ce/strings.xml b/port/assets/menus/ce/strings.xml index f267aa32..23c77ae9 100644 --- a/port/assets/menus/ce/strings.xml +++ b/port/assets/menus/ce/strings.xml @@ -1023,11 +1023,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -1111,7 +1135,6 @@ - diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json index 6986c10f..1886ef25 100644 --- a/port/assets/menus/menus.json +++ b/port/assets/menus/menus.json @@ -35,6 +35,7 @@ "ce/main_menu.settings_select.multiplayer_setup.playlist_edit.slayer_edit.xml", "ce/main_menu.settings_select.multiplayer_setup.playlist_edit.xml", "ce/main_menu.settings_select.multiplayer_setup.playlist_select.xml", + "ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml", "ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml", "ce/main_menu.settings_select.multiplayer_setup.vehicle_options_edit.xml", "ce/main_menu.settings_select.multiplayer_setup.xml", diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index ea3ef686..6f981c0c 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -249,6 +249,27 @@ for `network.votekick_ban_minutes`. The host sends each client as it changes), for the scoreboard, whose right click frees the mouse to pick a player; the host's own scoreboard adds Kick and Ban. Builds without votes drop both kinds as unknown, so the network version is unchanged. + +Voice chat (`network_voice.c`, the sound in `port/linux/src/voice_audio.c`) +travels as distributed messages too, but in the lobby as well as in a game: +`network_voice_handles_message` takes them before the netcode's checks that +a game is running (`network_distributed_handle_message`, and the host's +stream and datagram paths in `network_server_message_handler.c`). A client +sends its 20 ms Opus frames to the host unreliably +(`_distributed_message_voice_up`), each with the key the host gave it over +its stream (`_distributed_message_voice_config`, with the host's settings, +again every ten seconds and when they change), so a datagram sent as from +another machine is dropped; an empty one every two seconds says it hears +voices. The host checks the key, the frame's size against its quality, a +rate of 50 frames a second (a burst of 10), and at most 8 machines talking, +then sends the frame to each machine that may hear it +(`_distributed_message_voice_down`, with that machine's key) by its +`network.voice_mode`, `network.voice_lobby` and `network.voice_proximity`, +or plays it itself. The host never decodes a frame it only relays, and +never sends a machine its own. A listener sets the gain and pan of a voice +heard for being near by its own view of the speaker's unit. Mutes are each +machine's own. Builds without voice chat never say they hear voices, so the +host sends them none, and drop the kinds as unknown. A speed hack of less than a tenth is let be: the host's bounds on how far and how fast a client's player moves and fires hold it to the host's time anyway. diff --git a/port/linux/README.md b/port/linux/README.md index ae7ddf3d..a25afca9 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -121,6 +121,7 @@ gamepads' only. | zoom | Z, middle mouse button | | show the scores (hold) | tab | | pause menu | escape | +| talk in voice chat (hold) | V | Always: \` opens the developer console, F12 releases or captures the mouse, F11 changes between fullscreen and window. @@ -248,12 +249,15 @@ the setting for one start of the game. It has priority over the file. | `audio.music_volume` | `1.0` | `HALO_MUSIC_VOLUME` | The music's volume, of the master volume. | | `audio.effects_volume` | `1.0` | `HALO_EFFECTS_VOLUME` | The volume of the other sounds (effects and speech), of the master volume. | | `audio.reverb` | `true` | `HALO_REVERB` | `true`: the sounds of the world reverberate as the place the player is in does: the sound environments of the maps (a corridor, a cave, a large hall, outdoors) set the reverberation, as the I3DL2 reverb of the Xbox did. A sound behind a wall or a door is muffled in it too. `false`: no reverberation (sounds behind a wall are still muffled). | +| `audio.voice_chat` | `"push_to_talk"` | `HALO_VOICE_CHAT` | How you talk in voice chat: `"push_to_talk"` (while `controls.push_to_talk` is held; the microphone opens when you first press it), `"open_mic"` (when the microphone hears speech), or `"off"`. You hear the other players in every case. Refer to "Voice chat". | +| `audio.voice_volume` | `1.0` | `HALO_VOICE_VOLUME` | The volume of the voices of the other players, `0` to `2`. | +| `audio.output_device`, `audio.input_device` | `"default"` | `HALO_AUDIO_OUTPUT_DEVICE`, `HALO_AUDIO_INPUT_DEVICE` | The speakers and the microphone, by the name that Settings > Audio shows, or `"default"` for the device of the system. If the device is not found, the game uses the device of the system. Not on Android. | | `audio.loose_sounds` | `false` | `HALO_LOOSE_SOUNDS` | For those who make sounds. `true`: each sound of a map that has a sound tag file of its name in `tags/` in the data root (for example `tags/sound/sfx/weapons/assault rifle/fire.sound`) plays from that file. The files are Halo PC tag files, as the Halo Editing Kit and Invader write them. At the console, `loose_sounds_reload` reads the files again, and `loose_sounds false` plays the sounds of the map again. When a file changes, all sounds stop. | | `input.mouse_sensitivity` | `1.0` | `HALO_MOUSE_SENSITIVITY` | The multiplier for the mouse aim. | | `input.mouse_vertical_sensitivity` | `0.0` | `HALO_MOUSE_VERTICAL_SENSITIVITY` | The multiplier for the vertical mouse aim. `0`: the same as `input.mouse_sensitivity`. | | `input.invert_mouse` | `false` | `HALO_MOUSE_INVERT=1` sets `true` | `true`: the vertical mouse aim is inverted. | | `input.mouse_aim_assist` | `false` | `HALO_MOUSE_AIM_ASSIST` | `true`: the magnetism of the controller also operates for the mouse. `false`: when the mouse moved after the right stick, the view is not slowed or dragged by a target. The autoaim of the bullets operates in both cases. | -| `controls.` | (the table in "Controls") | `HALO_KEY_` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. | +| `controls.` | (the table in "Controls") | `HALO_KEY_` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`, `push_to_talk`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. | | `game.console_log` | `"important"` | `HALO_CONSOLE_LOG` | What the console shows on the screen. `"important"`: bans, players that the host drops for cheating, the reasons that the game refuses a command, and the asserts that stop the game. `"all"`: all the lines. `"none"`: only the asserts that stop the game. The output of a command always shows. `debug.txt` gets all the lines. | | `game.language` | `""` | `HALO_LANGUAGE` | The language of the menus: `ja`, `de`, `fr`, `es` or `it`. Empty: English. | | `paths.data` | `""` | `HALO_DATA_ROOT` | The data root. Refer to "Start the game". | @@ -271,6 +275,10 @@ the setting for one start of the game. It has priority over the file. | `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup writes its choice here. | | `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad), up to 8 times the squad however many players there are. PER PLAYER in co-op's Server Setup writes its choice here. | | `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup writes its choice here. | +| `network.voice_lobby` | `true` | `HALO_NET_VOICE_LOBBY` | When you host: `true`, all players hear all players in the lobby, before and after a game. | +| `network.voice_mode` | `"team_global_enemy_proximity"` | `HALO_NET_VOICE_MODE` | When you host: who hears whom during a game. `"off"`; `"team_proximity"` (teammates who are near); `"team_enemy_proximity"` (all players who are near); `"team_global"` (all teammates); `"team_global_enemy_proximity"` (all teammates, and enemies who are near). Refer to "Voice chat". | +| `network.voice_kbps` | `24` | `HALO_NET_VOICE_KBPS` | When you host: the voice quality in the lobby and in a game, in kilobits per second, `8` to `64`. | +| `network.voice_proximity` | `15.0` | `HALO_NET_VOICE_PROXIMITY` | When you host: the distance in world units (1 unit is approximately 3 metres) at which players are near, for voice chat. `5` to `100`. | | `network.votekick` | `true` | `HALO_NET_VOTEKICK` | When you host: `true`, the players can vote to kick a player. Refer to "Security". `false`: no votes. | | `network.votekick_minutes` | `5` | `HALO_NET_VOTEKICK_MINUTES` | When you host: the minutes that a player must play on the server before the player can start a vote to kick (`0` to `60`). To vote, a player must play for 2 minutes, or for this time if it is less. | | `network.votekick_ban_minutes` | `30` | `HALO_NET_VOTEKICK_BAN_MINUTES` | When you host: the minutes that a player who is kicked by a vote cannot join again (`1` to `1440`). | @@ -289,6 +297,7 @@ the setting for one start of the game. It has priority over the file. | `debug.gpu_skip_vertex_shaders`, `debug.gpu_debug_expression`, `debug.gpu_debug_flat`, `debug.gpu_debug_texture0` | off | `HALO_GPU_SKIP_VS`, `HALO_GPU_DEBUG_EXPR`, `HALO_GPU_DEBUG_FLAT`, `HALO_GPU_DEBUG_T0` | Tools to find problems in the graphics: skip the draws of a vertex shader, or replace the output of all pixel shaders with a GLSL expression (for example `t0.rgb`). | | `debug.network_test`, `debug.network_test_start`, `debug.network_test_kill`, `debug.network_test_score`, `debug.network_test_shoot`, `debug.network_test_vehicle`, `debug.network_test_pickup`, `debug.network_test_pickup_weapon`, `debug.test_input` | off | `HALO_NETWORK_TEST`, `HALO_NETWORK_TEST_START`, `HALO_NETWORK_TEST_KILL`, `HALO_NETWORK_TEST_SCORE`, `HALO_NETWORK_TEST_SHOOT`, `HALO_NETWORK_TEST_VEHICLE`, `HALO_NETWORK_TEST_PICKUP`, `HALO_NETWORK_TEST_PICKUP_WEAPON`, `HALO_TEST_INPUT` | Automatic tests of system link (`game/network_test.c`). Refer to `NETCODE.md`. | | `debug.network_latency`, `debug.network_loss`, `debug.network_corrupt`, `debug.network_corrupt_stream`, `debug.network_corrupt_after` | `0` | `HALO_NETWORK_LATENCY`, `HALO_NETWORK_LOSS`, `HALO_NETWORK_CORRUPT`, `HALO_NETWORK_CORRUPT_STREAM`, `HALO_NETWORK_CORRUPT_AFTER` | The game holds all the data that it receives for this number of milliseconds, ignores this percentage of the datagrams, and damages this percentage of the datagrams it receives, and this percentage of its reads of streams, at random (bytes changed, cut short, stretched or replaced), from this many seconds after the start. Use the first two to test the netcode as on the internet, and the others to test that nothing another machine sends can crash the game (a damaged stream is closed, so a little goes a long way; a host's messages to its own client are damaged too, so start damaging once the game has started). | +| `debug.voice_test` | `false` | `HALO_VOICE_TEST` | Automatic tests of voice chat: a tone replaces the microphone, and each voice that the game hears is written to the log once each second. | | `debug.telnet_console`, `debug.telnet_console_port` | `false`, `2323` | `HALO_TELNET_CONSOLE`, `HALO_TELNET_CONSOLE_PORT` | The game listens on 127.0.0.1, on this port, for a script console (connect with telnet). The console has no password, so only this computer can reach it. | With Mesa drivers, the game sends its GL calls through the GL thread of @@ -459,6 +468,45 @@ When the machines connect, the game of the host shows in Multiplayer, System Link. Join the game as on a local network. System link on a local network does not need an invite. +### Voice chat + +Network games have voice chat, on the local network and on the internet. +Hold V (`controls.push_to_talk`, Settings > Controls Setup) to talk, or set +VOICE CHAT in Settings > Audio to OPEN MIC. VOICE VOLUME sets the volume of +the other players. On Android, set VOICE CHAT to OPEN MIC to talk: the game +then asks for the microphone. + +OUTPUT DEVICE and INPUT DEVICE in Settings > Audio select the speakers and +the microphone (SYSTEM DEFAULT, the default, follows the system). The list +has the devices that the computer had when the game started. + +The host sets voice chat for all players in Server Setup > Teamplay Options +(`network.voice_*` in `config.toml`): + +- In the lobby, before and after a game, all players hear all players + (LOBBY VOICE CHAT). +- During a game (VOICE CHAT): OFF; TEAM NEAR; ANYONE NEAR; TEAM; or TEAM, + ENEMIES NEAR (the default). Near is VOICE NEAR DISTANCE (45 metres is the + default), and both players must be alive. A player who is near sounds + quieter when further, and from their direction. In a game without teams + all players are enemies; in co-op, all are teammates. +- The quality (VOICE QUALITY), 8 to 64 kilobits per second (24 is the + default), in the lobby and in the game. + +In co-op, Server Setup has no Teamplay Options: the host sets voice chat in +`config.toml`. + +A speaker shows next to the name of a player who talks: in the lobby, on +the scoreboard, and in a list at the left of the screen during a game. To +mute a player, open the scoreboard, right-click, click the player, and +click Mute voice. You no longer hear them, and nobody else is told. + +The host sends each voice on to the players who can hear it. The host +accepts a voice only from the machine of the player (with a key that it +gives each machine on its connection), only at the quality it set, at most +50 frames each second from a machine, and from at most 8 players at a time. +The voices are compressed with Opus (`port/third_party/opus`). + ### Server browser Server Setup in Create Game > Internet has a LISTING row: diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 180a42e0..b9c4b75e 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -78,6 +78,8 @@ their handlers open opens. #include "halo_menus.h" #include "custom_edition_cache.h" #include "custom_edition_maps.h" +#include "network_voice.h" +#include "text/draw_string.h" /* (internet play's server browser: the platform layer's) */ #include "../src/p2p.h" @@ -101,6 +103,8 @@ void platform_binding_capture_begin(void); int platform_binding_capture_poll(int *input); void halo_input_name(int input, char *name, unsigned int size); short pc_menu_string_index(long definition_index); +/* cseries_windows.c's */ +unsigned long system_milliseconds(void); /* the game's (port) */ boolean ui_widget_port_dispatch_event(struct widget_instance *widget, short event_type, short controller_index, @@ -1875,6 +1879,7 @@ static struct { "controls.flashlight", L"FLASHLIGHT", 2 }, { "controls.scoreboard", L"SHOW SCORES", 2 }, { "controls.pause", L"PAUSE MENU", 2 }, + { "controls.push_to_talk", L"PUSH TO TALK", 2 }, }; static struct @@ -4222,6 +4227,58 @@ static void lobby_map_show(struct widget_instance *description, char const *map_ widget->parameters.text_box.string_list_index = level; } +/* the lobby's rows' texts as last updated, and their players' machines, +for their speaker icons (menu_functions_text_box_drawn); and when */ +static struct +{ + struct widget_instance *texts[LOBBY_ROWS]; + long machines[LOBBY_ROWS]; + short count; + unsigned long updated_at; +} lobby_icons; + +/* ui_widget.c's: a text box drawn, at the bounds its text was drawn in +(its font and justification still set): a lobby row's player talking in +voice chat (or muted) has a speaker just left of their name +(port/linux/game/network_voice.c) */ +void menu_functions_text_box_drawn(struct widget_instance *widget, rectangle2d const *bounds) +{ + short row; + + /* (as the lobby last updated them: the rows' widgets go with it) */ + if (!lobby_icons.count || system_milliseconds() - lobby_icons.updated_at > 250) + return; + for (row = 0; row < lobby_icons.count; row++) + { + long machine_index = lobby_icons.machines[row]; + rectangle2d icon; + short size; + + if (lobby_icons.texts[row] != widget) + continue; + if (!network_voice_machine_speaking(machine_index) && !network_voice_machine_muted(machine_index)) + return; + size = (short)MIN(14, bounds->y1 - bounds->y0); + icon.x1 = (short)(bounds->x1 - 4); + icon.y0 = (short)((bounds->y0 + bounds->y1 - size) / 2); + /* (the name's ink: the icon just left of it) */ + if (widget->parameters.text_box.text && widget->parameters.text_box.text[0]) + { + rectangle2d text; + rectangle2d cursor; + + draw_unicode_string_compute_bounds(bounds, widget->parameters.text_box.text, &text, &cursor); + if (text.x0 - size - 6 >= bounds->x0) + icon.x1 = (short)(text.x0 - 6); + icon.y0 = (short)((text.y0 + text.y1 - size) / 2); + } + icon.x0 = (short)(icon.x1 - size); + icon.y1 = (short)(icon.y0 + size); + network_voice_draw_icon(&icon, network_voice_machine_muted(machine_index), 1.0f); + return; + } +} + /* "port lobby update" */ static void lobby_update(struct widget_instance *list) { @@ -4242,6 +4299,21 @@ static void lobby_update(struct widget_instance *list) if (multiplayer.lobby_first > MAX(0, lobby_player_count - LOBBY_ROWS)) multiplayer.lobby_first = (short)MAX(0, lobby_player_count - LOBBY_ROWS); rows_update(list, (short)MIN(lobby_player_count, LOBBY_ROWS), lobby_row_text); + /* (the rows' players, for their speaker icons) */ + { + struct widget_instance *row = list->child; + + lobby_icons.count = 0; + for (index = 0; row && index < MIN(lobby_player_count, LOBBY_ROWS); row = row->next, index++) + { + if (strncmp(row->name, "list_item_", 10)) + break; + lobby_icons.texts[index] = named(row, "list_item_text", 0); + lobby_icons.machines[index] = lobby_players[multiplayer.lobby_first + index]->machine_index; + lobby_icons.count = (short)(index + 1); + } + lobby_icons.updated_at = system_milliseconds(); + } lobby_join_help(list); visible_set(named(list, "lobby_button_team", 0), game && game->variant.universal_variant.teams); /* (the buttons' focus, off Switch Team when it is hidden) */ @@ -4476,7 +4548,8 @@ enum _option_health, /* the variant's health, tenths */ _option_short, /* a short of the options */ _option_option_byte, /* a byte of the options */ - _option_radar /* the options' radar players, and the variant's flag */ + _option_radar, /* the options' radar players, and the variant's flag */ + _option_setting /* the host's own (config.toml): Server Setup's copy only */ }; struct gametype_option @@ -4487,6 +4560,9 @@ struct gametype_option unsigned long argument; short count; long values[16]; + /* (_option_setting: the setting, and its values' text) */ + char const *setting; + char const *setting_values[16]; }; #define VARIANT_FIELD(field) (short)offsetof(struct game_variant, field) @@ -4578,11 +4654,38 @@ static struct gametype_option const gametype_options[] = { _friendly_fire_off, _friendly_fire_on, _friendly_fire_shields_only, _friendly_fire_explosives_only } }, { "friendly_fire_penalty_spinner", _option_short, OPTIONS_FIELD(friendly_fire_penalty), 0, 4, { 0, 5, 10, 15 } }, { "autobalance_spinner", _option_option_byte, OPTIONS_FIELD(auto_team_balance), 0, 2, { 0, 1 } }, + /* (the host's voice chat, below them: port/linux/game/network_voice.c; + tools/port_settings.py's VOICE_ROWS) */ + { "voice_mode_spinner", _option_setting, 0, 0, 5, { 0 }, "network.voice_mode", + { "off", "team_proximity", "team_enemy_proximity", "team_global", "team_global_enemy_proximity" } }, + { "voice_lobby_spinner", _option_setting, 0, 0, 2, { 0 }, "network.voice_lobby", { "true", "false" } }, + { "voice_kbps_spinner", _option_setting, 0, 0, 7, { 0 }, "network.voice_kbps", + { "8", "12", "16", "24", "32", "48", "64" } }, + { "voice_proximity_spinner", _option_setting, 0, 0, 6, { 0 }, "network.voice_proximity", + { "5", "10", "15", "20", "30", "50" } }, /* vehicle options (the side's set and counts: vehicles_update) */ { "vehicles_respawn_spinner", _option_short, OPTIONS_FIELD(vehicle_respawn_time), 0, 7, { 0, 30, 60, 90, 120, 180, 300 } }, }; +/* an option of the host's own setting: the value shown for it (the one +set, else the nearest) */ +static short gametype_option_setting_index(struct gametype_option const *option) +{ + struct pc_menu_setting setting; + char text[64]; + short index; + + csmemset(&setting, 0, sizeof(setting)); + setting.setting = option->setting; + setting.value_count = option->count; + for (index = 0; index < option->count; index++) + setting.values[index] = option->setting_values[index]; + if (!setting_text(option->setting, text, sizeof(text), FALSE)) + return 0; + return setting_value_index(&setting, text); +} + /* (an instance's name is its definition's cut to 31 characters: "item_options_infinite_grenades_") */ #define WIDGET_NAME_LENGTH 31 @@ -4688,6 +4791,25 @@ static void gametype_options_each(struct widget_instance *list, boolean save) if (!option) continue; + /* (the host's own settings: Server Setup's alone, their rows + hidden in Edit Gametypes', which edits a gametype for any game; + written on OK, as the gametype's are kept) */ + if (option->kind == _option_setting) + { + short index = (short)PIN(spinner->parameters.list.selected_index, 0, option->count - 1); + + visible_set(row, gametype_edit.setup); + if (!gametype_edit.setup) + continue; + if (!save) + spinner->parameters.list.selected_index = gametype_option_setting_index(option); + else if (index != gametype_option_setting_index(option) && + !setting_write(option->setting, option->setting_values[index])) + { + platform_log("menus: could not set %s", option->setting); + } + continue; + } if (save) { short index = (short)PIN(spinner->parameters.list.selected_index, 0, option->count - 1); diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index d4548bd5..0565835e 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -46,6 +46,8 @@ void platform_log(char const *format, ...); char const *config_string(char const *name); int platform_display_resolutions(long *widths, long *heights, int maximum); int platform_window_sizes(long *widths, long *heights, int maximum); +/* (sdl_platform.c's PLATFORM_AUDIO_DEVICE_NAME_SIZE) */ +int platform_audio_devices(int recording, char (*names)[128], int maximum); /* the game's network server (this machine hosts: network_game_globals.c) */ void *global_network_game_server_get(void); @@ -734,13 +736,50 @@ static long split(char const *text, char const **pieces) /* a spinner's strings, or its values, split: its own, but Video Setup's sizes are the display's (sdl_platform.c), shown "1920 x 1080" and set "1920x1080": its resolutions after Resolution's own (NATIVE), and the -window sizes that fit it in place of Window Size's own */ +window sizes that fit it in place of Window Size's own; and Audio Setup's +devices SDL finds, after their SYSTEM DEFAULT, each set by its name and +shown by it (in capitals, as the menus' words are, and cut short) */ static long spinner_split(struct halo_menu_widget const *widget, boolean values, char const **pieces) { long count = split(values ? widget->values : widget->strings, pieces); long widths[MAXIMUM_STRINGS], heights[MAXIMUM_STRINGS]; long added, index; + if (widget->setting && (!strcmp(widget->setting, "audio.output_device") || + !strcmp(widget->setting, "audio.input_device"))) + { + static char names[MAXIMUM_STRINGS][128]; + + added = platform_audio_devices(!strcmp(widget->setting, "audio.input_device"), names, + (int)(MAXIMUM_STRINGS - count)); + for (index = 0; index < added; index++) + { + char shown[32]; + char const *name = names[index]; + char *piece; + long length; + + if (!values) + { + long at; + + for (at = 0; name[at] && at < (long)sizeof(shown) - 1; at++) + shown[at] = name[at] >= 'a' && name[at] <= 'z' ? (char)(name[at] - 'a' + 'A') : name[at]; + shown[at] = 0; + /* (cut short: what the spinner shows) */ + if (strlen(name) > 18) + strcpy(shown + 15, "..."); + name = shown; + } + length = (long)strlen(name); + piece = allocate(length + 1); + if (!piece) + break; + strcpy(piece, name); + pieces[count++] = piece; + } + return count; + } if (widget->setting && !strcmp(widget->setting, "display.resolution")) added = platform_display_resolutions(widths, heights, (int)(MAXIMUM_STRINGS - count)); else if (widget->setting && !strcmp(widget->setting, "display.window_size")) diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index 983aed36..ce60180b 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -72,6 +72,7 @@ machine (their datum identifiers need not be). #include "units/bipeds.h" #include "network_coop.h" #include "network_distributed.h" +#include "network_voice.h" #include #include @@ -3904,6 +3905,12 @@ void network_distributed_handle_message( short index; word entry_size; + /* (voice chat's, in the lobby too: network_voice.c) */ + if (network_voice_handles_message(message, size)) + { + network_voice_handle_message(machine_index, message, size); + return; + } /* (none between games: loading, or in the menus) */ if (size < sizeof(header) || !game_in_progress()) return; diff --git a/port/linux/game/network_distributed.h b/port/linux/game/network_distributed.h index be3aef10..0ba55191 100644 --- a/port/linux/game/network_distributed.h +++ b/port/linux/game/network_distributed.h @@ -112,6 +112,12 @@ enum /* the vote running as the host counts it, to each client, every second and when it changes (unreliable: network_votekick.c) */ _distributed_message_votekick_status = 79, + /* voice chat (network_voice.c), in the lobby too: a client's frame to + the host, unreliably, with its key; a frame the host relays to a + client; the host's settings and the client's key, reliably */ + _distributed_message_voice_up = 80, + _distributed_message_voice_down = 81, + _distributed_message_voice_config = 82, NUMBER_OF_DISTRIBUTED_MESSAGES }; diff --git a/port/linux/game/network_voice.c b/port/linux/game/network_voice.c new file mode 100644 index 00000000..d3d8f11b --- /dev/null +++ b/port/linux/game/network_voice.c @@ -0,0 +1,1047 @@ +/* +NETWORK_VOICE.C + +Voice chat (port/linux/NETCODE.md): each machine's microphone, encoded with +Opus (port/linux/src/voice_audio.c), goes to the host, which sends it on to +the machines that may hear it. The host decides everything, from its own +config.toml: + +- In the lobby, before and after a game (and in a game's postgame), every + player hears every other (network.voice_lobby). +- In a game, network.voice_mode: off; teammates near; anyone near; all + teammates; or all teammates and enemies near ("near": within + network.voice_proximity world units of each other, both alive). A game + without teams has only enemies, and co-op only teammates. A voice heard + for being near is quieter further away, and comes from the speaker's side + (each listener's own reckoning of where everyone is). +- The quality: network.voice_kbps (8 to 64), in the lobby and in a game, + which the host tells each machine to encode at, and holds it to. + +What the host takes is held to that too: a machine's frames only with the +key the host gave it over its stream (a datagram's source address is all +that names its machine, and anyone can send one as from another's), no +larger than the quality allows, no more than VOICE_FRAMES_PER_SECOND (with +a little burst), only while voice is on, and no more than +VOICE_MAXIMUM_TALKERS machines at once. A machine is sent voices once it +has said it hears them (a frame, or an empty one every two seconds), so a +build without voice chat is never sent any. The host relays only: it never +decodes, and a machine's own voice never comes back to it. + +Each machine decides only for itself: whether it talks (audio.voice_chat: +push to talk, open mic, or not), how loud the others are +(audio.voice_volume), and whom it mutes (the scoreboard's menu, +game_engine.c), which it does not tell anyone. +*/ + +#include "cseries.h" +#include "cseries/errors.h" +#include "memory/data.h" +#include "math/integer_math.h" +#include "camera/observer.h" +#include "game/game.h" +#include "game/game_engine.h" +#include "game/players.h" +#include "main/console.h" +#include "networking/network_game_globals.h" +#include "networking/network_game_manager.h" +#include "networking/network_client_manager.h" +#include "objects/objects.h" +#include "network_distributed.h" +#include "network_voice.h" +#include "../src/voice_audio.h" + +#include + +/* port_config.c's, network_game_globals.c's, network_server_message_handler.c's, +xinput_sdl.c's, posix_net.c's, cseries_windows.c's */ +int config_boolean(const char *name); +long config_integer(const char *name); +double config_real(const char *name); +const char *config_string(const char *name); +unsigned long config_changes(void); +boolean network_distributed_client_send(void *message, word size); +boolean network_distributed_server_send_to_machine(long machine_index, void *message, word size); +boolean network_distributed_server_send_to_machine_reliably(long machine_index, void *message, word size); +short network_distributed_server_machines(long *machine_indices, short maximum); +int halo_push_to_talk_held(void); +/* cinematics.c's */ +void draw_quad(rectangle2d *rectangle, pixel32 color); +void posix_random_bytes(void *buffer, unsigned int size); +unsigned long system_milliseconds(void); + +/* ---------- constants */ + +enum +{ + VOICE_MACHINES = HALO_PORT_MAXIMUM_NETWORK_MACHINES, + /* a frame every 20 ms, and a burst of a few (a client's frames sent + in a clump after a stall) */ + VOICE_FRAMES_PER_SECOND = 50, + VOICE_FRAME_BURST = 10, + /* machines heard at once, at most */ + VOICE_MAXIMUM_TALKERS = 8, + /* a machine talking: one whose frame came this lately (ms) */ + VOICE_TALKING_MS = 500, + /* a machine that hears voices: one heard from this lately (ms) */ + VOICE_LISTENING_MS = 6000, + /* how often a client says it hears voices, and the host repeats its + settings (ms) */ + VOICE_HELLO_MS = 2000, + VOICE_CONFIG_MS = 10000, + /* a client forgets the host's settings unheard this long (ms) */ + VOICE_CONFIG_TIMEOUT_MS = 25000, + /* a speaker's icon shows this long after their last frame (ms) */ + VOICE_SPEAKING_MS = 300, + /* open mic: talking while the microphone's level is over this (about + -40 dB), and for this long after (ms) */ + VOICE_OPEN_MIC_HOLD_MS = 400, + VOICE_MINIMUM_KBPS = 8, + VOICE_MAXIMUM_KBPS = 64, +}; + +#define VOICE_OPEN_MIC_LEVEL 0.01f +/* proximity: full volume this near, quieter further (world units) */ +#define VOICE_NEAR_DISTANCE 3.0f +#define VOICE_FARTHEST_GAIN 0.2f + +/* how the host's settings travel */ +enum +{ + _voice_config_lobby_bit, +}; + +/* network_client_manager.c's client state in game (its enum is its own) */ +enum +{ + _voice_client_state_ingame = 3, +}; + +/* ---------- structures */ + +/* a client's frame to the host (its Opus packet follows; none: it hears +voices, and says so) */ +struct distributed_voice_up +{ + unsigned long key; + word sequence; + byte length; + byte pad; +}; + +/* a frame relayed to a client (the packet follows) */ +struct distributed_voice_down +{ + unsigned long key; + word sequence; + /* the machine that spoke, and its first player (absolute; NO_PLAYER + in the lobby) */ + byte machine_index; + byte player_index; + /* how it is heard (_voice_route_*) */ + byte route; + byte length; + byte pad[2]; +}; + +/* the host's settings, to each client over its stream, with its key */ +struct distributed_voice_config +{ + unsigned long key; + byte flags; + byte mode; + byte kbps; + byte pad0; + word proximity_tenths; + byte pad[2]; +}; + +typedef char distributed_voice_up_size_assert[sizeof(struct distributed_voice_up) == 8 ? 1 : -1]; +typedef char distributed_voice_down_size_assert[sizeof(struct distributed_voice_down) == 12 ? 1 : -1]; +typedef char distributed_voice_config_size_assert[sizeof(struct distributed_voice_config) == 12 ? 1 : -1]; + +/* the host's settings as they are used */ +struct voice_settings +{ + boolean lobby; + short mode; + short kbps; + real proximity; +}; + +/* ---------- globals */ + +/* the host: each machine slot's key, whether it was told it, and when it +was told the settings last; when it was heard from; its frames' allowance +and when it was topped up; when its last frame was relayed */ +static struct +{ + unsigned long key; + boolean told; + unsigned long told_at; + unsigned long heard_at; + boolean heard; + real allowance; + unsigned long allowance_at; + unsigned long talked_at; + boolean talked; +} voice_machines[VOICE_MACHINES]; +static struct voice_settings voice_host_settings; +static unsigned long voice_host_settings_read_at = (unsigned long)-1; +/* (the settings told, to tell again when they change) */ +static struct voice_settings voice_host_settings_told; + +/* this machine: the host's settings and its key (heard: a client), when +they came; the sequence of its frames; whether it talked lately, and the +microphone was asked for */ +static struct voice_settings voice_settings; +static unsigned long voice_key; +static boolean voice_settings_heard; +static unsigned long voice_settings_heard_at; +static word voice_sequence; +static unsigned long voice_hello_at; +static unsigned long voice_talked_at; +static boolean voice_talked; +static unsigned long voice_open_mic_until; +static boolean voice_microphone_wanted; +static boolean voice_session; + +/* every machine slot's last frame heard (for its icon), and whether this +machine mutes it, with the names it had then (a new machine at the slot is +not muted) */ +static unsigned long voice_heard_at[VOICE_MACHINES]; +static boolean voice_heard[VOICE_MACHINES]; +static boolean voice_muted[VOICE_MACHINES]; +static wchar_t voice_muted_names[VOICE_MACHINES][12]; + +/* ---------- the route (pure: tools/harness/tests/voice_route.c) */ + +/* how a listener hears a speaker: in the lobby (and the postgame), anyone +if the lobby has voice; in a game, by the mode, teammates anywhere or near, +enemies near ("near": within the range, both alive) */ +static short voice_route( + short mode, + boolean lobby, + boolean lobby_voice, + boolean teammates, + boolean both_alive, + real distance, + real range) +{ + boolean near = both_alive && distance <= range; + + if (lobby) + return lobby_voice ? _voice_route_global : _voice_route_none; + switch (mode) + { + case _voice_mode_team_proximity: + return teammates && near ? _voice_route_proximity : _voice_route_none; + case _voice_mode_team_enemy_proximity: + return near ? _voice_route_proximity : _voice_route_none; + case _voice_mode_team_global: + return teammates ? _voice_route_global : _voice_route_none; + case _voice_mode_team_global_enemy_proximity: + if (teammates) + return _voice_route_global; + return near ? _voice_route_proximity : _voice_route_none; + default: + return _voice_route_none; + } +} + +/* the largest packet a frame may be at the quality (kilobits a second): +twice its share, for Opus's variable rate, and a little */ +static long voice_packet_limit( + short kbps) +{ + long limit = (long)kbps * 1000 / 8 / VOICE_FRAMES_PER_SECOND * 2 + 16; + + return MIN(limit, VOICE_MAXIMUM_PACKET); +} + +/* ---------- settings */ + +static short voice_mode_from_text( + char const *text) +{ + static char const *const names[NUMBER_OF_VOICE_MODES] = + { + "off", "team_proximity", "team_enemy_proximity", "team_global", "team_global_enemy_proximity", + }; + short mode; + + for (mode = 0; text && mode < NUMBER_OF_VOICE_MODES; mode++) + { + if (!csstrcmp(text, names[mode])) + return mode; + } + return _voice_mode_team_global_enemy_proximity; +} + +/* the host's, from its config.toml (read again as it changes) */ +static void voice_host_settings_read( + void) +{ + if (voice_host_settings_read_at == config_changes()) + return; + voice_host_settings_read_at = config_changes(); + voice_host_settings.lobby = config_boolean("network.voice_lobby") != 0; + voice_host_settings.mode = voice_mode_from_text(config_string("network.voice_mode")); + voice_host_settings.kbps = (short)PIN(config_integer("network.voice_kbps"), VOICE_MINIMUM_KBPS, + VOICE_MAXIMUM_KBPS); + voice_host_settings.proximity = (real)PIN(config_real("network.voice_proximity"), 5.0, 100.0); +} + +static boolean voice_host( + void) +{ + return global_network_game_server_get() != NULL; +} + +/* in the lobby, as voice has it: this machine's network game not in game +(the menus' own game, the main menu's, runs meanwhile), or in a multiplayer +game's postgame (co-op, with no game engine, has none) */ +static boolean voice_in_lobby( + void) +{ + struct network_game_client *client = global_network_game_client_get(); + short state_data; + + if (!client || network_game_client_get_state(client, &state_data) != _voice_client_state_ingame || + !game_in_progress()) + { + return TRUE; + } + return game_engine && game_engine_showing_postgame(); +} + +/* ---------- players */ + +/* the network game this machine is in (the host's, its client's), or NULL */ +static struct network_game *voice_network_game( + void) +{ + struct network_game_client *client = global_network_game_client_get(); + + return client ? network_game_client_get_game(client) : NULL; +} + +/* a machine's first player in the game (its datum), or NULL */ +static struct player_datum *voice_machine_player( + long machine_index, + long *player_index) +{ + struct data_iterator iterator; + struct player_datum *player; + + if (!game_in_progress() || !player_data) + return NULL; + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL) + { + if (!player->quit_out_of_game && player->network_player_data.machine_index == machine_index) + { + if (player_index) + *player_index = iterator.datum_index; + return player; + } + } + return NULL; +} + +/* a machine's first player's name as the network game has it (empty if +none) */ +static void voice_machine_name( + long machine_index, + wchar_t *name) +{ + struct network_game *game = voice_network_game(); + short index; + + csmemset(name, 0, 12 * sizeof(*name)); + for (index = 0; game && index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; index++) + { + struct network_player *player = &game->players[index]; + + if (network_player_is_valid(player) && player->machine_index == machine_index) + { + csmemcpy(name, player->name, sizeof(player->name)); + return; + } + } +} + +/* a player's living unit's position */ +static boolean voice_player_position( + struct player_datum const *player, + real_point3d *position) +{ + long unit_index = player ? distributed_living_unit(player) : NONE; + + if (unit_index == NONE) + return FALSE; + object_get_origin(unit_index, position); + return TRUE; +} + +/* (the host) each machine's first player in the game (NULL: none), found +once for a frame's relaying */ +static void voice_machine_players( + struct player_datum **players) +{ + struct data_iterator iterator; + struct player_datum *player; + + csmemset(players, 0, VOICE_MACHINES * sizeof(*players)); + if (voice_in_lobby() || !player_data) + return; + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL) + { + long machine_index = player->network_player_data.machine_index; + + if (!player->quit_out_of_game && machine_index >= 0 && machine_index < VOICE_MACHINES && + !players[machine_index]) + { + players[machine_index] = player; + } + } +} + +/* (the host) how a listener's machine hears a speaker's, now, by their +first players */ +static short voice_route_between( + struct player_datum *speaker, + struct player_datum *listener) +{ + real_point3d speaker_position; + real_point3d listener_position; + boolean both_alive; + boolean teammates; + real distance = 0.0f; + + if (voice_in_lobby()) + return voice_route(voice_host_settings.mode, TRUE, voice_host_settings.lobby, FALSE, FALSE, 0.0f, 0.0f); + if (!speaker || !listener) + return _voice_route_none; + /* (co-op: one team; a game without teams: none) */ + teammates = !game_engine || (game_engine_has_teams() && speaker->team_index == listener->team_index); + both_alive = voice_player_position(speaker, &speaker_position) && + voice_player_position(listener, &listener_position); + if (both_alive) + distance = distance3d(&speaker_position, &listener_position); + return voice_route(voice_host_settings.mode, FALSE, voice_host_settings.lobby, teammates, both_alive, distance, + voice_host_settings.proximity); +} + +/* a message's headers: the transport's (its size: the connection checks it) +and the distributed netcode's (its time the game's, none in the lobby) */ +static void voice_fill_header( + struct distributed_message_header *header, + byte type, + word size) +{ + header->header = 0; + build_message_header(&header->header, size, 2, 0); + header->type = type; + header->count = 1; + header->game_time = game_in_progress() ? game_time_get() : 0; +} + +/* ---------- the host */ + +static boolean voice_machine_valid( + long machine_index) +{ + return machine_index >= 0 && machine_index < VOICE_MACHINES; +} + +/* (network_server_manager.c) a machine joined at the slot: a new key, told +to it before anything is taken from it */ +void network_voice_machine_joined( + long machine_index) +{ + if (!voice_machine_valid(machine_index)) + return; + csmemset(&voice_machines[machine_index], 0, sizeof(voice_machines[machine_index])); + do + { + posix_random_bytes(&voice_machines[machine_index].key, sizeof(voice_machines[machine_index].key)); + } while (!voice_machines[machine_index].key); +} + +/* the settings and its key, to one client, over its stream */ +static void voice_host_tell( + long machine_index) +{ + struct + { + struct distributed_message_header header; + struct distributed_voice_config config; + } message; + + csmemset(&message, 0, sizeof(message)); + voice_fill_header(&message.header, _distributed_message_voice_config, (word)sizeof(message)); + message.config.key = voice_machines[machine_index].key; + message.config.flags = voice_host_settings.lobby ? FLAG(_voice_config_lobby_bit) : 0; + message.config.mode = (byte)voice_host_settings.mode; + message.config.kbps = (byte)voice_host_settings.kbps; + message.config.proximity_tenths = (word)(voice_host_settings.proximity * 10.0f); + if (network_distributed_server_send_to_machine_reliably(machine_index, &message, (word)sizeof(message))) + { + voice_machines[machine_index].told = TRUE; + voice_machines[machine_index].told_at = system_milliseconds(); + } +} + +/* whether a machine is one this machine plays a voice of: not muted */ +static boolean voice_audible( + long machine_index) +{ + return voice_machine_valid(machine_index) && !voice_muted[machine_index]; +} + +/* the gain and pan of a voice heard for being near (this machine's own +reckoning: its first player's view, and the speaker's unit) */ +static void voice_proximity_gain( + short player_index, + real *gain, + real *pan) +{ + struct player_datum *player = player_index != NONE ? distributed_player(player_index) : NULL; + struct observer_result const *camera; + real_point3d position; + real_vector3d direction; + real_vector3d right; + real distance; + real range = voice_settings.proximity > VOICE_NEAR_DISTANCE ? voice_settings.proximity : VOICE_NEAR_DISTANCE + 1.0f; + + *gain = 1.0f; + *pan = 0.0f; + if (!voice_player_position(player, &position) || local_player_get_next(NONE) == NONE) + return; + camera = observer_get_camera((short)local_player_get_next(NONE)); + if (!camera) + return; + vector_from_points3d(&camera->position, &position, &direction); + distance = magnitude3d(&direction); + if (distance > VOICE_NEAR_DISTANCE) + { + real t = PIN((distance - VOICE_NEAR_DISTANCE) / (range - VOICE_NEAR_DISTANCE), 0.0f, 1.0f); + + *gain = 1.0f - (1.0f - VOICE_FARTHEST_GAIN) * t; + } + if (distance > 0.01f) + { + cross_product3d(&camera->forward, &camera->up, &right); + normalize3d(&right); + scale_vector3d(&direction, 1.0f / distance, &direction); + *pan = 0.8f * dot_product3d(&direction, &right); + } +} + +/* a frame heard here (the host's own listening, or one relayed to a +client); the speaker's first player (absolute index, NONE for none) */ +static void voice_play( + long machine_index, + short player_index, + short route, + word sequence, + byte const *packet, + long length) +{ + real gain = 1.0f; + real pan = 0.0f; + + if (!voice_machine_valid(machine_index) || length <= 0) + return; + voice_heard_at[machine_index] = system_milliseconds(); + voice_heard[machine_index] = TRUE; + if (!voice_audible(machine_index)) + return; + if (route == _voice_route_proximity) + voice_proximity_gain(player_index, &gain, &pan); + voice_audio_play((int)machine_index, sequence, packet, (int)length, gain, pan); +} + +/* how many machines talk now, but the one */ +static short voice_host_talkers( + long except_machine, + unsigned long now) +{ + short count = 0; + long index; + + for (index = 0; index < VOICE_MACHINES; index++) + { + if (index != except_machine && voice_machines[index].talked && + now - voice_machines[index].talked_at < VOICE_TALKING_MS) + { + count++; + } + } + return count; +} + +/* (the host) a frame of a machine (its own, or a client's), checked, and +sent to those that hear it */ +static void voice_host_relay( + long speaker_machine, + word sequence, + byte const *packet, + long length) +{ + static struct player_datum *players[VOICE_MACHINES]; + long machine_indices[VOICE_MACHINES]; + short count = network_distributed_server_machines(machine_indices, VOICE_MACHINES); + short local_machine = network_game_client_get_local_machine_index(); + long speaker_player = NONE; + unsigned long now = system_milliseconds(); + short index; + + if (!voice_machine_valid(speaker_machine) || length <= 0 || + length > voice_packet_limit(voice_host_settings.kbps)) + { + return; + } + /* (no more than so many at once: those talking keep talking) */ + if (!(voice_machines[speaker_machine].talked && now - voice_machines[speaker_machine].talked_at < VOICE_TALKING_MS) && + voice_host_talkers(speaker_machine, now) >= VOICE_MAXIMUM_TALKERS) + { + return; + } + voice_machines[speaker_machine].talked = TRUE; + voice_machines[speaker_machine].talked_at = now; + voice_machine_players(players); + /* (the lobby has no players' positions: the menus' game has players of + its own) */ + if (!voice_in_lobby()) + voice_machine_player(speaker_machine, &speaker_player); + for (index = 0; index < count; index++) + { + long listener = machine_indices[index]; + short route; + + if (!voice_machine_valid(listener) || listener == speaker_machine) + continue; + route = voice_route_between(players[speaker_machine], players[listener]); + if (route == _voice_route_none) + continue; + if (listener == local_machine) + { + voice_play(speaker_machine, speaker_player != NONE ? (short)DATUM_INDEX_TO_ABSOLUTE_INDEX(speaker_player) : + NONE, route, sequence, packet, length); + continue; + } + /* (only to a machine that has said it hears voices) */ + if (!voice_machines[listener].heard || now - voice_machines[listener].heard_at >= VOICE_LISTENING_MS || + !voice_machines[listener].told) + { + continue; + } + { + struct + { + struct distributed_message_header header; + struct distributed_voice_down down; + byte packet[VOICE_MAXIMUM_PACKET]; + } message; + + csmemset(&message, 0, sizeof(message.header) + sizeof(message.down)); + voice_fill_header(&message.header, _distributed_message_voice_down, + (word)(sizeof(message.header) + sizeof(message.down) + length)); + message.down.key = voice_machines[listener].key; + message.down.sequence = sequence; + message.down.machine_index = (byte)speaker_machine; + message.down.player_index = speaker_player != NONE ? + (byte)DATUM_INDEX_TO_ABSOLUTE_INDEX(speaker_player) : NO_PLAYER; + message.down.route = (byte)route; + message.down.length = (byte)length; + csmemcpy(message.packet, packet, length); + network_distributed_server_send_to_machine(listener, &message, + (word)(sizeof(message.header) + sizeof(message.down) + length)); + } + } +} + +/* (the host) a client's frame: its key, its allowance, the settings' */ +static void voice_host_handle_up( + long machine_index, + struct distributed_voice_up const *up, + byte const *packet, + long length) +{ + unsigned long now = system_milliseconds(); + + if (!voice_machine_valid(machine_index) || !voice_machines[machine_index].key || + up->key != voice_machines[machine_index].key || up->length != length) + { + return; + } + voice_machines[machine_index].heard = TRUE; + voice_machines[machine_index].heard_at = now; + /* (an empty frame: it hears voices) */ + if (!length) + return; + /* (its allowance: so many frames a second, a few at once) */ + if (!voice_machines[machine_index].allowance_at) + voice_machines[machine_index].allowance = VOICE_FRAME_BURST; + else + { + voice_machines[machine_index].allowance += (real)(now - voice_machines[machine_index].allowance_at) * + VOICE_FRAMES_PER_SECOND / 1000.0f; + } + voice_machines[machine_index].allowance = MIN(voice_machines[machine_index].allowance, (real)VOICE_FRAME_BURST); + voice_machines[machine_index].allowance_at = now; + if (voice_machines[machine_index].allowance < 1.0f) + return; + voice_machines[machine_index].allowance -= 1.0f; + voice_host_relay(machine_index, up->sequence, packet, length); +} + +/* ---------- messages (network_distributed.c) */ + +boolean network_voice_handles_message( + word const *message, + word size) +{ + struct distributed_message_header header; + + if (size < sizeof(header)) + return FALSE; + csmemcpy(&header, message, sizeof(header)); + return header.type == _distributed_message_voice_up || header.type == _distributed_message_voice_down || + header.type == _distributed_message_voice_config; +} + +void network_voice_handle_message( + long machine_index, + word const *message, + word size) +{ + struct distributed_message_header header; + byte const *entries = (byte const *)message + sizeof(header); + long length = (long)size - (long)sizeof(header); + + if (!network_voice_handles_message(message, size)) + return; + csmemcpy(&header, message, sizeof(header)); + switch (header.type) + { + case _distributed_message_voice_up: + { + struct distributed_voice_up up; + + if (machine_index == NONE || !voice_host() || length < (long)sizeof(up)) + return; + csmemcpy(&up, entries, sizeof(up)); + voice_host_handle_up(machine_index, &up, entries + sizeof(up), length - (long)sizeof(up)); + break; + } + case _distributed_message_voice_down: + { + struct distributed_voice_down down; + + if (machine_index != NONE || voice_host() || !voice_settings_heard || length < (long)sizeof(down)) + return; + csmemcpy(&down, entries, sizeof(down)); + if (down.key != voice_key || down.length != length - (long)sizeof(down) || + down.machine_index == network_game_client_get_local_machine_index()) + { + return; + } + voice_play(down.machine_index, down.player_index != NO_PLAYER && !voice_in_lobby() ? down.player_index : NONE, + down.route, down.sequence, entries + sizeof(down), down.length); + break; + } + case _distributed_message_voice_config: + { + struct distributed_voice_config config; + + if (machine_index != NONE || voice_host() || length < (long)sizeof(config)) + return; + csmemcpy(&config, entries, sizeof(config)); + voice_key = config.key; + voice_settings.lobby = TEST_FLAG(config.flags, _voice_config_lobby_bit); + voice_settings.mode = config.mode < NUMBER_OF_VOICE_MODES ? config.mode : _voice_mode_off; + voice_settings.kbps = (short)PIN(config.kbps, VOICE_MINIMUM_KBPS, VOICE_MAXIMUM_KBPS); + voice_settings.proximity = config.proximity_tenths / 10.0f; + voice_settings_heard = TRUE; + voice_settings_heard_at = system_milliseconds(); + break; + } + } +} + +/* ---------- every frame (main.c) */ + +/* whether this machine may talk now, as the host's settings have it */ +static boolean voice_may_talk( + void) +{ + if (voice_in_lobby()) + return voice_settings.lobby; + return voice_settings.mode != _voice_mode_off; +} + +/* the host: every client told its key and the settings (again as they +change, and now and then) */ +static void voice_host_update( + void) +{ + long machine_indices[VOICE_MACHINES]; + short count = network_distributed_server_machines(machine_indices, VOICE_MACHINES); + short local_machine = network_game_client_get_local_machine_index(); + boolean changed; + unsigned long now = system_milliseconds(); + short index; + + voice_host_settings_read(); + changed = csmemcmp(&voice_host_settings, &voice_host_settings_told, sizeof(voice_host_settings)) != 0; + voice_host_settings_told = voice_host_settings; + /* (the host's own, as a client hears them) */ + voice_settings = voice_host_settings; + voice_settings_heard = TRUE; + voice_settings_heard_at = now; + for (index = 0; index < count; index++) + { + long machine_index = machine_indices[index]; + + if (!voice_machine_valid(machine_index) || machine_index == local_machine) + continue; + if (!voice_machines[machine_index].key) + network_voice_machine_joined(machine_index); + if (!voice_machines[machine_index].told || changed || + now - voice_machines[machine_index].told_at >= VOICE_CONFIG_MS) + { + voice_host_tell(machine_index); + } + } +} + +/* this machine's frame: the host's own relayed, a client's sent up (none: +it hears voices) */ +static void voice_send( + byte const *packet, + long length) +{ + if (voice_host()) + { + if (length > 0) + voice_host_relay(network_game_client_get_local_machine_index(), voice_sequence, packet, length); + } + else + { + struct + { + struct distributed_message_header header; + struct distributed_voice_up up; + byte packet[VOICE_MAXIMUM_PACKET]; + } message; + + csmemset(&message, 0, sizeof(message.header) + sizeof(message.up)); + voice_fill_header(&message.header, _distributed_message_voice_up, + (word)(sizeof(message.header) + sizeof(message.up) + length)); + message.up.key = voice_key; + message.up.sequence = voice_sequence; + message.up.length = (byte)length; + if (length > 0) + csmemcpy(message.packet, packet, length); + network_distributed_client_send(&message, (word)(sizeof(message.header) + sizeof(message.up) + length)); + } + if (length > 0) + voice_sequence++; +} + +/* the session over (or none): the microphone closed, voices forgotten */ +static void voice_end_session( + void) +{ + if (!voice_session) + return; + voice_session = FALSE; + voice_microphone_wanted = FALSE; + voice_audio_microphone(FALSE); + voice_audio_forget_all(); + voice_settings_heard = FALSE; + voice_talked = FALSE; + csmemset(voice_heard, 0, sizeof(voice_heard)); +} + +void network_voice_update( + void) +{ + char const *chat = config_string("audio.voice_chat"); + boolean open_mic = chat && !csstrcmp(chat, "open_mic"); + boolean push_to_talk = chat && !csstrcmp(chat, "push_to_talk"); + boolean held; + unsigned long now = system_milliseconds(); + float frame[VOICE_FRAME_SAMPLES]; + short local_machine = network_game_client_get_local_machine_index(); + long index; + + voice_audio_set_volume((float)PIN(config_real("audio.voice_volume"), 0.0, 2.0)); + /* (a network game: the host's, or one joined) */ + if (!voice_network_game() || local_machine == NONE) + { + voice_end_session(); + return; + } + voice_session = TRUE; + if (voice_host()) + voice_host_update(); + else if (voice_settings_heard && now - voice_settings_heard_at >= VOICE_CONFIG_TIMEOUT_MS) + { + /* (the host repeats them: one that stopped has no voice chat) */ + voice_settings_heard = FALSE; + } + /* (mutes of a machine whose players changed are forgotten) */ + for (index = 0; index < VOICE_MACHINES; index++) + { + wchar_t name[12]; + + if (!voice_muted[index]) + continue; + voice_machine_name(index, name); + if (csmemcmp(name, voice_muted_names[index], sizeof(name))) + voice_muted[index] = FALSE; + } + if (!voice_settings_heard) + { + voice_audio_microphone(FALSE); + voice_microphone_wanted = FALSE; + return; + } + /* (a client says it hears voices) */ + if (!voice_host() && now - voice_hello_at >= VOICE_HELLO_MS) + { + voice_hello_at = now; + voice_send(NULL, 0); + } + /* the microphone: open mic, open while voice is on; push to talk, from + the first press (none asked for until then) */ + held = push_to_talk && halo_push_to_talk_held() && !console_is_active(); + if (!voice_may_talk() || (!open_mic && !push_to_talk)) + voice_microphone_wanted = FALSE; + else if (open_mic || held) + voice_microphone_wanted = TRUE; + voice_audio_microphone(voice_microphone_wanted); + while (voice_microphone_wanted && voice_audio_read_frame(frame)) + { + boolean talk = held; + byte packet[VOICE_MAXIMUM_PACKET]; + long length; + + if (open_mic) + { + if (voice_audio_level(frame) >= VOICE_OPEN_MIC_LEVEL) + voice_open_mic_until = now + VOICE_OPEN_MIC_HOLD_MS; + talk = (long)(voice_open_mic_until - now) > 0; + } + if (!talk || !voice_may_talk()) + continue; + length = voice_audio_encode(frame, voice_settings.kbps * 1000, packet, + (int)MIN(voice_packet_limit(voice_settings.kbps), VOICE_MAXIMUM_PACKET)); + if (length <= 0) + continue; + voice_send(packet, length); + voice_talked = TRUE; + voice_talked_at = now; + } +} + +/* ---------- the menus and the scoreboard (network_voice.h) */ + +boolean network_voice_machine_speaking( + long machine_index) +{ + unsigned long now = system_milliseconds(); + + if (!voice_session || !voice_machine_valid(machine_index)) + return FALSE; + if (machine_index == network_game_client_get_local_machine_index()) + return voice_talked && now - voice_talked_at < VOICE_SPEAKING_MS; + return voice_heard[machine_index] && now - voice_heard_at[machine_index] < VOICE_SPEAKING_MS && + !voice_muted[machine_index]; +} + +boolean network_voice_machine_muted( + long machine_index) +{ + return voice_machine_valid(machine_index) && voice_muted[machine_index]; +} + +void network_voice_mute_machine( + long machine_index, + boolean mute) +{ + if (!voice_machine_valid(machine_index) || machine_index == network_game_client_get_local_machine_index()) + return; + voice_muted[machine_index] = mute; + voice_machine_name(machine_index, voice_muted_names[machine_index]); + if (mute) + voice_audio_forget((int)machine_index); +} + +boolean network_voice_available( + void) +{ + return voice_session && voice_settings_heard; +} + +/* a speaker drawn in the rectangle (its height the icon's size, at its left; +muted: grey, struck through in red), fading with alpha */ +void network_voice_draw_icon( + rectangle2d const *bounds, + boolean muted, + real alpha) +{ + short size = (short)(bounds->y1 - bounds->y0); + short x = bounds->x0; + short y = bounds->y0; + pixel32 a = (pixel32)(PIN(alpha, 0.0f, 1.0f) * 255.0f + 0.5f) << 24; + pixel32 color = a | (muted ? 0x00909090 : 0x0050E050); + rectangle2d part; + short slice; + + if (size < 4) + return; + /* (the body, the cone in slices widening, and two waves) */ + part.x0 = x; + part.x1 = (short)(x + size * 3 / 10); + part.y0 = (short)(y + size * 35 / 100); + part.y1 = (short)(y + size * 65 / 100); + draw_quad(&part, color); + for (slice = 0; slice < 3; slice++) + { + part.x0 = (short)(x + size * (30 + slice * 8) / 100); + part.x1 = (short)(x + size * (38 + slice * 8) / 100); + part.y0 = (short)(y + size * (30 - slice * 10) / 100); + part.y1 = (short)(y + size * (70 + slice * 10) / 100); + draw_quad(&part, color); + } + if (!muted) + { + part.x0 = (short)(x + size * 66 / 100); + part.x1 = (short)(part.x0 + MAX(2, size / 8)); + part.y0 = (short)(y + size * 30 / 100); + part.y1 = (short)(y + size * 70 / 100); + draw_quad(&part, color); + part.x0 = (short)(x + size * 84 / 100); + part.x1 = (short)(part.x0 + MAX(2, size / 8)); + part.y0 = (short)(y + size * 15 / 100); + part.y1 = (short)(y + size * 85 / 100); + draw_quad(&part, color); + return; + } + /* (struck through: a slash of small squares) */ + for (slice = 0; slice < 6; slice++) + { + short step = (short)(size * slice / 6); + + part.x0 = (short)(x + step); + part.x1 = (short)(part.x0 + MAX(2, size / 6)); + part.y0 = (short)(y + step); + part.y1 = (short)(part.y0 + MAX(2, size / 6)); + draw_quad(&part, a | 0x00E03030); + } +} diff --git a/port/linux/game/network_voice.h b/port/linux/game/network_voice.h new file mode 100644 index 00000000..b0a36a45 --- /dev/null +++ b/port/linux/game/network_voice.h @@ -0,0 +1,59 @@ +/* +NETWORK_VOICE.H + +Voice chat (network_voice.c): what the main loop, the netcode, the host's +server, the lobby (menu_functions.c) and the scoreboard (game_engine.c) ask +of it. +*/ + +#ifndef __NETWORK_VOICE_H +#define __NETWORK_VOICE_H +#pragma once + +/* ---------- constants */ + +/* network.voice_mode: who hears whom in a game */ +enum +{ + _voice_mode_off, + _voice_mode_team_proximity, + _voice_mode_team_enemy_proximity, + _voice_mode_team_global, + _voice_mode_team_global_enemy_proximity, + NUMBER_OF_VOICE_MODES +}; + +/* how a listener hears a speaker */ +enum +{ + _voice_route_none, + /* as spoken */ + _voice_route_global, + /* quieter further, from the speaker's side */ + _voice_route_proximity, +}; + +/* ---------- prototypes/NETWORK_VOICE.C */ + +/* every frame (main.c): the microphone, the host's settings told */ +void network_voice_update(void); +/* whether a message of the distributed kind is voice chat's, which it takes +in the lobby too; and its handling (machine_index: the sender's on the +host, NONE on a client) */ +boolean network_voice_handles_message(word const *message, word size); +void network_voice_handle_message(long machine_index, word const *message, word size); +/* (the host's server) a machine joined at the slot: its new key */ +void network_voice_machine_joined(long machine_index); +/* whether this machine has voice chat now (the host's settings heard) */ +boolean network_voice_available(void); +/* whether a machine (its index in the network game) is talking now (this +machine's own, as it sends); muted ones never */ +boolean network_voice_machine_speaking(long machine_index); +/* this machine's mutes of others (which it tells no one) */ +boolean network_voice_machine_muted(long machine_index); +void network_voice_mute_machine(long machine_index, boolean mute); +/* a speaker icon, at the rectangle's left and as tall as it (muted: struck +through), fading with alpha */ +void network_voice_draw_icon(rectangle2d const *bounds, boolean muted, real alpha); + +#endif // __NETWORK_VOICE_H diff --git a/port/linux/include/halo_keyboard.h b/port/linux/include/halo_keyboard.h index e8a66d12..4a3791ed 100644 --- a/port/linux/include/halo_keyboard.h +++ b/port/linux/include/halo_keyboard.h @@ -34,6 +34,9 @@ enum halo_keyboard_action read from it) */ HALO_KEYBOARD_SCOREBOARD, HALO_KEYBOARD_PAUSE, + /* (voice chat's, which the game itself does not read: + halo_push_to_talk_held) */ + HALO_KEYBOARD_PUSH_TO_TALK, NUMBER_OF_HALO_KEYBOARD_ACTIONS }; @@ -41,5 +44,8 @@ enum halo_keyboard_action (a bit for each halo_keyboard_action): theirs is the first's; none while a menu or the console is up */ unsigned long halo_keyboard_actions(short controller_index); +/* whether voice chat's push to talk is held (controls.push_to_talk), in the +game and in the menus alike */ +int halo_push_to_talk_held(void); #endif diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 3c41da36..4c3f9967 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -37,6 +37,7 @@ turns the reverb off; audio.enabled = false skips opening a device #include "platform.h" #include "sdl_platform.h" #include "port_config.h" +#include "voice_audio.h" #include #include @@ -1143,6 +1144,8 @@ static void mix(float *output, unsigned long frames) if ((!enabled && reverb.level <= 0.0f) || reverb.quiet > REVERB_QUIET_FRAMES) reverb_clear(); } + /* the players' voices (voice_audio.c), dry, under the limiter */ + voice_audio_mix(output, frames); limit(output, frames); } @@ -1150,6 +1153,17 @@ static void mix(float *output, unsigned long frames) static SDL_AudioStream *audio_stream; static BOOL audio_started = FALSE; +/* the device it plays on (audio.output_device), and when it was looked at */ +static char audio_device_name[PLATFORM_AUDIO_DEVICE_NAME_SIZE]; +static unsigned long audio_device_read_at = (unsigned long)-1; + +/* audio.output_device ("default": the system's; none on Android) */ +static const char *audio_device_setting(void) +{ + const char *name = config_string("audio.output_device"); + + return name && name[0] ? name : "default"; +} static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) { @@ -1220,7 +1234,12 @@ static void audio_start(void) #else SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, "512"); #endif - audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, NULL); + snprintf(audio_device_name, sizeof(audio_device_name), "%s", audio_device_setting()); + audio_device_read_at = config_changes(); + audio_stream = SDL_OpenAudioDeviceStream(platform_audio_device(FALSE, audio_device_name), &spec, + audio_callback, NULL); + if (!audio_stream && strcmp(audio_device_name, "default")) + audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, NULL); if (audio_stream) { SDL_ResumeAudioStreamDevice(audio_stream); @@ -1236,6 +1255,44 @@ static void audio_start(void) } } +/* (the event thread, each frame: sdl_platform.c) audio.output_device +changed (Settings > Audio): the sound goes on on the new device, else the +system's default */ +void dsound_sdl_output_device_check(void) +{ + SDL_AudioSpec spec; + SDL_AudioStream *stream; + + if (!audio_stream || audio_device_read_at == config_changes()) + return; + audio_device_read_at = config_changes(); + if (!strcmp(audio_device_name, audio_device_setting())) + return; + snprintf(audio_device_name, sizeof(audio_device_name), "%s", audio_device_setting()); + spec.format = SDL_AUDIO_F32; + spec.channels = OUTPUT_CHANNELS; + spec.freq = OUTPUT_RATE; + SDL_DestroyAudioStream(audio_stream); + stream = SDL_OpenAudioDeviceStream(platform_audio_device(FALSE, audio_device_name), &spec, audio_callback, NULL); + if (!stream) + stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, NULL); + audio_stream = stream; + if (audio_stream) + { + SDL_ResumeAudioStreamDevice(audio_stream); + platform_log("audio: playing on %s", audio_device_name); + } + else + { + pthread_t thread; + + /* (none at all: the voices drained in real time, as at the start) */ + platform_log("audio: cannot open an audio device (%s); sound is silent", SDL_GetError()); + pthread_create(&thread, NULL, silent_clock_thread, NULL); + pthread_detach(thread); + } +} + /* ---------- completion */ static void packet_release(struct voice_packet *entry) diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 66be9af5..cc890700 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -167,6 +167,21 @@ static const struct config_setting config_settings[] = "Reverberate the world's sounds as the place the player is in does (the\n" "maps' sound environments, as the Xbox's I3DL2 reverb did); false keeps\n" "them dry." }, + { "audio.voice_chat", _config_string, "\"push_to_talk\"", "HALO_VOICE_CHAT", _environment_value, _platform_all, + "Talking in network games' voice chat: \"push_to_talk\" (while\n" + "controls.push_to_talk is held; the microphone opens the first time),\n" + "\"open_mic\" (whenever the microphone hears speech), or \"off\". Others'\n" + "voices play whatever this is (audio.voice_volume 0 silences them)." }, + { "audio.voice_volume", _config_real, "1.0", "HALO_VOICE_VOLUME", _environment_value, _platform_all, + "The volume of the other players' voices (0 to 2)." }, + { "audio.output_device", _config_string, "\"default\"", "HALO_AUDIO_OUTPUT_DEVICE", _environment_value, + _platform_desktop, + "Where the game's sound and the voices play: a device's name as Settings >\n" + "Audio lists it, or \"default\" for the system's (also if it is not found)." }, + { "audio.input_device", _config_string, "\"default\"", "HALO_AUDIO_INPUT_DEVICE", _environment_value, + _platform_desktop, + "The microphone voice chat listens to: a device's name as Settings >\n" + "Audio lists it, or \"default\" for the system's (also if it is not found)." }, { "audio.loose_sounds", _config_boolean, "false", "HALO_LOOSE_SOUNDS", _environment_value, _platform_all, "For those making sounds: play each of a map's sounds that has a Halo PC\n" "sound tag file of its name under the data root's tags folder\n" @@ -231,6 +246,8 @@ static const struct config_setting config_settings[] = "Showing the scores (the controller's Back)." }, { "controls.pause", _config_string, "\"Escape\"", "HALO_KEY_PAUSE", _environment_value, _platform_all, "The pause menu (the controller's Start)." }, + { "controls.push_to_talk", _config_string, "\"V\"", "HALO_KEY_PUSH_TO_TALK", _environment_value, _platform_all, + "Voice chat: talk while it is held (audio.voice_chat \"push_to_talk\")." }, { "game.console_log", _config_string, "\"important\"", "HALO_CONSOLE_LOG", _environment_value, _platform_all, "What the game's console shows on screen of what it logs: \"important\"\n" @@ -295,6 +312,23 @@ static const struct config_setting config_settings[] = "in everyone's server browser: anyone can see and join it) or, false,\n" "PRIVATE (only players with its invite link can join). Server Setup's\n" "LISTING changes it for each game." }, + { "network.voice_lobby", _config_boolean, "true", "HALO_NET_VOICE_LOBBY", _environment_value, _platform_all, + "Hosting: voice chat in the lobby, before and after a game, where every\n" + "player hears every other." }, + { "network.voice_mode", _config_string, "\"team_global_enemy_proximity\"", "HALO_NET_VOICE_MODE", + _environment_value, _platform_all, + "Hosting: voice chat in a game: \"off\"; \"team_proximity\" (teammates\n" + "near); \"team_enemy_proximity\" (anyone near); \"team_global\" (all\n" + "teammates); or \"team_global_enemy_proximity\" (all teammates, and\n" + "enemies near). A game without teams has only enemies; co-op only\n" + "teammates." }, + { "network.voice_kbps", _config_integer, "24", "HALO_NET_VOICE_KBPS", _environment_value, _platform_all, + "Hosting: the voices' quality, in the lobby and in a game, in kilobits a\n" + "second (8 to 64)." }, + { "network.voice_proximity", _config_real, "15.0", "HALO_NET_VOICE_PROXIMITY", _environment_value, + _platform_all, + "Hosting: how near (world units: 1 is about 3 metres) a player must be\n" + "to be heard by proximity voice chat (5 to 100)." }, { "network.votekick", _config_boolean, "true", "HALO_NET_VOTEKICK", _environment_value, _platform_all, "Hosting: let the players vote to kick a player (the scoreboard's\n" "right-click, or the console's votekick). More than half of the players\n" @@ -424,6 +458,9 @@ static const struct config_setting config_settings[] = "Quit this many seconds after the window opens; 0 never." }, { "debug.hidden_window", _config_boolean, "false", "HALO_HIDDEN_WINDOW", _environment_set_is_true, _platform_desktop, "Keep the window hidden (and never fullscreen)." }, + { "debug.voice_test", _config_boolean, "false", "HALO_VOICE_TEST", _environment_set_is_true, _platform_all, + "Voice chat's automated tests: a tone instead of the microphone, and\n" + "each voice heard logged once a second." }, { "debug.null_renderer", _config_boolean, "false", "HALO_NULL_RENDERER", _environment_set_is_true, _platform_all, "Run without a window, drawing nothing." }, { "debug.gl_debug", _config_boolean, "false", "HALO_GL_DEBUG", _environment_set_is_true, _platform_all, diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 17a68bdb..3bb05134 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -90,6 +90,8 @@ static struct platform_keystroke keystroke_queue[KEYSTROKE_QUEUE_SIZE]; static unsigned long keystroke_head, keystroke_count; #ifndef HALO_ANDROID +/* dsound_sdl.c's: the output device followed */ +void dsound_sdl_output_device_check(void); /* updater.c's: the desktop self-updater */ void updater_start(void); void updater_poll(SDL_Window *window); @@ -675,6 +677,73 @@ int platform_window_sizes(long *widths, long *heights, int maximum) } #endif + +/* ---------- audio devices (Settings > Audio: audio.output_device, +audio.input_device) */ + +#ifndef HALO_ANDROID +int platform_audio_devices(int recording, char (*names)[PLATFORM_AUDIO_DEVICE_NAME_SIZE], int maximum) +{ + SDL_AudioDeviceID *devices; + int device_count = 0, count = 0, index; + + if (maximum < 1 || !platform_sdl_initialize()) + return 0; + devices = recording ? SDL_GetAudioRecordingDevices(&device_count) : SDL_GetAudioPlaybackDevices(&device_count); + for (index = 0; devices && index < device_count && count < maximum; index++) + { + const char *name = SDL_GetAudioDeviceName(devices[index]); + + /* (a name a setting can hold, and a menu show: no "|", which + separates a spinner's values) */ + if (!name || !name[0] || strchr(name, '|') || strlen(name) >= PLATFORM_AUDIO_DEVICE_NAME_SIZE) + continue; + snprintf(names[count++], PLATFORM_AUDIO_DEVICE_NAME_SIZE, "%s", name); + } + SDL_free(devices); + return count; +} + +SDL_AudioDeviceID platform_audio_device(int recording, const char *name) +{ + SDL_AudioDeviceID *devices; + SDL_AudioDeviceID found = recording ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING : SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; + int device_count = 0, index; + + if (!name || !name[0] || !strcmp(name, "default")) + return found; + devices = recording ? SDL_GetAudioRecordingDevices(&device_count) : SDL_GetAudioPlaybackDevices(&device_count); + for (index = 0; devices && index < device_count; index++) + { + const char *device_name = SDL_GetAudioDeviceName(devices[index]); + + if (device_name && !strcmp(device_name, name)) + { + found = devices[index]; + break; + } + } + SDL_free(devices); + if (found == (recording ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING : SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK)) + platform_log("audio: no %s device named \"%s\": the system's default", recording ? "input" : "output", name); + return found; +} +#else +int platform_audio_devices(int recording, char (*names)[PLATFORM_AUDIO_DEVICE_NAME_SIZE], int maximum) +{ + (void)recording; + (void)names; + (void)maximum; + return 0; +} + +SDL_AudioDeviceID platform_audio_device(int recording, const char *name) +{ + (void)name; + return recording ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING : SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; +} +#endif + #ifndef HALO_ANDROID /* the window's size (platform_window_size_setting), as the window was made or last resized: platform_display_apply */ @@ -1259,6 +1328,8 @@ void platform_pump_events(void) platform_show_pending_message(); #ifndef HALO_ANDROID updater_poll(platform_window); + /* (Settings > Audio's output device, as it changes: dsound_sdl.c) */ + dsound_sdl_output_device_check(); #endif pthread_mutex_lock(&input_lock); #ifndef HALO_ANDROID diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index 6e342994..09d8f31e 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -9,6 +9,7 @@ controller emulation (see sdl_platform.c). #define __HALO_LINUX_SDL_PLATFORM_H #include +#include #include #define PLATFORM_MOUSE_BUTTON_COUNT 8 @@ -71,6 +72,14 @@ int platform_display_resolutions(long *widths, long *heights, int maximum); that fit the desktop, and display.window_size's; how many. None on Android. */ int platform_window_sizes(long *widths, long *heights, int maximum); +/* Settings > Audio's devices (port/linux/game/menu_tags.c): the names of +the output (or, recording, input) devices SDL finds; how many. None on +Android, which plays and records with the system's. */ +#define PLATFORM_AUDIO_DEVICE_NAME_SIZE 128 +int platform_audio_devices(int recording, char (*names)[PLATFORM_AUDIO_DEVICE_NAME_SIZE], int maximum); +/* ... the device of a name (audio.output_device, audio.input_device): +the system's default for "default", or one not found */ +SDL_AudioDeviceID platform_audio_device(int recording, const char *name); void platform_video_drawable_size(int *width, int *height); /* the window's mode and size and V-Sync, from config.toml as Settings has just written it (the main thread's) */ diff --git a/port/linux/src/voice_audio.c b/port/linux/src/voice_audio.c new file mode 100644 index 00000000..d9ec2d86 --- /dev/null +++ b/port/linux/src/voice_audio.c @@ -0,0 +1,548 @@ +/* +VOICE_AUDIO.C + +Voice chat's sound (port/linux/game/network_voice.c decides who hears whom): +the microphone, read in frames of 20 ms at 48 kHz, mono, and encoded with +Opus (port/third_party/opus); and the other players' voices, decoded into a +buffer each and mixed into the game's output (dsound_sdl.c's mix, at its +48 kHz, stereo) at their gain and pan. + +The microphone is opened on its own thread (SDL asks Android for the +permission to record there, and waits for the answer), and only while voice +chat may send: network_voice.c closes it otherwise. A speaker plays once +VOICE_PREBUFFER_SAMPLES are buffered (the network's jitter), and waits again +when its buffer runs out; a frame lost in between is made up from the next +one's redundancy (Opus's in-band FEC), or by concealment. +*/ + +#include "platform.h" +#include "sdl_platform.h" +#include "port_config.h" +#include "voice_audio.h" +#include "../../third_party/opus/include/opus.h" + +#include +#include +#include +#include +#include + +#define VOICE_RATE 48000 +/* a speaker's buffer, and how much it holds before it plays */ +#define VOICE_RING_SAMPLES (VOICE_RATE / 2) +#define VOICE_PREBUFFER_SAMPLES (3 * VOICE_FRAME_SAMPLES) +/* how many speakers are decoded at once, and how long one unheard keeps +its decoder (milliseconds) */ +#define VOICE_SLOTS 16 +#define VOICE_SLOT_IDLE_MS 5000 +/* frames made up for when packets are lost, at most; more lost, it starts +over */ +#define VOICE_MAXIMUM_CONCEALED 3 +/* the microphone's frames kept before the oldest are dropped (a game that +stalled must not talk late) */ +#define VOICE_MAXIMUM_BACKLOG (5 * VOICE_FRAME_SAMPLES) +/* how often a microphone that would not open is tried again (ms) */ +#define VOICE_RETRY_MS 10000 + +struct voice_slot +{ + BOOL used; + int speaker; + OpusDecoder *decoder; + /* the sequence next expected; FALSE: none yet */ + BOOL sequenced; + unsigned short next_sequence; + Uint64 heard_ms; + /* the decoded samples, from read for count */ + float ring[VOICE_RING_SAMPLES]; + int read, count; + BOOL playing; + /* the gain and pan wanted, and those mixed last */ + float gain, pan; + float left, right; + /* (debug.voice_test: when it was logged) */ + Uint64 logged_ms; +}; + +static pthread_mutex_t voice_lock = PTHREAD_MUTEX_INITIALIZER; +static struct voice_slot voice_slots[VOICE_SLOTS]; +static float voice_volume = 1.0f; + +/* the microphone (under voice_lock: the opening thread sets it) */ +static SDL_AudioStream *microphone; +/* the device it was opened on (audio.input_device), and the one to open */ +static char microphone_device[PLATFORM_AUDIO_DEVICE_NAME_SIZE]; +static char microphone_opening_device[PLATFORM_AUDIO_DEVICE_NAME_SIZE]; +static BOOL microphone_wanted; +static BOOL microphone_opening; +static Uint64 microphone_failed_ms; +static BOOL microphone_failed; +static float capture[VOICE_FRAME_SAMPLES]; +static int capture_count; +static OpusEncoder *encoder; +static int encoder_bitrate; +/* debug.voice_test (the automated tests): a tone instead of the microphone, +a frame each 20 ms since the first; and each speaker's level logged once a +second */ +static int voice_test = -1; +static Uint64 voice_test_started_ms; +static unsigned long voice_test_frames; +static double voice_test_phase; + +/* ---------- the microphone */ + +static void *microphone_open_thread(void *parameter) +{ + SDL_AudioSpec spec; + SDL_AudioStream *stream; + + (void)parameter; + spec.format = SDL_AUDIO_F32; + spec.channels = 1; + spec.freq = VOICE_RATE; + stream = SDL_OpenAudioDeviceStream(platform_audio_device(TRUE, microphone_opening_device), &spec, NULL, NULL); + if (!stream && strcmp(microphone_opening_device, "default")) + stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, NULL, NULL); + if (stream) + SDL_ResumeAudioStreamDevice(stream); + else + platform_log("voice chat: no microphone (%s)", SDL_GetError()); + pthread_mutex_lock(&voice_lock); + microphone_opening = FALSE; + if (stream && !microphone_wanted) + { + /* (closed again meanwhile) */ + pthread_mutex_unlock(&voice_lock); + SDL_DestroyAudioStream(stream); + return NULL; + } + microphone = stream; + snprintf(microphone_device, sizeof(microphone_device), "%s", microphone_opening_device); + microphone_failed = stream == NULL; + microphone_failed_ms = SDL_GetTicks(); + pthread_mutex_unlock(&voice_lock); + if (stream) + platform_log("voice chat: microphone open"); + return NULL; +} + +static BOOL voice_testing(void) +{ + if (voice_test < 0) + voice_test = config_boolean("debug.voice_test") ? 1 : 0; + return voice_test != 0; +} + +int voice_audio_microphone(int open) +{ + SDL_AudioStream *closing = NULL; + BOOL start = FALSE; + BOOL ready; + + if (voice_testing()) + { + if (open && !voice_test_started_ms) + { + voice_test_started_ms = SDL_GetTicks(); + voice_test_frames = 0; + } + if (!open) + voice_test_started_ms = 0; + return open; + } + if (!platform_sdl_initialize()) + return FALSE; + { + /* (audio.input_device: Settings > Audio's, none on Android) */ + const char *device = config_string("audio.input_device"); + + device = device && device[0] ? device : "default"; + pthread_mutex_lock(&voice_lock); + /* (another device chosen: the one open closed, and the new opened) */ + if (microphone && strcmp(microphone_device, device)) + { + closing = microphone; + microphone = NULL; + microphone_failed = FALSE; + } + if (!microphone_opening) + snprintf(microphone_opening_device, sizeof(microphone_opening_device), "%s", device); + pthread_mutex_unlock(&voice_lock); + if (closing) + { + SDL_DestroyAudioStream(closing); + closing = NULL; + capture_count = 0; + } + } + pthread_mutex_lock(&voice_lock); + microphone_wanted = open; + if (open && !microphone && !microphone_opening && + (!microphone_failed || SDL_GetTicks() - microphone_failed_ms >= VOICE_RETRY_MS)) + { + microphone_opening = TRUE; + start = TRUE; + } + if (!open && microphone) + { + closing = microphone; + microphone = NULL; + microphone_failed = FALSE; + } + ready = microphone != NULL; + pthread_mutex_unlock(&voice_lock); + if (closing) + { + SDL_DestroyAudioStream(closing); + capture_count = 0; + platform_log("voice chat: microphone closed"); + } + if (start) + { + pthread_t thread; + + if (pthread_create(&thread, NULL, microphone_open_thread, NULL) == 0) + pthread_detach(thread); + else + { + pthread_mutex_lock(&voice_lock); + microphone_opening = FALSE; + pthread_mutex_unlock(&voice_lock); + } + } + return ready; +} + +int voice_audio_read_frame(float *frame) +{ + SDL_AudioStream *stream; + + if (voice_testing()) + { + int index; + + if (!voice_test_started_ms || + (SDL_GetTicks() - voice_test_started_ms) / 20 <= voice_test_frames) + { + return FALSE; + } + voice_test_frames++; + for (index = 0; index < VOICE_FRAME_SAMPLES; index++) + { + frame[index] = 0.3f * (float)sin(voice_test_phase); + voice_test_phase += 2.0 * 3.14159265358979 * 440.0 / VOICE_RATE; + } + voice_test_phase = fmod(voice_test_phase, 2.0 * 3.14159265358979); + return TRUE; + } + + pthread_mutex_lock(&voice_lock); + stream = microphone; + pthread_mutex_unlock(&voice_lock); + if (!stream) + return FALSE; + /* (a backlog dropped but for the latest frames) */ + { + int available = SDL_GetAudioStreamAvailable(stream) / (int)sizeof(float); + + while (available > VOICE_MAXIMUM_BACKLOG) + { + float discard[VOICE_FRAME_SAMPLES]; + int take = available - VOICE_MAXIMUM_BACKLOG; + + if (take > VOICE_FRAME_SAMPLES) + take = VOICE_FRAME_SAMPLES; + if (SDL_GetAudioStreamData(stream, discard, take * (int)sizeof(float)) <= 0) + break; + available -= take; + } + } + while (capture_count < VOICE_FRAME_SAMPLES) + { + int got = SDL_GetAudioStreamData(stream, capture + capture_count, + (VOICE_FRAME_SAMPLES - capture_count) * (int)sizeof(float)); + + if (got <= 0) + return FALSE; + capture_count += got / (int)sizeof(float); + } + memcpy(frame, capture, sizeof(capture)); + capture_count = 0; + return TRUE; +} + +float voice_audio_level(const float *frame) +{ + double sum = 0.0; + int index; + + for (index = 0; index < VOICE_FRAME_SAMPLES; index++) + sum += (double)frame[index] * frame[index]; + return (float)sqrt(sum / VOICE_FRAME_SAMPLES); +} + +int voice_audio_encode(const float *frame, int bitrate, unsigned char *packet, int maximum) +{ + int length; + + if (!encoder) + { + int error = 0; + + encoder = opus_encoder_create(VOICE_RATE, 1, OPUS_APPLICATION_VOIP, &error); + if (!encoder) + { + platform_log("voice chat: no Opus encoder (%d)", error); + return 0; + } + /* (a lost frame made up from the next's redundancy; modest + complexity: Android runs the game emulated) */ + opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(1)); + opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(10)); + opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(5)); + opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE)); + encoder_bitrate = 0; + } + if (bitrate != encoder_bitrate) + { + opus_encoder_ctl(encoder, OPUS_SET_BITRATE(bitrate)); + encoder_bitrate = bitrate; + } + length = opus_encode_float(encoder, frame, VOICE_FRAME_SAMPLES, packet, maximum); + return length > 0 ? length : 0; +} + +/* ---------- the voices */ + +/* (under voice_lock) the slot of a speaker, or a free one (or the longest +unheard) taken for it */ +static struct voice_slot *voice_slot_for(int speaker, BOOL create) +{ + Uint64 now = SDL_GetTicks(); + struct voice_slot *oldest = NULL; + int index; + + for (index = 0; index < VOICE_SLOTS; index++) + { + if (voice_slots[index].used && voice_slots[index].speaker == speaker) + return &voice_slots[index]; + } + if (!create) + return NULL; + for (index = 0; index < VOICE_SLOTS; index++) + { + struct voice_slot *slot = &voice_slots[index]; + + if (!slot->used) + { + oldest = slot; + break; + } + if (now - slot->heard_ms >= VOICE_SLOT_IDLE_MS && (!oldest || slot->heard_ms < oldest->heard_ms)) + oldest = slot; + } + if (!oldest) + return NULL; + if (!oldest->decoder) + { + int error = 0; + + oldest->decoder = opus_decoder_create(VOICE_RATE, 1, &error); + if (!oldest->decoder) + return NULL; + } + else + { + opus_decoder_ctl(oldest->decoder, OPUS_RESET_STATE); + } + oldest->used = TRUE; + oldest->speaker = speaker; + oldest->sequenced = FALSE; + oldest->read = oldest->count = 0; + oldest->playing = FALSE; + oldest->left = oldest->right = 0.0f; + return oldest; +} + +/* (under voice_lock) samples into a slot's buffer, the oldest dropped when +it is full */ +static void voice_slot_put(struct voice_slot *slot, const float *samples, int count) +{ + int index; + + for (index = 0; index < count; index++) + { + if (slot->count == VOICE_RING_SAMPLES) + { + slot->read = (slot->read + 1) % VOICE_RING_SAMPLES; + slot->count--; + } + slot->ring[(slot->read + slot->count) % VOICE_RING_SAMPLES] = samples[index]; + slot->count++; + } +} + +void voice_audio_play(int speaker, unsigned short sequence, const unsigned char *packet, int length, float gain, + float pan) +{ + float samples[(VOICE_MAXIMUM_CONCEALED + 1) * VOICE_FRAME_SAMPLES]; + struct voice_slot *slot; + OpusDecoder *decoder; + int lost = 0; + int total = 0; + int decoded; + + if (length <= 0 || length > VOICE_MAXIMUM_PACKET) + return; + pthread_mutex_lock(&voice_lock); + slot = voice_slot_for(speaker, TRUE); + if (!slot) + { + pthread_mutex_unlock(&voice_lock); + return; + } + slot->heard_ms = SDL_GetTicks(); + slot->gain = gain < 0.0f ? 0.0f : gain > 2.0f ? 2.0f : gain; + slot->pan = pan < -1.0f ? -1.0f : pan > 1.0f ? 1.0f : pan; + if (slot->sequenced) + { + unsigned short ahead = (unsigned short)(sequence - slot->next_sequence); + + /* (late, or repeated: dropped) */ + if (ahead >= 0x8000) + { + pthread_mutex_unlock(&voice_lock); + return; + } + /* (a few lost are made up for; more, it starts over) */ + if (ahead <= VOICE_MAXIMUM_CONCEALED) + lost = ahead; + } + slot->sequenced = TRUE; + slot->next_sequence = (unsigned short)(sequence + 1); + decoder = slot->decoder; + pthread_mutex_unlock(&voice_lock); + /* (decoded outside the lock, which the mixer waits on: only this + thread uses a slot's decoder, and only it gives slots to speakers) */ + while (lost > 0) + { + /* (the last lost from this packet's redundancy, those before it + concealed) */ + BOOL redundancy = lost == 1; + + decoded = opus_decode_float(decoder, redundancy ? packet : NULL, redundancy ? length : 0, + samples + total, VOICE_FRAME_SAMPLES, redundancy ? 1 : 0); + if (decoded > 0) + total += decoded; + lost--; + } + decoded = opus_decode_float(decoder, packet, length, samples + total, VOICE_FRAME_SAMPLES, 0); + if (decoded > 0) + total += decoded; + /* (the tests: what each speaker sounds like, once a second) */ + if (voice_testing() && total >= VOICE_FRAME_SAMPLES) + { + Uint64 now = SDL_GetTicks(); + + if (now - slot->logged_ms >= 1000) + { + slot->logged_ms = now; + platform_log("voice test: speaker %d level %.3f gain %.2f pan %.2f bytes %d", speaker, + voice_audio_level(samples + total - VOICE_FRAME_SAMPLES), gain, pan, length); + } + } + pthread_mutex_lock(&voice_lock); + if (slot->used && slot->speaker == speaker) + voice_slot_put(slot, samples, total); + pthread_mutex_unlock(&voice_lock); +} + +void voice_audio_forget(int speaker) +{ + struct voice_slot *slot; + + pthread_mutex_lock(&voice_lock); + slot = voice_slot_for(speaker, FALSE); + if (slot) + { + slot->used = FALSE; + slot->count = 0; + slot->playing = FALSE; + } + pthread_mutex_unlock(&voice_lock); +} + +void voice_audio_forget_all(void) +{ + int index; + + pthread_mutex_lock(&voice_lock); + for (index = 0; index < VOICE_SLOTS; index++) + { + voice_slots[index].used = FALSE; + voice_slots[index].count = 0; + voice_slots[index].playing = FALSE; + } + pthread_mutex_unlock(&voice_lock); +} + +int voice_audio_speaking(int speaker) +{ + struct voice_slot *slot; + BOOL speaking = FALSE; + + pthread_mutex_lock(&voice_lock); + slot = voice_slot_for(speaker, FALSE); + if (slot) + speaking = slot->playing || slot->count >= VOICE_FRAME_SAMPLES; + pthread_mutex_unlock(&voice_lock); + return speaking; +} + +void voice_audio_set_volume(float volume) +{ + voice_volume = volume < 0.0f ? 0.0f : volume > 2.0f ? 2.0f : volume; +} + +void voice_audio_mix(float *output, unsigned long frames) +{ + int index; + + pthread_mutex_lock(&voice_lock); + for (index = 0; index < VOICE_SLOTS; index++) + { + struct voice_slot *slot = &voice_slots[index]; + float angle, left, right, step_left, step_right; + unsigned long frame, count; + + if (!slot->used) + continue; + if (!slot->playing && slot->count >= VOICE_PREBUFFER_SAMPLES) + slot->playing = TRUE; + if (!slot->playing) + continue; + /* (equal power: both sides at the gain when centred) */ + angle = (slot->pan + 1.0f) * 0.25f * 3.14159265f; + left = slot->gain * voice_volume * cosf(angle) * 1.41421356f; + right = slot->gain * voice_volume * sinf(angle) * 1.41421356f; + count = (unsigned long)slot->count < frames ? (unsigned long)slot->count : frames; + /* (the gains glide to their new values over the chunk) */ + step_left = count ? (left - slot->left) / (float)count : 0.0f; + step_right = count ? (right - slot->right) / (float)count : 0.0f; + for (frame = 0; frame < count; frame++) + { + float sample = slot->ring[slot->read]; + + slot->left += step_left; + slot->right += step_right; + output[frame * 2] += sample * slot->left; + output[frame * 2 + 1] += sample * slot->right; + slot->read = (slot->read + 1) % VOICE_RING_SAMPLES; + } + slot->count -= (int)count; + /* (run out: it waits to buffer again) */ + if (!slot->count) + slot->playing = FALSE; + } + pthread_mutex_unlock(&voice_lock); +} diff --git a/port/linux/src/voice_audio.h b/port/linux/src/voice_audio.h new file mode 100644 index 00000000..ecc23328 --- /dev/null +++ b/port/linux/src/voice_audio.h @@ -0,0 +1,42 @@ +/* +VOICE_AUDIO.H + +Voice chat's sound (voice_audio.c): the microphone, Opus, and the other +players' voices mixed into the game's output. Called by +port/linux/game/network_voice.c (the game's side, so its flags are int) and +dsound_sdl.c (the mix). +*/ + +#ifndef VOICE_AUDIO_H +#define VOICE_AUDIO_H + +/* a frame: 20 ms at 48 kHz, mono */ +#define VOICE_FRAME_SAMPLES 960 +/* the longest packet sent or taken (64 kbps is 160 bytes a frame) */ +#define VOICE_MAXIMUM_PACKET 400 + +/* opens the microphone (on another thread: not at once) or closes it; +whether it is open */ +int voice_audio_microphone(int open); +/* the next 20 ms the microphone heard, if it has them all; 1 if so */ +int voice_audio_read_frame(float *frame); +/* a frame's level (its root mean square, 1 at full scale) */ +float voice_audio_level(const float *frame); +/* a frame encoded at the bitrate (bits a second); its packet's length, 0 +for none */ +int voice_audio_encode(const float *frame, int bitrate, unsigned char *packet, int maximum); +/* a speaker's packet (speaker: any number naming them), to play at the +gain (1: as spoken) and pan (-1 left to 1 right) */ +void voice_audio_play(int speaker, unsigned short sequence, const unsigned char *packet, int length, float gain, + float pan); +/* a speaker no longer heard: their buffer and decoder freed; or all */ +void voice_audio_forget(int speaker); +void voice_audio_forget_all(void); +/* whether a speaker's voice is playing now */ +int voice_audio_speaking(int speaker); +void voice_audio_set_volume(float volume); +/* (dsound_sdl.c, on the audio thread) the voices added to the output +(stereo, 48 kHz) */ +void voice_audio_mix(float *output, unsigned long frames); + +#endif diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index a2e87267..2e42ebb7 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -331,7 +331,7 @@ static const char *const binding_settings[NUMBER_OF_HALO_KEYBOARD_ACTIONS] = "controls.move_forward", "controls.move_backward", "controls.strafe_left", "controls.strafe_right", "controls.jump", "controls.crouch", "controls.fire", "controls.throw_grenade", "controls.melee", "controls.reload", "controls.zoom", "controls.switch_weapon", "controls.switch_grenade", "controls.action", - "controls.flashlight", "controls.scoreboard", "controls.pause", + "controls.flashlight", "controls.scoreboard", "controls.pause", "controls.push_to_talk", }; static const struct @@ -488,6 +488,22 @@ unsigned long halo_keyboard_actions(short controller_index) return controller_index == 0 ? keyboard_actions_held : 0; } +int halo_push_to_talk_held(void) +{ + struct platform_input_state input; + int slot; + + /* (the window losing the focus lets every key go: sdl_platform.c) */ + bindings_read(); + platform_input_read(&input, FALSE); + for (slot = 0; slot < MAXIMUM_BINDINGS; slot++) + { + if (input_held(&input, bindings[HALO_KEYBOARD_PUSH_TO_TALK][slot])) + return 1; + } + return 0; +} + /* A scroll of the wheel switches weapons once: it holds Y for WHEEL_PRESS_MS once the wheel has turned a notch, and the scroll lasts until the wheel has been still for WHEEL_SCROLL_GAP_MS. One notch often arrives as several events diff --git a/port/third_party/opus/AUTHORS b/port/third_party/opus/AUTHORS new file mode 100644 index 00000000..b3d22a20 --- /dev/null +++ b/port/third_party/opus/AUTHORS @@ -0,0 +1,6 @@ +Jean-Marc Valin (jmvalin@jmvalin.ca) +Koen Vos (koenvos74@gmail.com) +Timothy Terriberry (tterribe@xiph.org) +Karsten Vandborg Sorensen (karsten.vandborg.sorensen@skype.net) +Soren Skak Jensen (ssjensen@gn.com) +Gregory Maxwell (greg@xiph.org) diff --git a/port/third_party/opus/COPYING b/port/third_party/opus/COPYING new file mode 100644 index 00000000..75711467 --- /dev/null +++ b/port/third_party/opus/COPYING @@ -0,0 +1,44 @@ +Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic, + Jean-Marc Valin, Timothy B. Terriberry, + CSIRO, Gregory Maxwell, Mark Borgerding, + Erik de Castro Lopo, Mozilla, Amazon + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Opus is subject to the royalty-free patent licenses which are +specified at: + +Xiph.Org Foundation: +https://datatracker.ietf.org/ipr/1524/ + +Microsoft Corporation: +https://datatracker.ietf.org/ipr/1914/ + +Broadcom Corporation: +https://datatracker.ietf.org/ipr/1526/ diff --git a/port/third_party/opus/PORT.md b/port/third_party/opus/PORT.md new file mode 100644 index 00000000..81847a9f --- /dev/null +++ b/port/third_party/opus/PORT.md @@ -0,0 +1,11 @@ +# Opus + +[Opus](https://opus-codec.org) 1.5.2 (`opus-1.5.2.tar.gz`, SHA-256 +`65c1d2f78b9f2fb20082c38cbe47c951ad5839345876e46941612ee87f9a7ce1`), for +voice chat (`port/linux/game/network_voice.c`). BSD licence: `COPYING`. + +Only the library is kept: `include/`, `src/`, `celt/` and `silk/` with +`silk/float/` (the floating-point build), without the multistream and +projection API, the demos, the tests and the SIMD and DNN code. `sources.txt` +lists the files the builds compile (`tools/linux_build.py`), as plain C with +`OPUS_BUILD`, `VAR_ARRAYS`, `HAVE_LRINTF` and `HAVE_LRINT`. diff --git a/port/third_party/opus/README b/port/third_party/opus/README new file mode 100644 index 00000000..bcf2376d --- /dev/null +++ b/port/third_party/opus/README @@ -0,0 +1,189 @@ +== Opus audio codec == + +Opus is a codec for interactive speech and audio transmission over the Internet. + + Opus can handle a wide range of interactive audio applications, including +Voice over IP, videoconferencing, in-game chat, and even remote live music +performances. It can scale from low bit-rate narrowband speech to very high +quality stereo music. + + Opus, when coupled with an appropriate container format, is also suitable +for non-realtime stored-file applications such as music distribution, game +soundtracks, portable music players, jukeboxes, and other applications that +have historically used high latency formats such as MP3, AAC, or Vorbis. + + Opus is specified by IETF RFC 6716: + https://tools.ietf.org/html/rfc6716 + + The Opus format and this implementation of it are subject to the royalty- +free patent and copyright licenses specified in the file COPYING. + +This package implements a shared library for encoding and decoding raw Opus +bitstreams. Raw Opus bitstreams should be used over RTP according to + https://tools.ietf.org/html/rfc7587 + +The package also includes a number of test tools used for testing the +correct operation of the library. The bitstreams read/written by these +tools should not be used for Opus file distribution: They include +additional debugging data and cannot support seeking. + +Opus stored in files should use the Ogg encapsulation for Opus which is +described at: + https://tools.ietf.org/html/rfc7845 + +An opus-tools package is available which provides encoding and decoding of +Ogg encapsulated Opus files and includes a number of useful features. + +Opus-tools can be found at: + https://gitlab.xiph.org/xiph/opus-tools.git +or on the main Opus website: + https://opus-codec.org/ + +== Deep Learning and Opus == + +Lossy networks continue to be a challenge for real-time communications. +While the original implementation of Opus provides an excellent packet loss +concealment mechanism, the team has continued to advance the methodology used +to improve audio quality in challenge network environments. + +In Opus 1.5, we added a deep learning based redundancy encoder that enhances +audio in lossy networks by embedding one second of recovery data in the padding +data of each packet. The underlying algorithm behind encoding and decoding the +recovery data is called the deep redundancy (DRED) algorithm. By leveraging +the padding data within the packet, Opus 1.5 is fully backward compatible with +prior revisions of Opus. Please see the README under the "dnn" subdirectory to +understand DRED. + +DRED was developed by a team that Amazon Web Services initially sponsored, +who open-sourced the implementation as well as began the +standardization process at the IETF: + https://datatracker.ietf.org/doc/draft-ietf-mlcodec-opus-extension/ +The license behind Opus or the intellectual property position of Opus does +not change with Opus 1.5. + +== Compiling libopus == + +To build from a distribution tarball, you only need to do the following: + + % ./configure + % make + +To build from the git repository, the following steps are necessary: + +0) Set up a development environment: + +On an Ubuntu or Debian family Linux distribution: + + % sudo apt-get install git autoconf automake libtool gcc make + +On a Fedora/Redhat based Linux: + + % sudo dnf install git autoconf automake libtool gcc make + +Or for older Redhat/Centos Linux releases: + + % sudo yum install git autoconf automake libtool gcc make + +On Apple macOS, install Xcode and brew.sh, then in the Terminal enter: + + % brew install autoconf automake libtool + +1) Clone the repository: + + % git clone https://gitlab.xiph.org/xiph/opus.git + % cd opus + +2) Compiling the source + + % ./autogen.sh + % ./configure + % make + +On x86, it's a good idea to use a -march= option that allows the use of AVX2. + +3) Install the codec libraries (optional) + + % sudo make install + +Once you have compiled the codec, there will be a opus_demo executable +in the top directory. + +Usage: opus_demo [-e] + [options] + opus_demo -d [options] + + +mode: voip | audio | restricted-lowdelay +options: + -e : only runs the encoder (output the bit-stream) + -d : only runs the decoder (reads the bit-stream as input) + -cbr : enable constant bitrate; default: variable bitrate + -cvbr : enable constrained variable bitrate; default: + unconstrained + -bandwidth + : audio bandwidth (from narrowband to fullband); + default: sampling rate + -framesize <2.5|5|10|20|40|60> + : frame size in ms; default: 20 + -max_payload + : maximum payload size in bytes, default: 1024 + -complexity + : complexity, 0 (lowest) ... 10 (highest); default: 10 + -inbandfec : enable SILK inband FEC + -forcemono : force mono encoding, even for stereo input + -dtx : enable SILK DTX + -loss : simulate packet loss, in percent (0-100); default: 0 + +input and output are little-endian signed 16-bit PCM files or opus +bitstreams with simple opus_demo proprietary framing. + +== Testing == + +This package includes a collection of automated unit and system tests +which SHOULD be run after compiling the package especially the first +time it is run on a new platform. + +To run the integrated tests: + + % make check + +There is also collection of standard test vectors which are not +included in this package for size reasons but can be obtained from: +https://opus-codec.org/docs/opus_testvectors-rfc8251.tar.gz + +To run compare the code to these test vectors: + + % curl -OL https://opus-codec.org/docs/opus_testvectors-rfc8251.tar.gz + % tar -zxf opus_testvectors-rfc8251.tar.gz + % ./tests/run_vectors.sh ./ opus_newvectors 48000 + +== Compiling libopus for Windows and alternative build systems == + +See cmake/README.md or meson/README.md. + +== Portability notes == + +This implementation uses floating-point by default but can be compiled to +use only fixed-point arithmetic by setting --enable-fixed-point (if using +autoconf) or by defining the FIXED_POINT macro (if building manually). +The fixed point implementation has somewhat lower audio quality and is +slower on platforms with fast FPUs, it is normally only used in embedded +environments. + +The implementation can be compiled with either a C89 or a C99 compiler. +While it does not rely on any _undefined behavior_ as defined by C89 or +C99, it relies on common _implementation-defined behavior_ for two's +complement architectures: + +o Right shifts of negative values are consistent with two's + complement arithmetic, so that a>>b is equivalent to + floor(a/(2^b)), + +o For conversion to a signed integer of N bits, the value is reduced + modulo 2^N to be within range of the type, + +o The result of integer division of a negative value is truncated + towards zero, and + +o The compiler provides a 64-bit integer type (a C99 requirement + which is supported by most C89 compilers). diff --git a/port/third_party/opus/celt/_kiss_fft_guts.h b/port/third_party/opus/celt/_kiss_fft_guts.h new file mode 100644 index 00000000..17392b3e --- /dev/null +++ b/port/third_party/opus/celt/_kiss_fft_guts.h @@ -0,0 +1,182 @@ +/*Copyright (c) 2003-2004, Mark Borgerding + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE.*/ + +#ifndef KISS_FFT_GUTS_H +#define KISS_FFT_GUTS_H + +#define MIN(a,b) ((a)<(b) ? (a):(b)) +#define MAX(a,b) ((a)>(b) ? (a):(b)) + +/* kiss_fft.h + defines kiss_fft_scalar as either short or a float type + and defines + typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */ +#include "kiss_fft.h" + +/* + Explanation of macros dealing with complex math: + + C_MUL(m,a,b) : m = a*b + C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise + C_SUB( res, a,b) : res = a - b + C_SUBFROM( res , a) : res -= a + C_ADDTO( res , a) : res += a + * */ +#ifdef FIXED_POINT +#include "arch.h" + + +#define SAMP_MAX 2147483647 +#define TWID_MAX 32767 +#define TRIG_UPSCALE 1 + +#define SAMP_MIN -SAMP_MAX + + +# define S_MUL(a,b) MULT16_32_Q15(b, a) + +# define C_MUL(m,a,b) \ + do{ (m).r = SUB32_ovflw(S_MUL((a).r,(b).r) , S_MUL((a).i,(b).i)); \ + (m).i = ADD32_ovflw(S_MUL((a).r,(b).i) , S_MUL((a).i,(b).r)); }while(0) + +# define C_MULC(m,a,b) \ + do{ (m).r = ADD32_ovflw(S_MUL((a).r,(b).r) , S_MUL((a).i,(b).i)); \ + (m).i = SUB32_ovflw(S_MUL((a).i,(b).r) , S_MUL((a).r,(b).i)); }while(0) + +# define C_MULBYSCALAR( c, s ) \ + do{ (c).r = S_MUL( (c).r , s ) ;\ + (c).i = S_MUL( (c).i , s ) ; }while(0) + +# define DIVSCALAR(x,k) \ + (x) = S_MUL( x, (TWID_MAX-((k)>>1))/(k)+1 ) + +# define C_FIXDIV(c,div) \ + do { DIVSCALAR( (c).r , div); \ + DIVSCALAR( (c).i , div); }while (0) + +#define C_ADD( res, a,b)\ + do {(res).r=ADD32_ovflw((a).r,(b).r); (res).i=ADD32_ovflw((a).i,(b).i); \ + }while(0) +#define C_SUB( res, a,b)\ + do {(res).r=SUB32_ovflw((a).r,(b).r); (res).i=SUB32_ovflw((a).i,(b).i); \ + }while(0) +#define C_ADDTO( res , a)\ + do {(res).r = ADD32_ovflw((res).r, (a).r); (res).i = ADD32_ovflw((res).i,(a).i);\ + }while(0) + +#define C_SUBFROM( res , a)\ + do {(res).r = ADD32_ovflw((res).r,(a).r); (res).i = SUB32_ovflw((res).i,(a).i); \ + }while(0) + +#if defined(OPUS_ARM_INLINE_ASM) +#include "arm/kiss_fft_armv4.h" +#endif + +#if defined(OPUS_ARM_INLINE_EDSP) +#include "arm/kiss_fft_armv5e.h" +#endif +#if defined(MIPSr1_ASM) +#include "mips/kiss_fft_mipsr1.h" +#endif + +#else /* not FIXED_POINT*/ + +# define S_MUL(a,b) ( (a)*(b) ) +#define C_MUL(m,a,b) \ + do{ (m).r = (a).r*(b).r - (a).i*(b).i;\ + (m).i = (a).r*(b).i + (a).i*(b).r; }while(0) +#define C_MULC(m,a,b) \ + do{ (m).r = (a).r*(b).r + (a).i*(b).i;\ + (m).i = (a).i*(b).r - (a).r*(b).i; }while(0) + +#define C_MUL4(m,a,b) C_MUL(m,a,b) + +# define C_FIXDIV(c,div) /* NOOP */ +# define C_MULBYSCALAR( c, s ) \ + do{ (c).r *= (s);\ + (c).i *= (s); }while(0) +#endif + +#ifndef CHECK_OVERFLOW_OP +# define CHECK_OVERFLOW_OP(a,op,b) /* noop */ +#endif + +#ifndef C_ADD +#define C_ADD( res, a,b)\ + do { \ + CHECK_OVERFLOW_OP((a).r,+,(b).r)\ + CHECK_OVERFLOW_OP((a).i,+,(b).i)\ + (res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \ + }while(0) +#define C_SUB( res, a,b)\ + do { \ + CHECK_OVERFLOW_OP((a).r,-,(b).r)\ + CHECK_OVERFLOW_OP((a).i,-,(b).i)\ + (res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \ + }while(0) +#define C_ADDTO( res , a)\ + do { \ + CHECK_OVERFLOW_OP((res).r,+,(a).r)\ + CHECK_OVERFLOW_OP((res).i,+,(a).i)\ + (res).r += (a).r; (res).i += (a).i;\ + }while(0) + +#define C_SUBFROM( res , a)\ + do {\ + CHECK_OVERFLOW_OP((res).r,-,(a).r)\ + CHECK_OVERFLOW_OP((res).i,-,(a).i)\ + (res).r -= (a).r; (res).i -= (a).i; \ + }while(0) +#endif /* C_ADD defined */ + +#ifdef FIXED_POINT +/*# define KISS_FFT_COS(phase) TRIG_UPSCALE*floor(MIN(32767,MAX(-32767,.5+32768 * cos (phase)))) +# define KISS_FFT_SIN(phase) TRIG_UPSCALE*floor(MIN(32767,MAX(-32767,.5+32768 * sin (phase))))*/ +# define KISS_FFT_COS(phase) floor(.5+TWID_MAX*cos (phase)) +# define KISS_FFT_SIN(phase) floor(.5+TWID_MAX*sin (phase)) +# define HALF_OF(x) ((x)>>1) +#elif defined(USE_SIMD) +# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) ) +# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) ) +# define HALF_OF(x) ((x)*_mm_set1_ps(.5f)) +#else +# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase) +# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase) +# define HALF_OF(x) ((x)*.5f) +#endif + +#define kf_cexp(x,phase) \ + do{ \ + (x)->r = KISS_FFT_COS(phase);\ + (x)->i = KISS_FFT_SIN(phase);\ + }while(0) + +#define kf_cexp2(x,phase) \ + do{ \ + (x)->r = TRIG_UPSCALE*celt_cos_norm((phase));\ + (x)->i = TRIG_UPSCALE*celt_cos_norm((phase)-32768);\ +}while(0) + +#endif /* KISS_FFT_GUTS_H */ diff --git a/port/third_party/opus/celt/arch.h b/port/third_party/opus/celt/arch.h new file mode 100644 index 00000000..3845c3a0 --- /dev/null +++ b/port/third_party/opus/celt/arch.h @@ -0,0 +1,291 @@ +/* Copyright (c) 2003-2008 Jean-Marc Valin + Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/** + @file arch.h + @brief Various architecture definitions for CELT +*/ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef ARCH_H +#define ARCH_H + +#include "opus_types.h" +#include "opus_defines.h" + +# if !defined(__GNUC_PREREQ) +# if defined(__GNUC__)&&defined(__GNUC_MINOR__) +# define __GNUC_PREREQ(_maj,_min) \ + ((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min)) +# else +# define __GNUC_PREREQ(_maj,_min) 0 +# endif +# endif + +#if OPUS_GNUC_PREREQ(3, 0) +#define opus_likely(x) (__builtin_expect(!!(x), 1)) +#define opus_unlikely(x) (__builtin_expect(!!(x), 0)) +#else +#define opus_likely(x) (!!(x)) +#define opus_unlikely(x) (!!(x)) +#endif + +#define CELT_SIG_SCALE 32768.f + +#define CELT_FATAL(str) celt_fatal(str, __FILE__, __LINE__); + +#if defined(ENABLE_ASSERTIONS) || defined(ENABLE_HARDENING) +#ifdef __GNUC__ +__attribute__((noreturn)) +#endif +void celt_fatal(const char *str, const char *file, int line); + +#if defined(CELT_C) && !defined(OVERRIDE_celt_fatal) +#include +#include +#ifdef __GNUC__ +__attribute__((noreturn)) +#endif +void celt_fatal(const char *str, const char *file, int line) +{ + fprintf (stderr, "Fatal (internal) error in %s, line %d: %s\n", file, line, str); +#if defined(_MSC_VER) + _set_abort_behavior( 0, _WRITE_ABORT_MSG); +#endif + abort(); +} +#endif + +#define celt_assert(cond) {if (!(cond)) {CELT_FATAL("assertion failed: " #cond);}} +#define celt_assert2(cond, message) {if (!(cond)) {CELT_FATAL("assertion failed: " #cond "\n" message);}} +#define MUST_SUCCEED(call) celt_assert((call) == OPUS_OK) +#else +#define celt_assert(cond) +#define celt_assert2(cond, message) +#define MUST_SUCCEED(call) do {if((call) != OPUS_OK) {RESTORE_STACK; return OPUS_INTERNAL_ERROR;} } while (0) +#endif + +#if defined(ENABLE_ASSERTIONS) +#define celt_sig_assert(cond) {if (!(cond)) {CELT_FATAL("signal assertion failed: " #cond);}} +#else +#define celt_sig_assert(cond) +#endif + +#define IMUL32(a,b) ((a)*(b)) + +#define MIN16(a,b) ((a) < (b) ? (a) : (b)) /**< Minimum 16-bit value. */ +#define MAX16(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 16-bit value. */ +#define MIN32(a,b) ((a) < (b) ? (a) : (b)) /**< Minimum 32-bit value. */ +#define MAX32(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 32-bit value. */ +#define IMIN(a,b) ((a) < (b) ? (a) : (b)) /**< Minimum int value. */ +#define IMAX(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum int value. */ +#define UADD32(a,b) ((a)+(b)) +#define USUB32(a,b) ((a)-(b)) + +/* Set this if opus_int64 is a native type of the CPU. */ +/* Assume that all LP64 architectures have fast 64-bit types; also x86_64 + (which can be ILP32 for x32) and Win64 (which is LLP64). */ +#if defined(__x86_64__) || defined(__LP64__) || defined(_WIN64) +#define OPUS_FAST_INT64 1 +#else +#define OPUS_FAST_INT64 0 +#endif + +#define PRINT_MIPS(file) + +#ifdef FIXED_POINT + +typedef opus_int16 opus_val16; +typedef opus_int32 opus_val32; +typedef opus_int64 opus_val64; + +typedef opus_val32 celt_sig; +typedef opus_val16 celt_norm; +typedef opus_val32 celt_ener; + +#define celt_isnan(x) 0 + +#define Q15ONE 32767 + +#define SIG_SHIFT 12 +/* Safe saturation value for 32-bit signals. Should be less than + 2^31*(1-0.85) to avoid blowing up on DC at deemphasis.*/ +#define SIG_SAT (300000000) + +#define NORM_SCALING 16384 + +#define DB_SHIFT 10 + +#define EPSILON 1 +#define VERY_SMALL 0 +#define VERY_LARGE16 ((opus_val16)32767) +#define Q15_ONE ((opus_val16)32767) + +#define SCALEIN(a) (a) +#define SCALEOUT(a) (a) + +#define ABS16(x) ((x) < 0 ? (-(x)) : (x)) +#define ABS32(x) ((x) < 0 ? (-(x)) : (x)) + +static OPUS_INLINE opus_int16 SAT16(opus_int32 x) { + return x > 32767 ? 32767 : x < -32768 ? -32768 : (opus_int16)x; +} + +#ifdef FIXED_DEBUG +#include "fixed_debug.h" +#else + +#include "fixed_generic.h" + +#ifdef OPUS_ARM_PRESUME_AARCH64_NEON_INTR +#include "arm/fixed_arm64.h" +#elif defined (OPUS_ARM_INLINE_EDSP) +#include "arm/fixed_armv5e.h" +#elif defined (OPUS_ARM_INLINE_ASM) +#include "arm/fixed_armv4.h" +#elif defined (BFIN_ASM) +#include "fixed_bfin.h" +#elif defined (TI_C5X_ASM) +#include "fixed_c5x.h" +#elif defined (TI_C6X_ASM) +#include "fixed_c6x.h" +#endif + +#endif + +#else /* FIXED_POINT */ + +typedef float opus_val16; +typedef float opus_val32; +typedef float opus_val64; + +typedef float celt_sig; +typedef float celt_norm; +typedef float celt_ener; + +#ifdef FLOAT_APPROX +/* This code should reliably detect NaN/inf even when -ffast-math is used. + Assumes IEEE 754 format. */ +static OPUS_INLINE int celt_isnan(float x) +{ + union {float f; opus_uint32 i;} in; + in.f = x; + return ((in.i>>23)&0xFF)==0xFF && (in.i&0x007FFFFF)!=0; +} +#else +#ifdef __FAST_MATH__ +#error Cannot build libopus with -ffast-math unless FLOAT_APPROX is defined. This could result in crashes on extreme (e.g. NaN) input +#endif +#define celt_isnan(x) ((x)!=(x)) +#endif + +#define Q15ONE 1.0f + +#define NORM_SCALING 1.f + +#define EPSILON 1e-15f +#define VERY_SMALL 1e-30f +#define VERY_LARGE16 1e15f +#define Q15_ONE ((opus_val16)1.f) + +/* This appears to be the same speed as C99's fabsf() but it's more portable. */ +#define ABS16(x) ((float)fabs(x)) +#define ABS32(x) ((float)fabs(x)) + +#define QCONST16(x,bits) (x) +#define QCONST32(x,bits) (x) + +#define NEG16(x) (-(x)) +#define NEG32(x) (-(x)) +#define NEG32_ovflw(x) (-(x)) +#define EXTRACT16(x) (x) +#define EXTEND32(x) (x) +#define SHR16(a,shift) (a) +#define SHL16(a,shift) (a) +#define SHR32(a,shift) (a) +#define SHL32(a,shift) (a) +#define PSHR32(a,shift) (a) +#define VSHR32(a,shift) (a) + +#define PSHR(a,shift) (a) +#define SHR(a,shift) (a) +#define SHL(a,shift) (a) +#define SATURATE(x,a) (x) +#define SATURATE16(x) (x) + +#define ROUND16(a,shift) (a) +#define SROUND16(a,shift) (a) +#define HALF16(x) (.5f*(x)) +#define HALF32(x) (.5f*(x)) + +#define ADD16(a,b) ((a)+(b)) +#define SUB16(a,b) ((a)-(b)) +#define ADD32(a,b) ((a)+(b)) +#define SUB32(a,b) ((a)-(b)) +#define ADD32_ovflw(a,b) ((a)+(b)) +#define SUB32_ovflw(a,b) ((a)-(b)) +#define MULT16_16_16(a,b) ((a)*(b)) +#define MULT16_16(a,b) ((opus_val32)(a)*(opus_val32)(b)) +#define MAC16_16(c,a,b) ((c)+(opus_val32)(a)*(opus_val32)(b)) + +#define MULT16_32_Q15(a,b) ((a)*(b)) +#define MULT16_32_Q16(a,b) ((a)*(b)) + +#define MULT32_32_Q31(a,b) ((a)*(b)) + +#define MAC16_32_Q15(c,a,b) ((c)+(a)*(b)) +#define MAC16_32_Q16(c,a,b) ((c)+(a)*(b)) + +#define MULT16_16_Q11_32(a,b) ((a)*(b)) +#define MULT16_16_Q11(a,b) ((a)*(b)) +#define MULT16_16_Q13(a,b) ((a)*(b)) +#define MULT16_16_Q14(a,b) ((a)*(b)) +#define MULT16_16_Q15(a,b) ((a)*(b)) +#define MULT16_16_P15(a,b) ((a)*(b)) +#define MULT16_16_P13(a,b) ((a)*(b)) +#define MULT16_16_P14(a,b) ((a)*(b)) +#define MULT16_32_P16(a,b) ((a)*(b)) + +#define DIV32_16(a,b) (((opus_val32)(a))/(opus_val16)(b)) +#define DIV32(a,b) (((opus_val32)(a))/(opus_val32)(b)) + +#define SCALEIN(a) ((a)*CELT_SIG_SCALE) +#define SCALEOUT(a) ((a)*(1/CELT_SIG_SCALE)) + +#define SIG2WORD16(x) (x) + +#endif /* !FIXED_POINT */ + +#ifndef GLOBAL_STACK_SIZE +#ifdef FIXED_POINT +#define GLOBAL_STACK_SIZE 120000 +#else +#define GLOBAL_STACK_SIZE 120000 +#endif +#endif + +#endif /* ARCH_H */ diff --git a/port/third_party/opus/celt/bands.c b/port/third_party/opus/celt/bands.c new file mode 100644 index 00000000..6785e08e --- /dev/null +++ b/port/third_party/opus/celt/bands.c @@ -0,0 +1,1673 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Copyright (c) 2008-2009 Gregory Maxwell + Written by Jean-Marc Valin and Gregory Maxwell */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "bands.h" +#include "modes.h" +#include "vq.h" +#include "cwrs.h" +#include "stack_alloc.h" +#include "os_support.h" +#include "mathops.h" +#include "rate.h" +#include "quant_bands.h" +#include "pitch.h" + +int hysteresis_decision(opus_val16 val, const opus_val16 *thresholds, const opus_val16 *hysteresis, int N, int prev) +{ + int i; + for (i=0;iprev && val < thresholds[prev]+hysteresis[prev]) + i=prev; + if (i thresholds[prev-1]-hysteresis[prev-1]) + i=prev; + return i; +} + +opus_uint32 celt_lcg_rand(opus_uint32 seed) +{ + return 1664525 * seed + 1013904223; +} + +/* This is a cos() approximation designed to be bit-exact on any platform. Bit exactness + with this approximation is important because it has an impact on the bit allocation */ +opus_int16 bitexact_cos(opus_int16 x) +{ + opus_int32 tmp; + opus_int16 x2; + tmp = (4096+((opus_int32)(x)*(x)))>>13; + celt_sig_assert(tmp<=32767); + x2 = tmp; + x2 = (32767-x2) + FRAC_MUL16(x2, (-7651 + FRAC_MUL16(x2, (8277 + FRAC_MUL16(-626, x2))))); + celt_sig_assert(x2<=32766); + return 1+x2; +} + +int bitexact_log2tan(int isin,int icos) +{ + int lc; + int ls; + lc=EC_ILOG(icos); + ls=EC_ILOG(isin); + icos<<=15-lc; + isin<<=15-ls; + return (ls-lc)*(1<<11) + +FRAC_MUL16(isin, FRAC_MUL16(isin, -2597) + 7932) + -FRAC_MUL16(icos, FRAC_MUL16(icos, -2597) + 7932); +} + +#ifdef FIXED_POINT +/* Compute the amplitude (sqrt energy) in each of the bands */ +void compute_band_energies(const CELTMode *m, const celt_sig *X, celt_ener *bandE, int end, int C, int LM, int arch) +{ + int i, c, N; + const opus_int16 *eBands = m->eBands; + (void)arch; + N = m->shortMdctSize< 0) + { + int shift = celt_ilog2(maxval) - 14 + (((m->logN[i]>>BITRES)+LM+1)>>1); + j=eBands[i]<0) + { + do { + sum = MAC16_16(sum, EXTRACT16(SHR32(X[j+c*N],shift)), + EXTRACT16(SHR32(X[j+c*N],shift))); + } while (++jnbEBands] = EPSILON+VSHR32(EXTEND32(celt_sqrt(sum)),-shift); + } else { + bandE[i+c*m->nbEBands] = EPSILON; + } + /*printf ("%f ", bandE[i+c*m->nbEBands]);*/ + } + } while (++ceBands; + N = M*m->shortMdctSize; + c=0; do { + i=0; do { + opus_val16 g; + int j,shift; + opus_val16 E; + shift = celt_zlog2(bandE[i+c*m->nbEBands])-13; + E = VSHR32(bandE[i+c*m->nbEBands], shift); + g = EXTRACT16(celt_rcp(SHL32(E,3))); + j=M*eBands[i]; do { + X[j+c*N] = MULT16_16_Q15(VSHR32(freq[j+c*N],shift-1),g); + } while (++jeBands; + N = m->shortMdctSize<nbEBands] = celt_sqrt(sum); + /*printf ("%f ", bandE[i+c*m->nbEBands]);*/ + } + } while (++ceBands; + N = M*m->shortMdctSize; + c=0; do { + for (i=0;inbEBands]); + for (j=M*eBands[i];jeBands; + N = M*m->shortMdctSize; + bound = M*eBands[end]; + if (downsample!=1) + bound = IMIN(bound, N/downsample); + if (silence) + { + bound = 0; + start = end = 0; + } + f = freq; + x = X+M*eBands[start]; + for (i=0;i>DB_SHIFT); + if (shift>31) + { + shift=0; + g=0; + } else { + /* Handle the fractional part. */ + g = celt_exp2_frac(lg&((1< 16384 we'd be likely to overflow, so we're + capping the gain here, which is equivalent to a cap of 18 on lg. + This shouldn't trigger unless the bitstream is already corrupted. */ + if (shift <= -2) + { + g = 16384; + shift = -2; + } + do { + *f++ = SHL32(MULT16_16(*x++, g), -shift); + } while (++jeBands[i+1]-m->eBands[i]; + /* depth in 1/8 bits */ + celt_sig_assert(pulses[i]>=0); + depth = celt_udiv(1+pulses[i], (m->eBands[i+1]-m->eBands[i]))>>LM; + +#ifdef FIXED_POINT + thresh32 = SHR32(celt_exp2(-SHL16(depth, 10-BITRES)),1); + thresh = MULT16_32_Q15(QCONST16(0.5f, 15), MIN32(32767,thresh32)); + { + opus_val32 t; + t = N0<>1; + t = SHL32(t, (7-shift)<<1); + sqrt_1 = celt_rsqrt_norm(t); + } +#else + thresh = .5f*celt_exp2(-.125f*depth); + sqrt_1 = celt_rsqrt(N0<nbEBands+i]; + prev2 = prev2logE[c*m->nbEBands+i]; + if (C==1) + { + prev1 = MAX16(prev1,prev1logE[m->nbEBands+i]); + prev2 = MAX16(prev2,prev2logE[m->nbEBands+i]); + } + Ediff = EXTEND32(logE[c*m->nbEBands+i])-EXTEND32(MIN16(prev1,prev2)); + Ediff = MAX32(0, Ediff); + +#ifdef FIXED_POINT + if (Ediff < 16384) + { + opus_val32 r32 = SHR32(celt_exp2(-EXTRACT16(Ediff)),1); + r = 2*MIN16(16383,r32); + } else { + r = 0; + } + if (LM==3) + r = MULT16_16_Q14(23170, MIN32(23169, r)); + r = SHR16(MIN16(thresh, r),1); + r = SHR32(MULT16_16_Q15(sqrt_1, r),shift); +#else + /* r needs to be multiplied by 2 or 2*sqrt(2) depending on LM because + short blocks don't have the same energy as long */ + r = 2.f*celt_exp2(-Ediff); + if (LM==3) + r *= 1.41421356f; + r = MIN16(thresh, r); + r = r*sqrt_1; +#endif + X = X_+c*size+(m->eBands[i]<nbEBands]))-13; +#endif + left = VSHR32(bandE[i],shift); + right = VSHR32(bandE[i+m->nbEBands],shift); + norm = EPSILON + celt_sqrt(EPSILON+MULT16_16(left,left)+MULT16_16(right,right)); + a1 = DIV32_16(SHL32(EXTEND32(left),14),norm); + a2 = DIV32_16(SHL32(EXTEND32(right),14),norm); + for (j=0;j>1; + kr = celt_ilog2(Er)>>1; +#endif + t = VSHR32(El, (kl-7)<<1); + lgain = celt_rsqrt_norm(t); + t = VSHR32(Er, (kr-7)<<1); + rgain = celt_rsqrt_norm(t); + +#ifdef FIXED_POINT + if (kl < 7) + kl = 7; + if (kr < 7) + kr = 7; +#endif + + for (j=0;jeBands; + int decision; + int hf_sum=0; + + celt_assert(end>0); + + N0 = M*m->shortMdctSize; + + if (M*(eBands[end]-eBands[end-1]) <= 8) + return SPREAD_NONE; + c=0; do { + for (i=0;im->nbEBands-4) + hf_sum += celt_udiv(32*(tcount[1]+tcount[0]), N); + tmp = (2*tcount[2] >= N) + (2*tcount[1] >= N) + (2*tcount[0] >= N); + sum += tmp*spread_weight[i]; + nbBands+=spread_weight[i]; + } + } while (++cnbEBands+end)); + *hf_average = (*hf_average+hf_sum)>>1; + hf_sum = *hf_average; + if (*tapset_decision==2) + hf_sum += 4; + else if (*tapset_decision==0) + hf_sum -= 4; + if (hf_sum > 22) + *tapset_decision=2; + else if (hf_sum > 18) + *tapset_decision=1; + else + *tapset_decision=0; + } + /*printf("%d %d %d\n", hf_sum, *hf_average, *tapset_decision);*/ + celt_assert(nbBands>0); /* end has to be non-zero */ + celt_assert(sum>=0); + sum = celt_udiv((opus_int32)sum<<8, nbBands); + /* Recursive averaging */ + sum = (sum+*average)>>1; + *average = sum; + /* Hysteresis */ + sum = (3*sum + (((3-last_decision)<<7) + 64) + 2)>>2; + if (sum < 80) + { + decision = SPREAD_AGGRESSIVE; + } else if (sum < 256) + { + decision = SPREAD_NORMAL; + } else if (sum < 384) + { + decision = SPREAD_LIGHT; + } else { + decision = SPREAD_NONE; + } +#ifdef FUZZING + decision = rand()&0x3; + *tapset_decision=rand()%3; +#endif + return decision; +} + +/* Indexing table for converting from natural Hadamard to ordery Hadamard + This is essentially a bit-reversed Gray, on top of which we've added + an inversion of the order because we want the DC at the end rather than + the beginning. The lines are for N=2, 4, 8, 16 */ +static const int ordery_table[] = { + 1, 0, + 3, 0, 2, 1, + 7, 0, 4, 3, 6, 1, 5, 2, + 15, 0, 8, 7, 12, 3, 11, 4, 14, 1, 9, 6, 13, 2, 10, 5, +}; + +static void deinterleave_hadamard(celt_norm *X, int N0, int stride, int hadamard) +{ + int i,j; + VARDECL(celt_norm, tmp); + int N; + SAVE_STACK; + N = N0*stride; + ALLOC(tmp, N, celt_norm); + celt_assert(stride>0); + if (hadamard) + { + const int *ordery = ordery_table+stride-2; + for (i=0;i>= 1; + for (i=0;i>1)) { + qn = 1; + } else { + qn = exp2_table8[qb&0x7]>>(14-(qb>>BITRES)); + qn = (qn+1)>>1<<1; + } + celt_assert(qn <= 256); + return qn; +} + +struct band_ctx { + int encode; + int resynth; + const CELTMode *m; + int i; + int intensity; + int spread; + int tf_change; + ec_ctx *ec; + opus_int32 remaining_bits; + const celt_ener *bandE; + opus_uint32 seed; + int arch; + int theta_round; + int disable_inv; + int avoid_split_noise; +}; + +struct split_ctx { + int inv; + int imid; + int iside; + int delta; + int itheta; + int qalloc; +}; + +static void compute_theta(struct band_ctx *ctx, struct split_ctx *sctx, + celt_norm *X, celt_norm *Y, int N, int *b, int B, int B0, + int LM, + int stereo, int *fill) +{ + int qn; + int itheta=0; + int delta; + int imid, iside; + int qalloc; + int pulse_cap; + int offset; + opus_int32 tell; + int inv=0; + int encode; + const CELTMode *m; + int i; + int intensity; + ec_ctx *ec; + const celt_ener *bandE; + + encode = ctx->encode; + m = ctx->m; + i = ctx->i; + intensity = ctx->intensity; + ec = ctx->ec; + bandE = ctx->bandE; + + /* Decide on the resolution to give to the split parameter theta */ + pulse_cap = m->logN[i]+LM*(1<>1) - (stereo&&N==2 ? QTHETA_OFFSET_TWOPHASE : QTHETA_OFFSET); + qn = compute_qn(N, *b, offset, pulse_cap, stereo); + if (stereo && i>=intensity) + qn = 1; + if (encode) + { + /* theta is the atan() of the ratio between the (normalized) + side and mid. With just that parameter, we can re-scale both + mid and side because we know that 1) they have unit norm and + 2) they are orthogonal. */ + itheta = stereo_itheta(X, Y, stereo, N, ctx->arch); + } + tell = ec_tell_frac(ec); + if (qn!=1) + { + if (encode) + { + if (!stereo || ctx->theta_round == 0) + { + itheta = (itheta*(opus_int32)qn+8192)>>14; + if (!stereo && ctx->avoid_split_noise && itheta > 0 && itheta < qn) + { + /* Check if the selected value of theta will cause the bit allocation + to inject noise on one side. If so, make sure the energy of that side + is zero. */ + int unquantized = celt_udiv((opus_int32)itheta*16384, qn); + imid = bitexact_cos((opus_int16)unquantized); + iside = bitexact_cos((opus_int16)(16384-unquantized)); + delta = FRAC_MUL16((N-1)<<7,bitexact_log2tan(iside,imid)); + if (delta > *b) + itheta = qn; + else if (delta < -*b) + itheta = 0; + } + } else { + int down; + /* Bias quantization towards itheta=0 and itheta=16384. */ + int bias = itheta > 8192 ? 32767/qn : -32767/qn; + down = IMIN(qn-1, IMAX(0, (itheta*(opus_int32)qn + bias)>>14)); + if (ctx->theta_round < 0) + itheta = down; + else + itheta = down+1; + } + } + /* Entropy coding of the angle. We use a uniform pdf for the + time split, a step for stereo, and a triangular one for the rest. */ + if (stereo && N>2) + { + int p0 = 3; + int x = itheta; + int x0 = qn/2; + int ft = p0*(x0+1) + x0; + /* Use a probability of p0 up to itheta=8192 and then use 1 after */ + if (encode) + { + ec_encode(ec,x<=x0?p0*x:(x-1-x0)+(x0+1)*p0,x<=x0?p0*(x+1):(x-x0)+(x0+1)*p0,ft); + } else { + int fs; + fs=ec_decode(ec,ft); + if (fs<(x0+1)*p0) + x=fs/p0; + else + x=x0+1+(fs-(x0+1)*p0); + ec_dec_update(ec,x<=x0?p0*x:(x-1-x0)+(x0+1)*p0,x<=x0?p0*(x+1):(x-x0)+(x0+1)*p0,ft); + itheta = x; + } + } else if (B0>1 || stereo) { + /* Uniform pdf */ + if (encode) + ec_enc_uint(ec, itheta, qn+1); + else + itheta = ec_dec_uint(ec, qn+1); + } else { + int fs=1, ft; + ft = ((qn>>1)+1)*((qn>>1)+1); + if (encode) + { + int fl; + + fs = itheta <= (qn>>1) ? itheta + 1 : qn + 1 - itheta; + fl = itheta <= (qn>>1) ? itheta*(itheta + 1)>>1 : + ft - ((qn + 1 - itheta)*(qn + 2 - itheta)>>1); + + ec_encode(ec, fl, fl+fs, ft); + } else { + /* Triangular pdf */ + int fl=0; + int fm; + fm = ec_decode(ec, ft); + + if (fm < ((qn>>1)*((qn>>1) + 1)>>1)) + { + itheta = (isqrt32(8*(opus_uint32)fm + 1) - 1)>>1; + fs = itheta + 1; + fl = itheta*(itheta + 1)>>1; + } + else + { + itheta = (2*(qn + 1) + - isqrt32(8*(opus_uint32)(ft - fm - 1) + 1))>>1; + fs = qn + 1 - itheta; + fl = ft - ((qn + 1 - itheta)*(qn + 2 - itheta)>>1); + } + + ec_dec_update(ec, fl, fl+fs, ft); + } + } + celt_assert(itheta>=0); + itheta = celt_udiv((opus_int32)itheta*16384, qn); + if (encode && stereo) + { + if (itheta==0) + intensity_stereo(m, X, Y, bandE, i, N); + else + stereo_split(X, Y, N); + } + /* NOTE: Renormalising X and Y *may* help fixed-point a bit at very high rate. + Let's do that at higher complexity */ + } else if (stereo) { + if (encode) + { + inv = itheta > 8192 && !ctx->disable_inv; + if (inv) + { + int j; + for (j=0;j2<remaining_bits > 2<disable_inv) + inv = 0; + itheta = 0; + } + qalloc = ec_tell_frac(ec) - tell; + *b -= qalloc; + + if (itheta == 0) + { + imid = 32767; + iside = 0; + *fill &= (1<inv = inv; + sctx->imid = imid; + sctx->iside = iside; + sctx->delta = delta; + sctx->itheta = itheta; + sctx->qalloc = qalloc; +} +static unsigned quant_band_n1(struct band_ctx *ctx, celt_norm *X, celt_norm *Y, + celt_norm *lowband_out) +{ + int c; + int stereo; + celt_norm *x = X; + int encode; + ec_ctx *ec; + + encode = ctx->encode; + ec = ctx->ec; + + stereo = Y != NULL; + c=0; do { + int sign=0; + if (ctx->remaining_bits>=1<remaining_bits -= 1<resynth) + x[0] = sign ? -NORM_SCALING : NORM_SCALING; + x = Y; + } while (++c<1+stereo); + if (lowband_out) + lowband_out[0] = SHR16(X[0],4); + return 1; +} + +/* This function is responsible for encoding and decoding a mono partition. + It can split the band in two and transmit the energy difference with + the two half-bands. It can be called recursively so bands can end up being + split in 8 parts. */ +static unsigned quant_partition(struct band_ctx *ctx, celt_norm *X, + int N, int b, int B, celt_norm *lowband, + int LM, + opus_val16 gain, int fill) +{ + const unsigned char *cache; + int q; + int curr_bits; + int imid=0, iside=0; + int B0=B; + opus_val16 mid=0, side=0; + unsigned cm=0; + celt_norm *Y=NULL; + int encode; + const CELTMode *m; + int i; + int spread; + ec_ctx *ec; + + encode = ctx->encode; + m = ctx->m; + i = ctx->i; + spread = ctx->spread; + ec = ctx->ec; + + /* If we need 1.5 more bit than we can produce, split the band in two. */ + cache = m->cache.bits + m->cache.index[(LM+1)*m->nbEBands+i]; + if (LM != -1 && b > cache[cache[0]]+12 && N>2) + { + int mbits, sbits, delta; + int itheta; + int qalloc; + struct split_ctx sctx; + celt_norm *next_lowband2=NULL; + opus_int32 rebalance; + + N >>= 1; + Y = X+N; + LM -= 1; + if (B==1) + fill = (fill&1)|(fill<<1); + B = (B+1)>>1; + + compute_theta(ctx, &sctx, X, Y, N, &b, B, B0, LM, 0, &fill); + imid = sctx.imid; + iside = sctx.iside; + delta = sctx.delta; + itheta = sctx.itheta; + qalloc = sctx.qalloc; +#ifdef FIXED_POINT + mid = imid; + side = iside; +#else + mid = (1.f/32768)*imid; + side = (1.f/32768)*iside; +#endif + + /* Give more bits to low-energy MDCTs than they would otherwise deserve */ + if (B0>1 && (itheta&0x3fff)) + { + if (itheta > 8192) + /* Rough approximation for pre-echo masking */ + delta -= delta>>(4-LM); + else + /* Corresponds to a forward-masking slope of 1.5 dB per 10 ms */ + delta = IMIN(0, delta + (N<>(5-LM))); + } + mbits = IMAX(0, IMIN(b, (b-delta)/2)); + sbits = b-mbits; + ctx->remaining_bits -= qalloc; + + if (lowband) + next_lowband2 = lowband+N; /* >32-bit split case */ + + rebalance = ctx->remaining_bits; + if (mbits >= sbits) + { + cm = quant_partition(ctx, X, N, mbits, B, lowband, LM, + MULT16_16_P15(gain,mid), fill); + rebalance = mbits - (rebalance-ctx->remaining_bits); + if (rebalance > 3<>B)<<(B0>>1); + } else { + cm = quant_partition(ctx, Y, N, sbits, B, next_lowband2, LM, + MULT16_16_P15(gain,side), fill>>B)<<(B0>>1); + rebalance = sbits - (rebalance-ctx->remaining_bits); + if (rebalance > 3<remaining_bits -= curr_bits; + + /* Ensures we can never bust the budget */ + while (ctx->remaining_bits < 0 && q > 0) + { + ctx->remaining_bits += curr_bits; + q--; + curr_bits = pulses2bits(m, i, LM, q); + ctx->remaining_bits -= curr_bits; + } + + if (q!=0) + { + int K = get_pulses(q); + + /* Finally do the actual quantization */ + if (encode) + { + cm = alg_quant(X, N, K, spread, B, ec, gain, ctx->resynth, ctx->arch); + } else { + cm = alg_unquant(X, N, K, spread, B, ec, gain); + } + } else { + /* If there's no pulse, fill the band anyway */ + int j; + if (ctx->resynth) + { + unsigned cm_mask; + /* B can be as large as 16, so this shift might overflow an int on a + 16-bit platform; use a long to get defined behavior.*/ + cm_mask = (unsigned)(1UL<seed = celt_lcg_rand(ctx->seed); + X[j] = (celt_norm)((opus_int32)ctx->seed>>20); + } + cm = cm_mask; + } else { + /* Folded spectrum */ + for (j=0;jseed = celt_lcg_rand(ctx->seed); + /* About 48 dB below the "normal" folding level */ + tmp = QCONST16(1.0f/256, 10); + tmp = (ctx->seed)&0x8000 ? tmp : -tmp; + X[j] = lowband[j]+tmp; + } + cm = fill; + } + renormalise_vector(X, N, gain, ctx->arch); + } + } + } + } + + return cm; +} + + +/* This function is responsible for encoding and decoding a band for the mono case. */ +static unsigned quant_band(struct band_ctx *ctx, celt_norm *X, + int N, int b, int B, celt_norm *lowband, + int LM, celt_norm *lowband_out, + opus_val16 gain, celt_norm *lowband_scratch, int fill) +{ + int N0=N; + int N_B=N; + int N_B0; + int B0=B; + int time_divide=0; + int recombine=0; + int longBlocks; + unsigned cm=0; + int k; + int encode; + int tf_change; + + encode = ctx->encode; + tf_change = ctx->tf_change; + + longBlocks = B0==1; + + N_B = celt_udiv(N_B, B); + + /* Special case for one sample */ + if (N==1) + { + return quant_band_n1(ctx, X, NULL, lowband_out); + } + + if (tf_change>0) + recombine = tf_change; + /* Band recombining to increase frequency resolution */ + + if (lowband_scratch && lowband && (recombine || ((N_B&1) == 0 && tf_change<0) || B0>1)) + { + OPUS_COPY(lowband_scratch, lowband, N); + lowband = lowband_scratch; + } + + for (k=0;k>k, 1<>k, 1<>4]<<2; + } + B>>=recombine; + N_B<<=recombine; + + /* Increasing the time resolution */ + while ((N_B&1) == 0 && tf_change<0) + { + if (encode) + haar1(X, N_B, B); + if (lowband) + haar1(lowband, N_B, B); + fill |= fill<>= 1; + time_divide++; + tf_change++; + } + B0=B; + N_B0 = N_B; + + /* Reorganize the samples in time order instead of frequency order */ + if (B0>1) + { + if (encode) + deinterleave_hadamard(X, N_B>>recombine, B0<>recombine, B0<resynth) + { + /* Undo the sample reorganization going from time order to frequency order */ + if (B0>1) + interleave_hadamard(X, N_B>>recombine, B0<>= 1; + N_B <<= 1; + cm |= cm>>B; + haar1(X, N_B, B); + } + + for (k=0;k>k, 1<encode; + ec = ctx->ec; + + /* Special case for one sample */ + if (N==1) + { + return quant_band_n1(ctx, X, Y, lowband_out); + } + + orig_fill = fill; + + compute_theta(ctx, &sctx, X, Y, N, &b, B, B, LM, 1, &fill); + inv = sctx.inv; + imid = sctx.imid; + iside = sctx.iside; + delta = sctx.delta; + itheta = sctx.itheta; + qalloc = sctx.qalloc; +#ifdef FIXED_POINT + mid = imid; + side = iside; +#else + mid = (1.f/32768)*imid; + side = (1.f/32768)*iside; +#endif + + /* This is a special case for N=2 that only works for stereo and takes + advantage of the fact that mid and side are orthogonal to encode + the side with just one bit. */ + if (N==2) + { + int c; + int sign=0; + celt_norm *x2, *y2; + mbits = b; + sbits = 0; + /* Only need one bit for the side. */ + if (itheta != 0 && itheta != 16384) + sbits = 1< 8192; + ctx->remaining_bits -= qalloc+sbits; + + x2 = c ? Y : X; + y2 = c ? X : Y; + if (sbits) + { + if (encode) + { + /* Here we only need to encode a sign for the side. */ + sign = x2[0]*y2[1] - x2[1]*y2[0] < 0; + ec_enc_bits(ec, sign, 1); + } else { + sign = ec_dec_bits(ec, 1); + } + } + sign = 1-2*sign; + /* We use orig_fill here because we want to fold the side, but if + itheta==16384, we'll have cleared the low bits of fill. */ + cm = quant_band(ctx, x2, N, mbits, B, lowband, LM, lowband_out, Q15ONE, + lowband_scratch, orig_fill); + /* We don't split N=2 bands, so cm is either 1 or 0 (for a fold-collapse), + and there's no need to worry about mixing with the other channel. */ + y2[0] = -sign*x2[1]; + y2[1] = sign*x2[0]; + if (ctx->resynth) + { + celt_norm tmp; + X[0] = MULT16_16_Q15(mid, X[0]); + X[1] = MULT16_16_Q15(mid, X[1]); + Y[0] = MULT16_16_Q15(side, Y[0]); + Y[1] = MULT16_16_Q15(side, Y[1]); + tmp = X[0]; + X[0] = SUB16(tmp,Y[0]); + Y[0] = ADD16(tmp,Y[0]); + tmp = X[1]; + X[1] = SUB16(tmp,Y[1]); + Y[1] = ADD16(tmp,Y[1]); + } + } else { + /* "Normal" split code */ + opus_int32 rebalance; + + mbits = IMAX(0, IMIN(b, (b-delta)/2)); + sbits = b-mbits; + ctx->remaining_bits -= qalloc; + + rebalance = ctx->remaining_bits; + if (mbits >= sbits) + { + /* In stereo mode, we do not apply a scaling to the mid because we need the normalized + mid for folding later. */ + cm = quant_band(ctx, X, N, mbits, B, lowband, LM, lowband_out, Q15ONE, + lowband_scratch, fill); + rebalance = mbits - (rebalance-ctx->remaining_bits); + if (rebalance > 3<>B); + } else { + /* For a stereo split, the high bits of fill are always zero, so no + folding will be done to the side. */ + cm = quant_band(ctx, Y, N, sbits, B, NULL, LM, NULL, side, NULL, fill>>B); + rebalance = sbits - (rebalance-ctx->remaining_bits); + if (rebalance > 3<resynth) + { + if (N!=2) + stereo_merge(X, Y, mid, N, ctx->arch); + if (inv) + { + int j; + for (j=0;jeBands; + n1 = M*(eBands[start+1]-eBands[start]); + n2 = M*(eBands[start+2]-eBands[start+1]); + /* Duplicate enough of the first band folding data to be able to fold the second band. + Copies no data for CELT-only mode. */ + OPUS_COPY(&norm[n1], &norm[2*n1 - n2], n2-n1); + if (dual_stereo) + OPUS_COPY(&norm2[n1], &norm2[2*n1 - n2], n2-n1); +} +#endif + +void quant_all_bands(int encode, const CELTMode *m, int start, int end, + celt_norm *X_, celt_norm *Y_, unsigned char *collapse_masks, + const celt_ener *bandE, int *pulses, int shortBlocks, int spread, + int dual_stereo, int intensity, int *tf_res, opus_int32 total_bits, + opus_int32 balance, ec_ctx *ec, int LM, int codedBands, + opus_uint32 *seed, int complexity, int arch, int disable_inv) +{ + int i; + opus_int32 remaining_bits; + const opus_int16 * OPUS_RESTRICT eBands = m->eBands; + celt_norm * OPUS_RESTRICT norm, * OPUS_RESTRICT norm2; + VARDECL(celt_norm, _norm); + VARDECL(celt_norm, _lowband_scratch); + VARDECL(celt_norm, X_save); + VARDECL(celt_norm, Y_save); + VARDECL(celt_norm, X_save2); + VARDECL(celt_norm, Y_save2); + VARDECL(celt_norm, norm_save2); + int resynth_alloc; + celt_norm *lowband_scratch; + int B; + int M; + int lowband_offset; + int update_lowband = 1; + int C = Y_ != NULL ? 2 : 1; + int norm_offset; + int theta_rdo = encode && Y_!=NULL && !dual_stereo && complexity>=8; +#ifdef RESYNTH + int resynth = 1; +#else + int resynth = !encode || theta_rdo; +#endif + struct band_ctx ctx; + SAVE_STACK; + + M = 1<nbEBands-1]-norm_offset), celt_norm); + norm = _norm; + norm2 = norm + M*eBands[m->nbEBands-1]-norm_offset; + + /* For decoding, we can use the last band as scratch space because we don't need that + scratch space for the last band and we don't care about the data there until we're + decoding the last band. */ + if (encode && resynth) + resynth_alloc = M*(eBands[m->nbEBands]-eBands[m->nbEBands-1]); + else + resynth_alloc = ALLOC_NONE; + ALLOC(_lowband_scratch, resynth_alloc, celt_norm); + if (encode && resynth) + lowband_scratch = _lowband_scratch; + else + lowband_scratch = X_+M*eBands[m->effEBands-1]; + ALLOC(X_save, resynth_alloc, celt_norm); + ALLOC(Y_save, resynth_alloc, celt_norm); + ALLOC(X_save2, resynth_alloc, celt_norm); + ALLOC(Y_save2, resynth_alloc, celt_norm); + ALLOC(norm_save2, resynth_alloc, celt_norm); + + lowband_offset = 0; + ctx.bandE = bandE; + ctx.ec = ec; + ctx.encode = encode; + ctx.intensity = intensity; + ctx.m = m; + ctx.seed = *seed; + ctx.spread = spread; + ctx.arch = arch; + ctx.disable_inv = disable_inv; + ctx.resynth = resynth; + ctx.theta_round = 0; + /* Avoid injecting noise in the first band on transients. */ + ctx.avoid_split_noise = B > 1; + for (i=start;i 0); + tell = ec_tell_frac(ec); + + /* Compute how many bits we want to allocate to this band */ + if (i != start) + balance -= tell; + remaining_bits = total_bits-tell-1; + ctx.remaining_bits = remaining_bits; + if (i <= codedBands-1) + { + curr_balance = celt_sudiv(balance, IMIN(3, codedBands-i)); + b = IMAX(0, IMIN(16383, IMIN(remaining_bits+1,pulses[i]+curr_balance))); + } else { + b = 0; + } + +#ifndef DISABLE_UPDATE_DRAFT + if (resynth && (M*eBands[i]-N >= M*eBands[start] || i==start+1) && (update_lowband || lowband_offset==0)) + lowband_offset = i; + if (i == start+1) + special_hybrid_folding(m, norm, norm2, start, M, dual_stereo); +#else + if (resynth && M*eBands[i]-N >= M*eBands[start] && (update_lowband || lowband_offset==0)) + lowband_offset = i; +#endif + + tf_change = tf_res[i]; + ctx.tf_change = tf_change; + if (i>=m->effEBands) + { + X=norm; + if (Y_!=NULL) + Y = norm; + lowband_scratch = NULL; + } + if (last && !theta_rdo) + lowband_scratch = NULL; + + /* Get a conservative estimate of the collapse_mask's for the bands we're + going to be folding from. */ + if (lowband_offset != 0 && (spread!=SPREAD_AGGRESSIVE || B>1 || tf_change<0)) + { + int fold_start; + int fold_end; + int fold_i; + /* This ensures we never repeat spectral content within one band */ + effective_lowband = IMAX(0, M*eBands[lowband_offset]-norm_offset-N); + fold_start = lowband_offset; + while(M*eBands[--fold_start] > effective_lowband+norm_offset); + fold_end = lowband_offset-1; +#ifndef DISABLE_UPDATE_DRAFT + while(++fold_end < i && M*eBands[fold_end] < effective_lowband+norm_offset+N); +#else + while(M*eBands[++fold_end] < effective_lowband+norm_offset+N); +#endif + x_cm = y_cm = 0; + fold_i = fold_start; do { + x_cm |= collapse_masks[fold_i*C+0]; + y_cm |= collapse_masks[fold_i*C+C-1]; + } while (++fold_inbEBands], w); + /* Make a copy. */ + cm = x_cm|y_cm; + ec_save = *ec; + ctx_save = ctx; + OPUS_COPY(X_save, X, N); + OPUS_COPY(Y_save, Y, N); + /* Encode and round down. */ + ctx.theta_round = -1; + x_cm = quant_band_stereo(&ctx, X, Y, N, b, B, + effective_lowband != -1 ? norm+effective_lowband : NULL, LM, + last?NULL:norm+M*eBands[i]-norm_offset, lowband_scratch, cm); + dist0 = MULT16_32_Q15(w[0], celt_inner_prod(X_save, X, N, arch)) + MULT16_32_Q15(w[1], celt_inner_prod(Y_save, Y, N, arch)); + + /* Save first result. */ + cm2 = x_cm; + ec_save2 = *ec; + ctx_save2 = ctx; + OPUS_COPY(X_save2, X, N); + OPUS_COPY(Y_save2, Y, N); + if (!last) + OPUS_COPY(norm_save2, norm+M*eBands[i]-norm_offset, N); + nstart_bytes = ec_save.offs; + nend_bytes = ec_save.storage; + bytes_buf = ec_save.buf+nstart_bytes; + save_bytes = nend_bytes-nstart_bytes; + OPUS_COPY(bytes_save, bytes_buf, save_bytes); + + /* Restore */ + *ec = ec_save; + ctx = ctx_save; + OPUS_COPY(X, X_save, N); + OPUS_COPY(Y, Y_save, N); +#ifndef DISABLE_UPDATE_DRAFT + if (i == start+1) + special_hybrid_folding(m, norm, norm2, start, M, dual_stereo); +#endif + /* Encode and round up. */ + ctx.theta_round = 1; + x_cm = quant_band_stereo(&ctx, X, Y, N, b, B, + effective_lowband != -1 ? norm+effective_lowband : NULL, LM, + last?NULL:norm+M*eBands[i]-norm_offset, lowband_scratch, cm); + dist1 = MULT16_32_Q15(w[0], celt_inner_prod(X_save, X, N, arch)) + MULT16_32_Q15(w[1], celt_inner_prod(Y_save, Y, N, arch)); + if (dist0 >= dist1) { + x_cm = cm2; + *ec = ec_save2; + ctx = ctx_save2; + OPUS_COPY(X, X_save2, N); + OPUS_COPY(Y, Y_save2, N); + if (!last) + OPUS_COPY(norm+M*eBands[i]-norm_offset, norm_save2, N); + OPUS_COPY(bytes_buf, bytes_save, save_bytes); + } + } else { + ctx.theta_round = 0; + x_cm = quant_band_stereo(&ctx, X, Y, N, b, B, + effective_lowband != -1 ? norm+effective_lowband : NULL, LM, + last?NULL:norm+M*eBands[i]-norm_offset, lowband_scratch, x_cm|y_cm); + } + } else { + x_cm = quant_band(&ctx, X, N, b, B, + effective_lowband != -1 ? norm+effective_lowband : NULL, LM, + last?NULL:norm+M*eBands[i]-norm_offset, Q15ONE, lowband_scratch, x_cm|y_cm); + } + y_cm = x_cm; + } + collapse_masks[i*C+0] = (unsigned char)x_cm; + collapse_masks[i*C+C-1] = (unsigned char)y_cm; + balance += pulses[i] + tell; + + /* Update the folding position only as long as we have 1 bit/sample depth. */ + update_lowband = b>(N< +#include "celt.h" +#include "pitch.h" +#include "bands.h" +#include "modes.h" +#include "entcode.h" +#include "quant_bands.h" +#include "rate.h" +#include "stack_alloc.h" +#include "mathops.h" +#include "float_cast.h" +#include +#include "celt_lpc.h" +#include "vq.h" + +#ifndef PACKAGE_VERSION +#define PACKAGE_VERSION "unknown" +#endif + +#if defined(MIPSr1_ASM) +#include "mips/celt_mipsr1.h" +#endif + + +int resampling_factor(opus_int32 rate) +{ + int ret; + switch (rate) + { + case 48000: + ret = 1; + break; + case 24000: + ret = 2; + break; + case 16000: + ret = 3; + break; + case 12000: + ret = 4; + break; + case 8000: + ret = 6; + break; + default: +#ifndef CUSTOM_MODES + celt_assert(0); +#endif + ret = 0; + break; + } + return ret; +} + +#if !defined(OVERRIDE_COMB_FILTER_CONST) || defined(NON_STATIC_COMB_FILTER_CONST_C) +/* This version should be faster on ARM */ +#ifdef OPUS_ARM_ASM +#ifndef NON_STATIC_COMB_FILTER_CONST_C +static +#endif +void comb_filter_const_c(opus_val32 *y, opus_val32 *x, int T, int N, + opus_val16 g10, opus_val16 g11, opus_val16 g12) +{ + opus_val32 x0, x1, x2, x3, x4; + int i; + x4 = SHL32(x[-T-2], 1); + x3 = SHL32(x[-T-1], 1); + x2 = SHL32(x[-T], 1); + x1 = SHL32(x[-T+1], 1); + for (i=0;inbEBands;i++) + { + int N; + N=(m->eBands[i+1]-m->eBands[i])<cache.caps[m->nbEBands*(2*LM+C-1)+i]+64)*C*N>>2; + } +} + + + +const char *opus_strerror(int error) +{ + static const char * const error_strings[8] = { + "success", + "invalid argument", + "buffer too small", + "internal error", + "corrupted stream", + "request not implemented", + "invalid state", + "memory allocation failed" + }; + if (error > 0 || error < -7) + return "unknown error"; + else + return error_strings[-error]; +} + +const char *opus_get_version_string(void) +{ + return "libopus " PACKAGE_VERSION + /* Applications may rely on the presence of this substring in the version + string to determine if they have a fixed-point or floating-point build + at runtime. */ +#ifdef FIXED_POINT + "-fixed" +#endif +#ifdef FUZZING + "-fuzzing" +#endif + ; +} diff --git a/port/third_party/opus/celt/celt.h b/port/third_party/opus/celt/celt.h new file mode 100644 index 00000000..2f501951 --- /dev/null +++ b/port/third_party/opus/celt/celt.h @@ -0,0 +1,252 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Copyright (c) 2008 Gregory Maxwell + Written by Jean-Marc Valin and Gregory Maxwell */ +/** + @file celt.h + @brief Contains all the functions for encoding and decoding audio + */ + +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef CELT_H +#define CELT_H + +#include "opus_types.h" +#include "opus_defines.h" +#include "opus_custom.h" +#include "entenc.h" +#include "entdec.h" +#include "arch.h" + +#ifdef ENABLE_DEEP_PLC +#include "lpcnet.h" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define CELTEncoder OpusCustomEncoder +#define CELTDecoder OpusCustomDecoder +#define CELTMode OpusCustomMode + +#define LEAK_BANDS 19 + +typedef struct { + int valid; + float tonality; + float tonality_slope; + float noisiness; + float activity; + float music_prob; + float music_prob_min; + float music_prob_max; + int bandwidth; + float activity_probability; + float max_pitch_ratio; + /* Store as Q6 char to save space. */ + unsigned char leak_boost[LEAK_BANDS]; +} AnalysisInfo; + +typedef struct { + int signalType; + int offset; +} SILKInfo; + +#define __celt_check_mode_ptr_ptr(ptr) ((ptr) + ((ptr) - (const CELTMode**)(ptr))) + +#define __celt_check_analysis_ptr(ptr) ((ptr) + ((ptr) - (const AnalysisInfo*)(ptr))) + +#define __celt_check_silkinfo_ptr(ptr) ((ptr) + ((ptr) - (const SILKInfo*)(ptr))) + +/* Encoder/decoder Requests */ + + +#define CELT_SET_PREDICTION_REQUEST 10002 +/** Controls the use of interframe prediction. + 0=Independent frames + 1=Short term interframe prediction allowed + 2=Long term prediction allowed + */ +#define CELT_SET_PREDICTION(x) CELT_SET_PREDICTION_REQUEST, __opus_check_int(x) + +#define CELT_SET_INPUT_CLIPPING_REQUEST 10004 +#define CELT_SET_INPUT_CLIPPING(x) CELT_SET_INPUT_CLIPPING_REQUEST, __opus_check_int(x) + +#define CELT_GET_AND_CLEAR_ERROR_REQUEST 10007 +#define CELT_GET_AND_CLEAR_ERROR(x) CELT_GET_AND_CLEAR_ERROR_REQUEST, __opus_check_int_ptr(x) + +#define CELT_SET_CHANNELS_REQUEST 10008 +#define CELT_SET_CHANNELS(x) CELT_SET_CHANNELS_REQUEST, __opus_check_int(x) + + +/* Internal */ +#define CELT_SET_START_BAND_REQUEST 10010 +#define CELT_SET_START_BAND(x) CELT_SET_START_BAND_REQUEST, __opus_check_int(x) + +#define CELT_SET_END_BAND_REQUEST 10012 +#define CELT_SET_END_BAND(x) CELT_SET_END_BAND_REQUEST, __opus_check_int(x) + +#define CELT_GET_MODE_REQUEST 10015 +/** Get the CELTMode used by an encoder or decoder */ +#define CELT_GET_MODE(x) CELT_GET_MODE_REQUEST, __celt_check_mode_ptr_ptr(x) + +#define CELT_SET_SIGNALLING_REQUEST 10016 +#define CELT_SET_SIGNALLING(x) CELT_SET_SIGNALLING_REQUEST, __opus_check_int(x) + +#define CELT_SET_TONALITY_REQUEST 10018 +#define CELT_SET_TONALITY(x) CELT_SET_TONALITY_REQUEST, __opus_check_int(x) +#define CELT_SET_TONALITY_SLOPE_REQUEST 10020 +#define CELT_SET_TONALITY_SLOPE(x) CELT_SET_TONALITY_SLOPE_REQUEST, __opus_check_int(x) + +#define CELT_SET_ANALYSIS_REQUEST 10022 +#define CELT_SET_ANALYSIS(x) CELT_SET_ANALYSIS_REQUEST, __celt_check_analysis_ptr(x) + +#define OPUS_SET_LFE_REQUEST 10024 +#define OPUS_SET_LFE(x) OPUS_SET_LFE_REQUEST, __opus_check_int(x) + +#define OPUS_SET_ENERGY_MASK_REQUEST 10026 +#define OPUS_SET_ENERGY_MASK(x) OPUS_SET_ENERGY_MASK_REQUEST, __opus_check_val16_ptr(x) + +#define CELT_SET_SILK_INFO_REQUEST 10028 +#define CELT_SET_SILK_INFO(x) CELT_SET_SILK_INFO_REQUEST, __celt_check_silkinfo_ptr(x) + +/* Encoder stuff */ + +int celt_encoder_get_size(int channels); + +int celt_encode_with_ec(OpusCustomEncoder * OPUS_RESTRICT st, const opus_val16 * pcm, int frame_size, unsigned char *compressed, int nbCompressedBytes, ec_enc *enc); + +int celt_encoder_init(CELTEncoder *st, opus_int32 sampling_rate, int channels, + int arch); + + + +/* Decoder stuff */ + +int celt_decoder_get_size(int channels); + + +int celt_decoder_init(CELTDecoder *st, opus_int32 sampling_rate, int channels); + +int celt_decode_with_ec_dred(CELTDecoder * OPUS_RESTRICT st, const unsigned char *data, + int len, opus_val16 * OPUS_RESTRICT pcm, int frame_size, ec_dec *dec, int accum +#ifdef ENABLE_DEEP_PLC + ,LPCNetPLCState *lpcnet +#endif + ); + +int celt_decode_with_ec(OpusCustomDecoder * OPUS_RESTRICT st, const unsigned char *data, + int len, opus_val16 * OPUS_RESTRICT pcm, int frame_size, ec_dec *dec, int accum); + +#define celt_encoder_ctl opus_custom_encoder_ctl +#define celt_decoder_ctl opus_custom_decoder_ctl + + +#ifdef CUSTOM_MODES +#define OPUS_CUSTOM_NOSTATIC +#else +#define OPUS_CUSTOM_NOSTATIC static OPUS_INLINE +#endif + +static const unsigned char trim_icdf[11] = {126, 124, 119, 109, 87, 41, 19, 9, 4, 2, 0}; +/* Probs: NONE: 21.875%, LIGHT: 6.25%, NORMAL: 65.625%, AGGRESSIVE: 6.25% */ +static const unsigned char spread_icdf[4] = {25, 23, 2, 0}; + +static const unsigned char tapset_icdf[3]={2,1,0}; + +#ifdef CUSTOM_MODES +static const unsigned char toOpusTable[20] = { + 0xE0, 0xE8, 0xF0, 0xF8, + 0xC0, 0xC8, 0xD0, 0xD8, + 0xA0, 0xA8, 0xB0, 0xB8, + 0x00, 0x00, 0x00, 0x00, + 0x80, 0x88, 0x90, 0x98, +}; + +static const unsigned char fromOpusTable[16] = { + 0x80, 0x88, 0x90, 0x98, + 0x40, 0x48, 0x50, 0x58, + 0x20, 0x28, 0x30, 0x38, + 0x00, 0x08, 0x10, 0x18 +}; + +static OPUS_INLINE int toOpus(unsigned char c) +{ + int ret=0; + if (c<0xA0) + ret = toOpusTable[c>>3]; + if (ret == 0) + return -1; + else + return ret|(c&0x7); +} + +static OPUS_INLINE int fromOpus(unsigned char c) +{ + if (c<0x80) + return -1; + else + return fromOpusTable[(c>>3)-16] | (c&0x7); +} +#endif /* CUSTOM_MODES */ + +#define COMBFILTER_MAXPERIOD 1024 +#define COMBFILTER_MINPERIOD 15 + +extern const signed char tf_select_table[4][8]; + +#if defined(ENABLE_HARDENING) || defined(ENABLE_ASSERTIONS) +void validate_celt_decoder(CELTDecoder *st); +#define VALIDATE_CELT_DECODER(st) validate_celt_decoder(st) +#else +#define VALIDATE_CELT_DECODER(st) +#endif + +int resampling_factor(opus_int32 rate); + +void celt_preemphasis(const opus_val16 * OPUS_RESTRICT pcmp, celt_sig * OPUS_RESTRICT inp, + int N, int CC, int upsample, const opus_val16 *coef, celt_sig *mem, int clip); + +void comb_filter(opus_val32 *y, opus_val32 *x, int T0, int T1, int N, + opus_val16 g0, opus_val16 g1, int tapset0, int tapset1, + const opus_val16 *window, int overlap, int arch); + +void init_caps(const CELTMode *m,int *cap,int LM,int C); + +#ifdef RESYNTH +void deemphasis(celt_sig *in[], opus_val16 *pcm, int N, int C, int downsample, const opus_val16 *coef, celt_sig *mem, int accum); +void celt_synthesis(const CELTMode *mode, celt_norm *X, celt_sig * out_syn[], + opus_val16 *oldBandE, int start, int effEnd, int C, int CC, int isTransient, + int LM, int downsample, int silence, int arch); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* CELT_H */ diff --git a/port/third_party/opus/celt/celt_decoder.c b/port/third_party/opus/celt/celt_decoder.c new file mode 100644 index 00000000..743c2031 --- /dev/null +++ b/port/third_party/opus/celt/celt_decoder.c @@ -0,0 +1,1591 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2010 Xiph.Org Foundation + Copyright (c) 2008 Gregory Maxwell + Written by Jean-Marc Valin and Gregory Maxwell */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#define CELT_DECODER_C + +#include "cpu_support.h" +#include "os_support.h" +#include "mdct.h" +#include +#include "celt.h" +#include "pitch.h" +#include "bands.h" +#include "modes.h" +#include "entcode.h" +#include "quant_bands.h" +#include "rate.h" +#include "stack_alloc.h" +#include "mathops.h" +#include "float_cast.h" +#include +#include "celt_lpc.h" +#include "vq.h" + +#ifdef ENABLE_DEEP_PLC +#include "lpcnet.h" +#include "lpcnet_private.h" +#endif + +/* The maximum pitch lag to allow in the pitch-based PLC. It's possible to save + CPU time in the PLC pitch search by making this smaller than MAX_PERIOD. The + current value corresponds to a pitch of 66.67 Hz. */ +#define PLC_PITCH_LAG_MAX (720) +/* The minimum pitch lag to allow in the pitch-based PLC. This corresponds to a + pitch of 480 Hz. */ +#define PLC_PITCH_LAG_MIN (100) + +/**********************************************************************/ +/* */ +/* DECODER */ +/* */ +/**********************************************************************/ +#define DECODE_BUFFER_SIZE 2048 + +#define PLC_UPDATE_FRAMES 4 +#define PLC_UPDATE_SAMPLES (PLC_UPDATE_FRAMES*FRAME_SIZE) + +/** Decoder state + @brief Decoder state + */ +struct OpusCustomDecoder { + const OpusCustomMode *mode; + int overlap; + int channels; + int stream_channels; + + int downsample; + int start, end; + int signalling; + int disable_inv; + int complexity; + int arch; + + /* Everything beyond this point gets cleared on a reset */ +#define DECODER_RESET_START rng + + opus_uint32 rng; + int error; + int last_pitch_index; + int loss_duration; + int skip_plc; + int postfilter_period; + int postfilter_period_old; + opus_val16 postfilter_gain; + opus_val16 postfilter_gain_old; + int postfilter_tapset; + int postfilter_tapset_old; + int prefilter_and_fold; + + celt_sig preemph_memD[2]; + +#ifdef ENABLE_DEEP_PLC + opus_int16 plc_pcm[PLC_UPDATE_SAMPLES]; + int plc_fill; + float plc_preemphasis_mem; +#endif + + celt_sig _decode_mem[1]; /* Size = channels*(DECODE_BUFFER_SIZE+mode->overlap) */ + /* opus_val16 lpc[], Size = channels*CELT_LPC_ORDER */ + /* opus_val16 oldEBands[], Size = 2*mode->nbEBands */ + /* opus_val16 oldLogE[], Size = 2*mode->nbEBands */ + /* opus_val16 oldLogE2[], Size = 2*mode->nbEBands */ + /* opus_val16 backgroundLogE[], Size = 2*mode->nbEBands */ +}; + +#if defined(ENABLE_HARDENING) || defined(ENABLE_ASSERTIONS) +/* Make basic checks on the CELT state to ensure we don't end + up writing all over memory. */ +void validate_celt_decoder(CELTDecoder *st) +{ +#ifndef CUSTOM_MODES + celt_assert(st->mode == opus_custom_mode_create(48000, 960, NULL)); + celt_assert(st->overlap == 120); + celt_assert(st->end <= 21); +#else +/* From Section 4.3 in the spec: "The normal CELT layer uses 21 of those bands, + though Opus Custom (see Section 6.2) may use a different number of bands" + + Check if it's within the maximum number of Bark frequency bands instead */ + celt_assert(st->end <= 25); +#endif + celt_assert(st->channels == 1 || st->channels == 2); + celt_assert(st->stream_channels == 1 || st->stream_channels == 2); + celt_assert(st->downsample > 0); + celt_assert(st->start == 0 || st->start == 17); + celt_assert(st->start < st->end); +#ifdef OPUS_ARCHMASK + celt_assert(st->arch >= 0); + celt_assert(st->arch <= OPUS_ARCHMASK); +#endif + celt_assert(st->last_pitch_index <= PLC_PITCH_LAG_MAX); + celt_assert(st->last_pitch_index >= PLC_PITCH_LAG_MIN || st->last_pitch_index == 0); + celt_assert(st->postfilter_period < MAX_PERIOD); + celt_assert(st->postfilter_period >= COMBFILTER_MINPERIOD || st->postfilter_period == 0); + celt_assert(st->postfilter_period_old < MAX_PERIOD); + celt_assert(st->postfilter_period_old >= COMBFILTER_MINPERIOD || st->postfilter_period_old == 0); + celt_assert(st->postfilter_tapset <= 2); + celt_assert(st->postfilter_tapset >= 0); + celt_assert(st->postfilter_tapset_old <= 2); + celt_assert(st->postfilter_tapset_old >= 0); +} +#endif + +int celt_decoder_get_size(int channels) +{ + const CELTMode *mode = opus_custom_mode_create(48000, 960, NULL); + return opus_custom_decoder_get_size(mode, channels); +} + +OPUS_CUSTOM_NOSTATIC int opus_custom_decoder_get_size(const CELTMode *mode, int channels) +{ + int size = sizeof(struct CELTDecoder) + + (channels*(DECODE_BUFFER_SIZE+mode->overlap)-1)*sizeof(celt_sig) + + channels*CELT_LPC_ORDER*sizeof(opus_val16) + + 4*2*mode->nbEBands*sizeof(opus_val16); + return size; +} + +#ifdef CUSTOM_MODES +CELTDecoder *opus_custom_decoder_create(const CELTMode *mode, int channels, int *error) +{ + int ret; + CELTDecoder *st = (CELTDecoder *)opus_alloc(opus_custom_decoder_get_size(mode, channels)); + ret = opus_custom_decoder_init(st, mode, channels); + if (ret != OPUS_OK) + { + opus_custom_decoder_destroy(st); + st = NULL; + } + if (error) + *error = ret; + return st; +} +#endif /* CUSTOM_MODES */ + +int celt_decoder_init(CELTDecoder *st, opus_int32 sampling_rate, int channels) +{ + int ret; + ret = opus_custom_decoder_init(st, opus_custom_mode_create(48000, 960, NULL), channels); + if (ret != OPUS_OK) + return ret; + st->downsample = resampling_factor(sampling_rate); + if (st->downsample==0) + return OPUS_BAD_ARG; + else + return OPUS_OK; +} + +OPUS_CUSTOM_NOSTATIC int opus_custom_decoder_init(CELTDecoder *st, const CELTMode *mode, int channels) +{ + if (channels < 0 || channels > 2) + return OPUS_BAD_ARG; + + if (st==NULL) + return OPUS_ALLOC_FAIL; + + OPUS_CLEAR((char*)st, opus_custom_decoder_get_size(mode, channels)); + + st->mode = mode; + st->overlap = mode->overlap; + st->stream_channels = st->channels = channels; + + st->downsample = 1; + st->start = 0; + st->end = st->mode->effEBands; + st->signalling = 1; +#ifndef DISABLE_UPDATE_DRAFT + st->disable_inv = channels == 1; +#else + st->disable_inv = 0; +#endif + st->arch = opus_select_arch(); + + opus_custom_decoder_ctl(st, OPUS_RESET_STATE); + + return OPUS_OK; +} + +#ifdef CUSTOM_MODES +void opus_custom_decoder_destroy(CELTDecoder *st) +{ + opus_free(st); +} +#endif /* CUSTOM_MODES */ + +#ifndef CUSTOM_MODES +/* Special case for stereo with no downsampling and no accumulation. This is + quite common and we can make it faster by processing both channels in the + same loop, reducing overhead due to the dependency loop in the IIR filter. */ +static void deemphasis_stereo_simple(celt_sig *in[], opus_val16 *pcm, int N, const opus_val16 coef0, + celt_sig *mem) +{ + celt_sig * OPUS_RESTRICT x0; + celt_sig * OPUS_RESTRICT x1; + celt_sig m0, m1; + int j; + x0=in[0]; + x1=in[1]; + m0 = mem[0]; + m1 = mem[1]; + for (j=0;j1) + { + /* Shortcut for the standard (non-custom modes) case */ + for (j=0;joverlap; + nbEBands = mode->nbEBands; + N = mode->shortMdctSize<shortMdctSize; + shift = mode->maxLM; + } else { + B = 1; + NB = mode->shortMdctSize<maxLM-LM; + } + + if (CC==2&&C==1) + { + /* Copying a mono streams to two channels */ + celt_sig *freq2; + denormalise_bands(mode, X, freq, oldBandE, start, effEnd, M, + downsample, silence); + /* Store a temporary copy in the output buffer because the IMDCT destroys its input. */ + freq2 = out_syn[1]+overlap/2; + OPUS_COPY(freq2, freq, N); + for (b=0;bmdct, &freq2[b], out_syn[0]+NB*b, mode->window, overlap, shift, B, arch); + for (b=0;bmdct, &freq[b], out_syn[1]+NB*b, mode->window, overlap, shift, B, arch); + } else if (CC==1&&C==2) + { + /* Downmixing a stereo stream to mono */ + celt_sig *freq2; + freq2 = out_syn[0]+overlap/2; + denormalise_bands(mode, X, freq, oldBandE, start, effEnd, M, + downsample, silence); + /* Use the output buffer as temp array before downmixing. */ + denormalise_bands(mode, X+N, freq2, oldBandE+nbEBands, start, effEnd, M, + downsample, silence); + for (i=0;imdct, &freq[b], out_syn[0]+NB*b, mode->window, overlap, shift, B, arch); + } else { + /* Normal case (mono or stereo) */ + c=0; do { + denormalise_bands(mode, X+c*N, freq, oldBandE+c*nbEBands, start, effEnd, M, + downsample, silence); + for (b=0;bmdct, &freq[b], out_syn[c]+NB*b, mode->window, overlap, shift, B, arch); + } while (++cstorage*8; + tell = ec_tell(dec); + logp = isTransient ? 2 : 4; + tf_select_rsv = LM>0 && tell+logp+1<=budget; + budget -= tf_select_rsv; + tf_changed = curr = 0; + for (i=start;i>1, opus_val16 ); + pitch_downsample(decode_mem, lp_pitch_buf, + DECODE_BUFFER_SIZE, C, arch); + pitch_search(lp_pitch_buf+(PLC_PITCH_LAG_MAX>>1), lp_pitch_buf, + DECODE_BUFFER_SIZE-PLC_PITCH_LAG_MAX, + PLC_PITCH_LAG_MAX-PLC_PITCH_LAG_MIN, &pitch_index, arch); + pitch_index = PLC_PITCH_LAG_MAX-pitch_index; + RESTORE_STACK; + return pitch_index; +} + +static void prefilter_and_fold(CELTDecoder * OPUS_RESTRICT st, int N) +{ + int c; + int CC; + int i; + int overlap; + celt_sig *decode_mem[2]; + const OpusCustomMode *mode; + VARDECL(opus_val32, etmp); + mode = st->mode; + overlap = st->overlap; + CC = st->channels; + ALLOC(etmp, overlap, opus_val32); + c=0; do { + decode_mem[c] = st->_decode_mem + c*(DECODE_BUFFER_SIZE+overlap); + } while (++cpostfilter_period_old, st->postfilter_period, overlap, + -st->postfilter_gain_old, -st->postfilter_gain, + st->postfilter_tapset_old, st->postfilter_tapset, NULL, 0, st->arch); + + /* Simulate TDAC on the concealed audio so that it blends with the + MDCT of the next frame. */ + for (i=0;iwindow[i], etmp[overlap-1-i]) + + MULT16_32_Q15(mode->window[overlap-i-1], etmp[i]); + } + } while (++cfec_read_pos; + tmp_fec_skip = lpcnet->fec_skip; + for (i=0;ifec_read_pos = tmp_read_post; + lpcnet->fec_skip = tmp_fec_skip; +} +#endif + +static void celt_decode_lost(CELTDecoder * OPUS_RESTRICT st, int N, int LM +#ifdef ENABLE_DEEP_PLC + ,LPCNetPLCState *lpcnet +#endif + ) +{ + int c; + int i; + const int C = st->channels; + celt_sig *decode_mem[2]; + celt_sig *out_syn[2]; + opus_val16 *lpc; + opus_val16 *oldBandE, *oldLogE, *oldLogE2, *backgroundLogE; + const OpusCustomMode *mode; + int nbEBands; + int overlap; + int start; + int loss_duration; + int noise_based; + const opus_int16 *eBands; + SAVE_STACK; + + mode = st->mode; + nbEBands = mode->nbEBands; + overlap = mode->overlap; + eBands = mode->eBands; + + c=0; do { + decode_mem[c] = st->_decode_mem + c*(DECODE_BUFFER_SIZE+overlap); + out_syn[c] = decode_mem[c]+DECODE_BUFFER_SIZE-N; + } while (++c_decode_mem+(DECODE_BUFFER_SIZE+overlap)*C); + oldBandE = lpc+C*CELT_LPC_ORDER; + oldLogE = oldBandE + 2*nbEBands; + oldLogE2 = oldLogE + 2*nbEBands; + backgroundLogE = oldLogE2 + 2*nbEBands; + + loss_duration = st->loss_duration; + start = st->start; +#ifdef ENABLE_DEEP_PLC + noise_based = start != 0 || (lpcnet->fec_fill_pos == 0 && (st->skip_plc || loss_duration >= 80)); +#else + noise_based = loss_duration >= 40 || start != 0 || st->skip_plc; +#endif + if (noise_based) + { + /* Noise-based PLC/CNG */ + VARDECL(celt_norm, X); + opus_uint32 seed; + int end; + int effEnd; + opus_val16 decay; + end = st->end; + effEnd = IMAX(start, IMIN(end, mode->effEBands)); + + ALLOC(X, C*N, celt_norm); /**< Interleaved normalised MDCTs */ + c=0; do { + OPUS_MOVE(decode_mem[c], decode_mem[c]+N, + DECODE_BUFFER_SIZE-N+overlap); + } while (++cprefilter_and_fold) { + prefilter_and_fold(st, N); + } + + /* Energy decay */ + decay = loss_duration==0 ? QCONST16(1.5f, DB_SHIFT) : QCONST16(.5f, DB_SHIFT); + c=0; do + { + for (i=start;irng; + for (c=0;c>20); + } + renormalise_vector(X+boffs, blen, Q15ONE, st->arch); + } + } + st->rng = seed; + + celt_synthesis(mode, X, out_syn, oldBandE, start, effEnd, C, C, 0, LM, st->downsample, 0, st->arch); + st->prefilter_and_fold = 0; + /* Skip regular PLC until we get two consecutive packets. */ + st->skip_plc = 1; + } else { + int exc_length; + /* Pitch-based PLC */ + const opus_val16 *window; + opus_val16 *exc; + opus_val16 fade = Q15ONE; + int pitch_index; + VARDECL(opus_val16, _exc); + VARDECL(opus_val16, fir_tmp); + + if (loss_duration == 0) + { +#ifdef ENABLE_DEEP_PLC + if (lpcnet->loaded) update_plc_state(lpcnet, decode_mem, &st->plc_preemphasis_mem, C); +#endif + st->last_pitch_index = pitch_index = celt_plc_pitch_search(decode_mem, C, st->arch); + } else { + pitch_index = st->last_pitch_index; + fade = QCONST16(.8f,15); + } + + /* We want the excitation for 2 pitch periods in order to look for a + decaying signal, but we can't get more than MAX_PERIOD. */ + exc_length = IMIN(2*pitch_index, MAX_PERIOD); + + ALLOC(_exc, MAX_PERIOD+CELT_LPC_ORDER, opus_val16); + ALLOC(fir_tmp, exc_length, opus_val16); + exc = _exc+CELT_LPC_ORDER; + window = mode->window; + c=0; do { + opus_val16 decay; + opus_val16 attenuation; + opus_val32 S1=0; + celt_sig *buf; + int extrapolation_offset; + int extrapolation_len; + int j; + + buf = decode_mem[c]; + for (i=0;iarch); + /* Add a noise floor of -40 dB. */ +#ifdef FIXED_POINT + ac[0] += SHR32(ac[0],13); +#else + ac[0] *= 1.0001f; +#endif + /* Use lag windowing to stabilize the Levinson-Durbin recursion. */ + for (i=1;i<=CELT_LPC_ORDER;i++) + { + /*ac[i] *= exp(-.5*(2*M_PI*.002*i)*(2*M_PI*.002*i));*/ +#ifdef FIXED_POINT + ac[i] -= MULT16_32_Q15(2*i*i, ac[i]); +#else + ac[i] -= ac[i]*(0.008f*0.008f)*i*i; +#endif + } + _celt_lpc(lpc+c*CELT_LPC_ORDER, ac, CELT_LPC_ORDER); +#ifdef FIXED_POINT + /* For fixed-point, apply bandwidth expansion until we can guarantee that + no overflow can happen in the IIR filter. This means: + 32768*sum(abs(filter)) < 2^31 */ + while (1) { + opus_val16 tmp=Q15ONE; + opus_val32 sum=QCONST16(1., SIG_SHIFT); + for (i=0;iarch); + OPUS_COPY(exc+MAX_PERIOD-exc_length, fir_tmp, exc_length); + } + + /* Check if the waveform is decaying, and if so how fast. + We do this to avoid adding energy when concealing in a segment + with decaying energy. */ + { + opus_val32 E1=1, E2=1; + int decay_length; +#ifdef FIXED_POINT + int shift = IMAX(0,2*celt_zlog2(celt_maxabs16(&exc[MAX_PERIOD-exc_length], exc_length))-20); +#endif + decay_length = exc_length>>1; + for (i=0;i= pitch_index) { + j -= pitch_index; + attenuation = MULT16_16_Q15(attenuation, decay); + } + buf[DECODE_BUFFER_SIZE-N+i] = + SHL32(EXTEND32(MULT16_16_Q15(attenuation, + exc[extrapolation_offset+j])), SIG_SHIFT); + /* Compute the energy of the previously decoded signal whose + excitation we're copying. */ + tmp = SROUND16( + buf[DECODE_BUFFER_SIZE-MAX_PERIOD-N+extrapolation_offset+j], + SIG_SHIFT); + S1 += SHR32(MULT16_16(tmp, tmp), 10); + } + { + opus_val16 lpc_mem[CELT_LPC_ORDER]; + /* Copy the last decoded samples (prior to the overlap region) to + synthesis filter memory so we can have a continuous signal. */ + for (i=0;iarch); +#ifdef FIXED_POINT + for (i=0; i < extrapolation_len; i++) + buf[DECODE_BUFFER_SIZE-N+i] = SATURATE(buf[DECODE_BUFFER_SIZE-N+i], SIG_SAT); +#endif + } + + /* Check if the synthesis energy is higher than expected, which can + happen with the signal changes during our window. If so, + attenuate. */ + { + opus_val32 S2=0; + for (i=0;i SHR32(S2,2))) +#else + /* The float test is written this way to catch NaNs in the output + of the IIR filter at the same time. */ + if (!(S1 > 0.2f*S2)) +#endif + { + for (i=0;iloaded && (st->complexity >= 5 || lpcnet->fec_fill_pos > 0)) { + float overlap_mem; + int samples_needed16k; + celt_sig *buf; + VARDECL(float, buf_copy); + buf = decode_mem[0]; + ALLOC(buf_copy, C*overlap, float); + c=0; do { + OPUS_COPY(buf_copy+c*overlap, &decode_mem[c][DECODE_BUFFER_SIZE-N], overlap); + } while (++cplc_fill = 0; + } + while (st->plc_fill < samples_needed16k) { + lpcnet_plc_conceal(lpcnet, &st->plc_pcm[st->plc_fill]); + st->plc_fill += FRAME_SIZE; + } + /* Resample to 48 kHz. */ + for (i=0;i<(N+overlap)/3;i++) { + int j; + float sum; + for (sum=0, j=0;j<17;j++) sum += 3*st->plc_pcm[i+j]*sinc_filter[3*j]; + buf[DECODE_BUFFER_SIZE-N+3*i] = sum; + for (sum=0, j=0;j<16;j++) sum += 3*st->plc_pcm[i+j+1]*sinc_filter[3*j+2]; + buf[DECODE_BUFFER_SIZE-N+3*i+1] = sum; + for (sum=0, j=0;j<16;j++) sum += 3*st->plc_pcm[i+j+1]*sinc_filter[3*j+1]; + buf[DECODE_BUFFER_SIZE-N+3*i+2] = sum; + } + OPUS_MOVE(st->plc_pcm, &st->plc_pcm[N/3], st->plc_fill-N/3); + st->plc_fill -= N/3; + for (i=0;iplc_preemphasis_mem; + st->plc_preemphasis_mem = tmp; + } + overlap_mem = st->plc_preemphasis_mem; + for (i=0;iprefilter_and_fold = 1; + } + + /* Saturate to soemthing large to avoid wrap-around. */ + st->loss_duration = IMIN(10000, loss_duration+(1<channels; + int LM, M; + int start; + int end; + int effEnd; + int codedBands; + int alloc_trim; + int postfilter_pitch; + opus_val16 postfilter_gain; + int intensity=0; + int dual_stereo=0; + opus_int32 total_bits; + opus_int32 balance; + opus_int32 tell; + int dynalloc_logp; + int postfilter_tapset; + int anti_collapse_rsv; + int anti_collapse_on=0; + int silence; + int C = st->stream_channels; + const OpusCustomMode *mode; + int nbEBands; + int overlap; + const opus_int16 *eBands; + opus_val16 max_background_increase; + ALLOC_STACK; + + VALIDATE_CELT_DECODER(st); + mode = st->mode; + nbEBands = mode->nbEBands; + overlap = mode->overlap; + eBands = mode->eBands; + start = st->start; + end = st->end; + frame_size *= st->downsample; + + lpc = (opus_val16*)(st->_decode_mem+(DECODE_BUFFER_SIZE+overlap)*CC); + oldBandE = lpc+CC*CELT_LPC_ORDER; + oldLogE = oldBandE + 2*nbEBands; + oldLogE2 = oldLogE + 2*nbEBands; + backgroundLogE = oldLogE2 + 2*nbEBands; + +#ifdef CUSTOM_MODES + if (st->signalling && data!=NULL) + { + int data0=data[0]; + /* Convert "standard mode" to Opus header */ + if (mode->Fs==48000 && mode->shortMdctSize==120) + { + data0 = fromOpus(data0); + if (data0<0) + return OPUS_INVALID_PACKET; + } + st->end = end = IMAX(1, mode->effEBands-2*(data0>>5)); + LM = (data0>>3)&0x3; + C = 1 + ((data0>>2)&0x1); + data++; + len--; + if (LM>mode->maxLM) + return OPUS_INVALID_PACKET; + if (frame_size < mode->shortMdctSize<shortMdctSize<maxLM;LM++) + if (mode->shortMdctSize<mode->maxLM) + return OPUS_BAD_ARG; + } + M=1<1275 || pcm==NULL) + return OPUS_BAD_ARG; + + N = M*mode->shortMdctSize; + c=0; do { + decode_mem[c] = st->_decode_mem + c*(DECODE_BUFFER_SIZE+overlap); + out_syn[c] = decode_mem[c]+DECODE_BUFFER_SIZE-N; + } while (++c mode->effEBands) + effEnd = mode->effEBands; + + if (data == NULL || len<=1) + { + celt_decode_lost(st, N, LM +#ifdef ENABLE_DEEP_PLC + , lpcnet +#endif + ); + deemphasis(out_syn, pcm, N, CC, st->downsample, mode->preemph, st->preemph_memD, accum); + RESTORE_STACK; + return frame_size/st->downsample; + } +#ifdef ENABLE_DEEP_PLC + else { + /* FIXME: This is a bit of a hack just to make sure opus_decode_native() knows we're no longer in PLC. */ + if (lpcnet) lpcnet->blend = 0; + } +#endif + + /* Check if there are at least two packets received consecutively before + * turning on the pitch-based PLC */ + if (st->loss_duration == 0) st->skip_plc = 0; + + if (dec == NULL) + { + ec_dec_init(&_dec,(unsigned char*)data,len); + dec = &_dec; + } + + if (C==1) + { + for (i=0;i= total_bits) + silence = 1; + else if (tell==1) + silence = ec_dec_bit_logp(dec, 15); + else + silence = 0; + if (silence) + { + /* Pretend we've read all the remaining bits */ + tell = len*8; + dec->nbits_total+=tell-ec_tell(dec); + } + + postfilter_gain = 0; + postfilter_pitch = 0; + postfilter_tapset = 0; + if (start==0 && tell+16 <= total_bits) + { + if(ec_dec_bit_logp(dec, 1)) + { + int qg, octave; + octave = ec_dec_uint(dec, 6); + postfilter_pitch = (16< 0 && tell+3 <= total_bits) + { + isTransient = ec_dec_bit_logp(dec, 3); + tell = ec_tell(dec); + } + else + isTransient = 0; + + if (isTransient) + shortBlocks = M; + else + shortBlocks = 0; + + /* Decode the global flags (first symbols in the stream) */ + intra_ener = tell+3<=total_bits ? ec_dec_bit_logp(dec, 3) : 0; + /* If recovering from packet loss, make sure we make the energy prediction safe to reduce the + risk of getting loud artifacts. */ + if (!intra_ener && st->loss_duration != 0) { + c=0; do + { + opus_val16 safety = 0; + int missing = IMIN(10, st->loss_duration>>LM); + if (LM==0) safety = QCONST16(1.5f,DB_SHIFT); + else if (LM==1) safety = QCONST16(.5f,DB_SHIFT); + for (i=start;i0) + dynalloc_logp = IMAX(2, dynalloc_logp-1); + } + + ALLOC(fine_quant, nbEBands, int); + alloc_trim = tell+(6<=2&&bits>=((LM+2)<rng, 0, + st->arch, st->disable_inv); + + if (anti_collapse_rsv > 0) + { + anti_collapse_on = ec_dec_bits(dec, 1); + } + + unquant_energy_finalise(mode, start, end, oldBandE, + fine_quant, fine_priority, len*8-ec_tell(dec), dec, C); + + if (anti_collapse_on) + anti_collapse(mode, X, collapse_masks, LM, C, N, + start, end, oldBandE, oldLogE, oldLogE2, pulses, st->rng, st->arch); + + if (silence) + { + for (i=0;iprefilter_and_fold) { + prefilter_and_fold(st, N); + } + celt_synthesis(mode, X, out_syn, oldBandE, start, effEnd, + C, CC, isTransient, LM, st->downsample, silence, st->arch); + + c=0; do { + st->postfilter_period=IMAX(st->postfilter_period, COMBFILTER_MINPERIOD); + st->postfilter_period_old=IMAX(st->postfilter_period_old, COMBFILTER_MINPERIOD); + comb_filter(out_syn[c], out_syn[c], st->postfilter_period_old, st->postfilter_period, mode->shortMdctSize, + st->postfilter_gain_old, st->postfilter_gain, st->postfilter_tapset_old, st->postfilter_tapset, + mode->window, overlap, st->arch); + if (LM!=0) + comb_filter(out_syn[c]+mode->shortMdctSize, out_syn[c]+mode->shortMdctSize, st->postfilter_period, postfilter_pitch, N-mode->shortMdctSize, + st->postfilter_gain, postfilter_gain, st->postfilter_tapset, postfilter_tapset, + mode->window, overlap, st->arch); + + } while (++cpostfilter_period_old = st->postfilter_period; + st->postfilter_gain_old = st->postfilter_gain; + st->postfilter_tapset_old = st->postfilter_tapset; + st->postfilter_period = postfilter_pitch; + st->postfilter_gain = postfilter_gain; + st->postfilter_tapset = postfilter_tapset; + if (LM!=0) + { + st->postfilter_period_old = st->postfilter_period; + st->postfilter_gain_old = st->postfilter_gain; + st->postfilter_tapset_old = st->postfilter_tapset; + } + + if (C==1) + OPUS_COPY(&oldBandE[nbEBands], oldBandE, nbEBands); + + if (!isTransient) + { + OPUS_COPY(oldLogE2, oldLogE, 2*nbEBands); + OPUS_COPY(oldLogE, oldBandE, 2*nbEBands); + } else { + for (i=0;i<2*nbEBands;i++) + oldLogE[i] = MIN16(oldLogE[i], oldBandE[i]); + } + /* In normal circumstances, we only allow the noise floor to increase by + up to 2.4 dB/second, but when we're in DTX we give the weight of + all missing packets to the update packet. */ + max_background_increase = IMIN(160, st->loss_duration+M)*QCONST16(0.001f,DB_SHIFT); + for (i=0;i<2*nbEBands;i++) + backgroundLogE[i] = MIN16(backgroundLogE[i] + max_background_increase, oldBandE[i]); + /* In case start or end were to change */ + c=0; do + { + for (i=0;irng = dec->rng; + + deemphasis(out_syn, pcm, N, CC, st->downsample, mode->preemph, st->preemph_memD, accum); + st->loss_duration = 0; + st->prefilter_and_fold = 0; + RESTORE_STACK; + if (ec_tell(dec) > 8*len) + return OPUS_INTERNAL_ERROR; + if(ec_get_error(dec)) + st->error = 1; + return frame_size/st->downsample; +} + +int celt_decode_with_ec(CELTDecoder * OPUS_RESTRICT st, const unsigned char *data, + int len, opus_val16 * OPUS_RESTRICT pcm, int frame_size, ec_dec *dec, int accum) +{ + return celt_decode_with_ec_dred(st, data, len, pcm, frame_size, dec, accum +#ifdef ENABLE_DEEP_PLC + , NULL +#endif + ); +} + +#ifdef CUSTOM_MODES + +#ifdef FIXED_POINT +int opus_custom_decode(CELTDecoder * OPUS_RESTRICT st, const unsigned char *data, int len, opus_int16 * OPUS_RESTRICT pcm, int frame_size) +{ + return celt_decode_with_ec(st, data, len, pcm, frame_size, NULL, 0); +} + +#ifndef DISABLE_FLOAT_API +int opus_custom_decode_float(CELTDecoder * OPUS_RESTRICT st, const unsigned char *data, int len, float * OPUS_RESTRICT pcm, int frame_size) +{ + int j, ret, C, N; + VARDECL(opus_int16, out); + ALLOC_STACK; + + if (pcm==NULL) + return OPUS_BAD_ARG; + + C = st->channels; + N = frame_size; + + ALLOC(out, C*N, opus_int16); + ret=celt_decode_with_ec(st, data, len, out, frame_size, NULL, 0); + if (ret>0) + for (j=0;jchannels; + N = frame_size; + ALLOC(out, C*N, celt_sig); + + ret=celt_decode_with_ec(st, data, len, out, frame_size, NULL, 0); + + if (ret>0) + for (j=0;j10) + { + goto bad_arg; + } + st->complexity = value; + } + break; + case OPUS_GET_COMPLEXITY_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->complexity; + } + break; + case CELT_SET_START_BAND_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<0 || value>=st->mode->nbEBands) + goto bad_arg; + st->start = value; + } + break; + case CELT_SET_END_BAND_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<1 || value>st->mode->nbEBands) + goto bad_arg; + st->end = value; + } + break; + case CELT_SET_CHANNELS_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<1 || value>2) + goto bad_arg; + st->stream_channels = value; + } + break; + case CELT_GET_AND_CLEAR_ERROR_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (value==NULL) + goto bad_arg; + *value=st->error; + st->error = 0; + } + break; + case OPUS_GET_LOOKAHEAD_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (value==NULL) + goto bad_arg; + *value = st->overlap/st->downsample; + } + break; + case OPUS_RESET_STATE: + { + int i; + opus_val16 *lpc, *oldBandE, *oldLogE, *oldLogE2; + lpc = (opus_val16*)(st->_decode_mem+(DECODE_BUFFER_SIZE+st->overlap)*st->channels); + oldBandE = lpc+st->channels*CELT_LPC_ORDER; + oldLogE = oldBandE + 2*st->mode->nbEBands; + oldLogE2 = oldLogE + 2*st->mode->nbEBands; + OPUS_CLEAR((char*)&st->DECODER_RESET_START, + opus_custom_decoder_get_size(st->mode, st->channels)- + ((char*)&st->DECODER_RESET_START - (char*)st)); + for (i=0;i<2*st->mode->nbEBands;i++) + oldLogE[i]=oldLogE2[i]=-QCONST16(28.f,DB_SHIFT); + st->skip_plc = 1; + } + break; + case OPUS_GET_PITCH_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (value==NULL) + goto bad_arg; + *value = st->postfilter_period; + } + break; + case CELT_GET_MODE_REQUEST: + { + const CELTMode ** value = va_arg(ap, const CELTMode**); + if (value==0) + goto bad_arg; + *value=st->mode; + } + break; + case CELT_SET_SIGNALLING_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + st->signalling = value; + } + break; + case OPUS_GET_FINAL_RANGE_REQUEST: + { + opus_uint32 * value = va_arg(ap, opus_uint32 *); + if (value==0) + goto bad_arg; + *value=st->rng; + } + break; + case OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>1) + { + goto bad_arg; + } + st->disable_inv = value; + } + break; + case OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->disable_inv; + } + break; + default: + goto bad_request; + } + va_end(ap); + return OPUS_OK; +bad_arg: + va_end(ap); + return OPUS_BAD_ARG; +bad_request: + va_end(ap); + return OPUS_UNIMPLEMENTED; +} diff --git a/port/third_party/opus/celt/celt_encoder.c b/port/third_party/opus/celt/celt_encoder.c new file mode 100644 index 00000000..7f32a801 --- /dev/null +++ b/port/third_party/opus/celt/celt_encoder.c @@ -0,0 +1,2639 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2010 Xiph.Org Foundation + Copyright (c) 2008 Gregory Maxwell + Written by Jean-Marc Valin and Gregory Maxwell */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#define CELT_ENCODER_C + +#include "cpu_support.h" +#include "os_support.h" +#include "mdct.h" +#include +#include "celt.h" +#include "pitch.h" +#include "bands.h" +#include "modes.h" +#include "entcode.h" +#include "quant_bands.h" +#include "rate.h" +#include "stack_alloc.h" +#include "mathops.h" +#include "float_cast.h" +#include +#include "celt_lpc.h" +#include "vq.h" + + +/** Encoder state + @brief Encoder state + */ +struct OpusCustomEncoder { + const OpusCustomMode *mode; /**< Mode used by the encoder */ + int channels; + int stream_channels; + + int force_intra; + int clip; + int disable_pf; + int complexity; + int upsample; + int start, end; + + opus_int32 bitrate; + int vbr; + int signalling; + int constrained_vbr; /* If zero, VBR can do whatever it likes with the rate */ + int loss_rate; + int lsb_depth; + int lfe; + int disable_inv; + int arch; + + /* Everything beyond this point gets cleared on a reset */ +#define ENCODER_RESET_START rng + + opus_uint32 rng; + int spread_decision; + opus_val32 delayedIntra; + int tonal_average; + int lastCodedBands; + int hf_average; + int tapset_decision; + + int prefilter_period; + opus_val16 prefilter_gain; + int prefilter_tapset; +#ifdef RESYNTH + int prefilter_period_old; + opus_val16 prefilter_gain_old; + int prefilter_tapset_old; +#endif + int consec_transient; + AnalysisInfo analysis; + SILKInfo silk_info; + + opus_val32 preemph_memE[2]; + opus_val32 preemph_memD[2]; + + /* VBR-related parameters */ + opus_int32 vbr_reservoir; + opus_int32 vbr_drift; + opus_int32 vbr_offset; + opus_int32 vbr_count; + opus_val32 overlap_max; + opus_val16 stereo_saving; + int intensity; + opus_val16 *energy_mask; + opus_val16 spec_avg; + +#ifdef RESYNTH + /* +MAX_PERIOD/2 to make space for overlap */ + celt_sig syn_mem[2][2*MAX_PERIOD+MAX_PERIOD/2]; +#endif + + celt_sig in_mem[1]; /* Size = channels*mode->overlap */ + /* celt_sig prefilter_mem[], Size = channels*COMBFILTER_MAXPERIOD */ + /* opus_val16 oldBandE[], Size = channels*mode->nbEBands */ + /* opus_val16 oldLogE[], Size = channels*mode->nbEBands */ + /* opus_val16 oldLogE2[], Size = channels*mode->nbEBands */ + /* opus_val16 energyError[], Size = channels*mode->nbEBands */ +}; + +int celt_encoder_get_size(int channels) +{ + CELTMode *mode = opus_custom_mode_create(48000, 960, NULL); + return opus_custom_encoder_get_size(mode, channels); +} + +OPUS_CUSTOM_NOSTATIC int opus_custom_encoder_get_size(const CELTMode *mode, int channels) +{ + int size = sizeof(struct CELTEncoder) + + (channels*mode->overlap-1)*sizeof(celt_sig) /* celt_sig in_mem[channels*mode->overlap]; */ + + channels*COMBFILTER_MAXPERIOD*sizeof(celt_sig) /* celt_sig prefilter_mem[channels*COMBFILTER_MAXPERIOD]; */ + + 4*channels*mode->nbEBands*sizeof(opus_val16); /* opus_val16 oldBandE[channels*mode->nbEBands]; */ + /* opus_val16 oldLogE[channels*mode->nbEBands]; */ + /* opus_val16 oldLogE2[channels*mode->nbEBands]; */ + /* opus_val16 energyError[channels*mode->nbEBands]; */ + return size; +} + +#ifdef CUSTOM_MODES +CELTEncoder *opus_custom_encoder_create(const CELTMode *mode, int channels, int *error) +{ + int ret; + CELTEncoder *st = (CELTEncoder *)opus_alloc(opus_custom_encoder_get_size(mode, channels)); + /* init will handle the NULL case */ + ret = opus_custom_encoder_init(st, mode, channels); + if (ret != OPUS_OK) + { + opus_custom_encoder_destroy(st); + st = NULL; + } + if (error) + *error = ret; + return st; +} +#endif /* CUSTOM_MODES */ + +static int opus_custom_encoder_init_arch(CELTEncoder *st, const CELTMode *mode, + int channels, int arch) +{ + if (channels < 0 || channels > 2) + return OPUS_BAD_ARG; + + if (st==NULL || mode==NULL) + return OPUS_ALLOC_FAIL; + + OPUS_CLEAR((char*)st, opus_custom_encoder_get_size(mode, channels)); + + st->mode = mode; + st->stream_channels = st->channels = channels; + + st->upsample = 1; + st->start = 0; + st->end = st->mode->effEBands; + st->signalling = 1; + st->arch = arch; + + st->constrained_vbr = 1; + st->clip = 1; + + st->bitrate = OPUS_BITRATE_MAX; + st->vbr = 0; + st->force_intra = 0; + st->complexity = 5; + st->lsb_depth=24; + + opus_custom_encoder_ctl(st, OPUS_RESET_STATE); + + return OPUS_OK; +} + +#ifdef CUSTOM_MODES +int opus_custom_encoder_init(CELTEncoder *st, const CELTMode *mode, int channels) +{ + return opus_custom_encoder_init_arch(st, mode, channels, opus_select_arch()); +} +#endif + +int celt_encoder_init(CELTEncoder *st, opus_int32 sampling_rate, int channels, + int arch) +{ + int ret; + ret = opus_custom_encoder_init_arch(st, + opus_custom_mode_create(48000, 960, NULL), channels, arch); + if (ret != OPUS_OK) + return ret; + st->upsample = resampling_factor(sampling_rate); + return OPUS_OK; +} + +#ifdef CUSTOM_MODES +void opus_custom_encoder_destroy(CELTEncoder *st) +{ + opus_free(st); +} +#endif /* CUSTOM_MODES */ + + +static int transient_analysis(const opus_val32 * OPUS_RESTRICT in, int len, int C, + opus_val16 *tf_estimate, int *tf_chan, int allow_weak_transients, + int *weak_transient) +{ + int i; + VARDECL(opus_val16, tmp); + opus_val32 mem0,mem1; + int is_transient = 0; + opus_int32 mask_metric = 0; + int c; + opus_val16 tf_max; + int len2; + /* Forward masking: 6.7 dB/ms. */ +#ifdef FIXED_POINT + int forward_shift = 4; +#else + opus_val16 forward_decay = QCONST16(.0625f,15); +#endif + /* Table of 6*64/x, trained on real data to minimize the average error */ + static const unsigned char inv_table[128] = { + 255,255,156,110, 86, 70, 59, 51, 45, 40, 37, 33, 31, 28, 26, 25, + 23, 22, 21, 20, 19, 18, 17, 16, 16, 15, 15, 14, 13, 13, 12, 12, + 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 9, 9, 9, 8, 8, + 8, 8, 8, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, + }; + SAVE_STACK; + ALLOC(tmp, len, opus_val16); + + *weak_transient = 0; + /* For lower bitrates, let's be more conservative and have a forward masking + decay of 3.3 dB/ms. This avoids having to code transients at very low + bitrate (mostly for hybrid), which can result in unstable energy and/or + partial collapse. */ + if (allow_weak_transients) + { +#ifdef FIXED_POINT + forward_shift = 5; +#else + forward_decay = QCONST16(.03125f,15); +#endif + } + len2=len/2; + for (c=0;c=0;i--) + { + /* Backward masking: 13.9 dB/ms. */ +#ifdef FIXED_POINT + /* FIXME: Use PSHR16() instead */ + tmp[i] = mem0 + PSHR32(tmp[i]-mem0,3); + mem0 = tmp[i]; + maxE = MAX16(maxE, mem0); +#else + mem0 = tmp[i] + 0.875f*mem0; + tmp[i] = 0.125f*mem0; + maxE = MAX16(maxE, 0.125f*mem0); +#endif + } + /*for (i=0;i>1))); +#else + mean = celt_sqrt(mean * maxE*.5*len2); +#endif + /* Inverse of the mean energy in Q15+6 */ + norm = SHL32(EXTEND32(len2),6+14)/ADD32(EPSILON,SHR32(mean,1)); + /* Compute harmonic mean discarding the unreliable boundaries + The data is smooth, so we only take 1/4th of the samples */ + unmask=0; + /* We should never see NaNs here. If we find any, then something really bad happened and we better abort + before it does any damage later on. If these asserts are disabled (no hardening), then the table + lookup a few lines below (id = ...) is likely to crash dur to an out-of-bounds read. DO NOT FIX + that crash on NaN since it could result in a worse issue later on. */ + celt_assert(!celt_isnan(tmp[0])); + celt_assert(!celt_isnan(norm)); + for (i=12;imask_metric) + { + *tf_chan = c; + mask_metric = unmask; + } + } + is_transient = mask_metric>200; + /* For low bitrates, define "weak transients" that need to be + handled differently to avoid partial collapse. */ + if (allow_weak_transients && is_transient && mask_metric<600) { + is_transient = 0; + *weak_transient = 1; + } + /* Arbitrary metric for VBR boost */ + tf_max = MAX16(0,celt_sqrt(27*mask_metric)-42); + /* *tf_estimate = 1 + MIN16(1, sqrt(MAX16(0, tf_max-30))/20); */ + *tf_estimate = celt_sqrt(MAX32(0, SHL32(MULT16_16(QCONST16(0.0069,14),MIN16(163,tf_max)),14)-QCONST32(0.139,28))); + /*printf("%d %f\n", tf_max, mask_metric);*/ + RESTORE_STACK; +#ifdef FUZZING + is_transient = rand()&0x1; +#endif + /*printf("%d %f %d\n", is_transient, (float)*tf_estimate, tf_max);*/ + return is_transient; +} + +/* Looks for sudden increases of energy to decide whether we need to patch + the transient decision */ +static int patch_transient_decision(opus_val16 *newE, opus_val16 *oldE, int nbEBands, + int start, int end, int C) +{ + int i, c; + opus_val32 mean_diff=0; + opus_val16 spread_old[26]; + /* Apply an aggressive (-6 dB/Bark) spreading function to the old frame to + avoid false detection caused by irrelevant bands */ + if (C==1) + { + spread_old[start] = oldE[start]; + for (i=start+1;i=start;i--) + spread_old[i] = MAX16(spread_old[i], spread_old[i+1]-QCONST16(1.0f, DB_SHIFT)); + /* Compute mean increase */ + c=0; do { + for (i=IMAX(2,start);i QCONST16(1.f, DB_SHIFT); +} + +/** Apply window and compute the MDCT for all sub-frames and + all channels in a frame */ +static void compute_mdcts(const CELTMode *mode, int shortBlocks, celt_sig * OPUS_RESTRICT in, + celt_sig * OPUS_RESTRICT out, int C, int CC, int LM, int upsample, + int arch) +{ + const int overlap = mode->overlap; + int N; + int B; + int shift; + int i, b, c; + if (shortBlocks) + { + B = shortBlocks; + N = mode->shortMdctSize; + shift = mode->maxLM; + } else { + B = 1; + N = mode->shortMdctSize<maxLM-LM; + } + c=0; do { + for (b=0;bmdct, in+c*(B*N+overlap)+b*N, + &out[b+c*N*B], mode->window, overlap, shift, B, + arch); + } + } while (++ceBands[len]-m->eBands[len-1])<eBands[len]-m->eBands[len-1])<eBands[i+1]-m->eBands[i])<eBands[i+1]-m->eBands[i])==1; + OPUS_COPY(tmp, &X[tf_chan*N0 + (m->eBands[i]<eBands[i]<>LM, 1<>k, 1<=0;i--) + { + if (tf_res[i+1] == 1) + tf_res[i] = path1[i+1]; + else + tf_res[i] = path0[i+1]; + } + /*printf("%d %f\n", *tf_sum, tf_estimate);*/ + RESTORE_STACK; +#ifdef FUZZING + tf_select = rand()&0x1; + tf_res[0] = rand()&0x1; + for (i=1;istorage*8; + tell = ec_tell(enc); + logp = isTransient ? 2 : 4; + /* Reserve space to code the tf_select decision. */ + tf_select_rsv = LM>0 && tell+logp+1 <= budget; + budget -= tf_select_rsv; + curr = tf_changed = 0; + for (i=start;i> 10; + trim = QCONST16(4.f, 8) + QCONST16(1.f/16.f, 8)*frac; + } + if (C==2) + { + opus_val16 sum = 0; /* Q10 */ + opus_val16 minXC; /* Q10 */ + /* Compute inter-channel correlation for low frequencies */ + for (i=0;i<8;i++) + { + opus_val32 partial; + partial = celt_inner_prod(&X[m->eBands[i]<eBands[i]<eBands[i+1]-m->eBands[i])<eBands[i]<eBands[i]<eBands[i+1]-m->eBands[i])<nbEBands]*(opus_int32)(2+2*i-end); + } + } while (++cvalid) + { + trim -= MAX16(-QCONST16(2.f, 8), MIN16(QCONST16(2.f, 8), + (opus_val16)(QCONST16(2.f, 8)*(analysis->tonality_slope+.05f)))); + } +#else + (void)analysis; +#endif + +#ifdef FIXED_POINT + trim_index = PSHR32(trim, 8); +#else + trim_index = (int)floor(.5f+trim); +#endif + trim_index = IMAX(0, IMIN(10, trim_index)); + /*printf("%d\n", trim_index);*/ +#ifdef FUZZING + trim_index = rand()%11; +#endif + return trim_index; +} + +static int stereo_analysis(const CELTMode *m, const celt_norm *X, + int LM, int N0) +{ + int i; + int thetas; + opus_val32 sumLR = EPSILON, sumMS = EPSILON; + + /* Use the L1 norm to model the entropy of the L/R signal vs the M/S signal */ + for (i=0;i<13;i++) + { + int j; + for (j=m->eBands[i]<eBands[i+1]<eBands[13]<<(LM+1))+thetas, sumMS) + > MULT16_32_Q15(m->eBands[13]<<(LM+1), sumLR); +} + +#define MSWAP(a,b) do {opus_val16 tmp = a;a=b;b=tmp;} while(0) +static opus_val16 median_of_5(const opus_val16 *x) +{ + opus_val16 t0, t1, t2, t3, t4; + t2 = x[2]; + if (x[0] > x[1]) + { + t0 = x[1]; + t1 = x[0]; + } else { + t0 = x[0]; + t1 = x[1]; + } + if (x[3] > x[4]) + { + t3 = x[4]; + t4 = x[3]; + } else { + t3 = x[3]; + t4 = x[4]; + } + if (t0 > t3) + { + MSWAP(t0, t3); + MSWAP(t1, t4); + } + if (t2 > t1) + { + if (t1 < t3) + return MIN16(t2, t3); + else + return MIN16(t4, t1); + } else { + if (t2 < t3) + return MIN16(t1, t3); + else + return MIN16(t2, t4); + } +} + +static opus_val16 median_of_3(const opus_val16 *x) +{ + opus_val16 t0, t1, t2; + if (x[0] > x[1]) + { + t0 = x[1]; + t1 = x[0]; + } else { + t0 = x[0]; + t1 = x[1]; + } + t2 = x[2]; + if (t1 < t2) + return t1; + else if (t0 < t2) + return t2; + else + return t0; +} + +static opus_val16 dynalloc_analysis(const opus_val16 *bandLogE, const opus_val16 *bandLogE2, const opus_val16 *oldBandE, + int nbEBands, int start, int end, int C, int *offsets, int lsb_depth, const opus_int16 *logN, + int isTransient, int vbr, int constrained_vbr, const opus_int16 *eBands, int LM, + int effectiveBytes, opus_int32 *tot_boost_, int lfe, opus_val16 *surround_dynalloc, + AnalysisInfo *analysis, int *importance, int *spread_weight) +{ + int i, c; + opus_int32 tot_boost=0; + opus_val16 maxDepth; + VARDECL(opus_val16, follower); + VARDECL(opus_val16, noise_floor); + VARDECL(opus_val16, bandLogE3); + SAVE_STACK; + ALLOC(follower, C*nbEBands, opus_val16); + ALLOC(noise_floor, C*nbEBands, opus_val16); + ALLOC(bandLogE3, nbEBands, opus_val16); + OPUS_CLEAR(offsets, nbEBands); + /* Dynamic allocation code */ + maxDepth=-QCONST16(31.9f, DB_SHIFT); + for (i=0;i=0;i--) + mask[i] = MAX16(mask[i], mask[i+1] - QCONST16(3.f, DB_SHIFT)); + for (i=0;i> shift; + } + /*for (i=0;i= (30 + 5*LM) && !lfe) + { + int last=0; + c=0;do + { + opus_val16 offset; + opus_val16 tmp; + opus_val16 *f; + OPUS_COPY(bandLogE3, &bandLogE2[c*nbEBands], end); + if (LM==0) { + /* For 2.5 ms frames, the first 8 bands have just one bin, so the + energy is highly unreliable (high variance). For that reason, + we take the max with the previous energy so that at least 2 bins + are getting used. */ + for (i=0;i bandLogE3[i-1]+QCONST16(.5f,DB_SHIFT)) + last=i; + f[i] = MIN16(f[i-1]+QCONST16(1.5f,DB_SHIFT), bandLogE3[i]); + } + for (i=last-1;i>=0;i--) + f[i] = MIN16(f[i], MIN16(f[i+1]+QCONST16(2.f,DB_SHIFT), bandLogE3[i])); + + /* Combine with a median filter to avoid dynalloc triggering unnecessarily. + The "offset" value controls how conservative we are -- a higher offset + reduces the impact of the median filter and makes dynalloc use more bits. */ + offset = QCONST16(1.f, DB_SHIFT); + for (i=2;i=12) + follower[i] = HALF16(follower[i]); + } +#ifdef DISABLE_FLOAT_API + (void)analysis; +#else + if (analysis->valid) + { + for (i=start;ileak_boost[i]; + } +#endif + for (i=start;i 48) { + boost = (int)SHR32(EXTEND32(follower[i])*8,DB_SHIFT); + boost_bits = (boost*width<>BITRES>>3 > 2*effectiveBytes/3) + { + opus_int32 cap = ((2*effectiveBytes/3)<mode; + overlap = mode->overlap; + ALLOC(_pre, CC*(N+COMBFILTER_MAXPERIOD), celt_sig); + + pre[0] = _pre; + pre[1] = _pre + (N+COMBFILTER_MAXPERIOD); + + + c=0; do { + OPUS_COPY(pre[c], prefilter_mem+c*COMBFILTER_MAXPERIOD, COMBFILTER_MAXPERIOD); + OPUS_COPY(pre[c]+COMBFILTER_MAXPERIOD, in+c*(N+overlap)+overlap, N); + } while (++c>1, opus_val16); + + pitch_downsample(pre, pitch_buf, COMBFILTER_MAXPERIOD+N, CC, st->arch); + /* Don't search for the fir last 1.5 octave of the range because + there's too many false-positives due to short-term correlation */ + pitch_search(pitch_buf+(COMBFILTER_MAXPERIOD>>1), pitch_buf, N, + COMBFILTER_MAXPERIOD-3*COMBFILTER_MINPERIOD, &pitch_index, + st->arch); + pitch_index = COMBFILTER_MAXPERIOD-pitch_index; + + gain1 = remove_doubling(pitch_buf, COMBFILTER_MAXPERIOD, COMBFILTER_MINPERIOD, + N, &pitch_index, st->prefilter_period, st->prefilter_gain, st->arch); + if (pitch_index > COMBFILTER_MAXPERIOD-2) + pitch_index = COMBFILTER_MAXPERIOD-2; + gain1 = MULT16_16_Q15(QCONST16(.7f,15),gain1); + /*printf("%d %d %f %f\n", pitch_change, pitch_index, gain1, st->analysis.tonality);*/ + if (st->loss_rate>2) + gain1 = HALF32(gain1); + if (st->loss_rate>4) + gain1 = HALF32(gain1); + if (st->loss_rate>8) + gain1 = 0; + } else { + gain1 = 0; + pitch_index = COMBFILTER_MINPERIOD; + } +#ifndef DISABLE_FLOAT_API + if (analysis->valid) + gain1 = (opus_val16)(gain1 * analysis->max_pitch_ratio); +#else + (void)analysis; +#endif + /* Gain threshold for enabling the prefilter/postfilter */ + pf_threshold = QCONST16(.2f,15); + + /* Adjusting the threshold based on rate and continuity */ + if (abs(pitch_index-st->prefilter_period)*10>pitch_index) + pf_threshold += QCONST16(.2f,15); + if (nbAvailableBytes<25) + pf_threshold += QCONST16(.1f,15); + if (nbAvailableBytes<35) + pf_threshold += QCONST16(.1f,15); + if (st->prefilter_gain > QCONST16(.4f,15)) + pf_threshold -= QCONST16(.1f,15); + if (st->prefilter_gain > QCONST16(.55f,15)) + pf_threshold -= QCONST16(.1f,15); + + /* Hard threshold at 0.2 */ + pf_threshold = MAX16(pf_threshold, QCONST16(.2f,15)); + if (gain1prefilter_gain)prefilter_gain; + +#ifdef FIXED_POINT + qg = ((gain1+1536)>>10)/3-1; +#else + qg = (int)floor(.5f+gain1*32/3)-1; +#endif + qg = IMAX(0, IMIN(7, qg)); + gain1 = QCONST16(0.09375f,15)*(qg+1); + pf_on = 1; + } + /*printf("%d %f\n", pitch_index, gain1);*/ + + c=0; do { + int offset = mode->shortMdctSize-overlap; + st->prefilter_period=IMAX(st->prefilter_period, COMBFILTER_MINPERIOD); + OPUS_COPY(in+c*(N+overlap), st->in_mem+c*(overlap), overlap); + if (offset) + comb_filter(in+c*(N+overlap)+overlap, pre[c]+COMBFILTER_MAXPERIOD, + st->prefilter_period, st->prefilter_period, offset, -st->prefilter_gain, -st->prefilter_gain, + st->prefilter_tapset, st->prefilter_tapset, NULL, 0, st->arch); + + comb_filter(in+c*(N+overlap)+overlap+offset, pre[c]+COMBFILTER_MAXPERIOD+offset, + st->prefilter_period, pitch_index, N-offset, -st->prefilter_gain, -gain1, + st->prefilter_tapset, prefilter_tapset, mode->window, overlap, st->arch); + OPUS_COPY(st->in_mem+c*(overlap), in+c*(N+overlap)+N, overlap); + + if (N>COMBFILTER_MAXPERIOD) + { + OPUS_COPY(prefilter_mem+c*COMBFILTER_MAXPERIOD, pre[c]+N, COMBFILTER_MAXPERIOD); + } else { + OPUS_MOVE(prefilter_mem+c*COMBFILTER_MAXPERIOD, prefilter_mem+c*COMBFILTER_MAXPERIOD+N, COMBFILTER_MAXPERIOD-N); + OPUS_COPY(prefilter_mem+c*COMBFILTER_MAXPERIOD+COMBFILTER_MAXPERIOD-N, pre[c]+COMBFILTER_MAXPERIOD, N); + } + } while (++cnbEBands; + eBands = mode->eBands; + + coded_bands = lastCodedBands ? lastCodedBands : nbEBands; + coded_bins = eBands[coded_bands]<analysis.activity, st->analysis.tonality, tf_estimate, st->stereo_saving, tot_boost, coded_bands);*/ +#ifndef DISABLE_FLOAT_API + if (analysis->valid && analysis->activity<.4) + target -= (opus_int32)((coded_bins<activity)); +#endif + /* Stereo savings */ + if (C==2) + { + int coded_stereo_bands; + int coded_stereo_dof; + opus_val16 max_frac; + coded_stereo_bands = IMIN(intensity, coded_bands); + coded_stereo_dof = (eBands[coded_stereo_bands]<valid && !lfe) + { + opus_int32 tonal_target; + float tonal; + + /* Tonality boost (compensating for the average). */ + tonal = MAX16(0.f,analysis->tonality-.15f)-0.12f; + tonal_target = target + (opus_int32)((coded_bins<tonality, tonal);*/ + target = tonal_target; + } +#else + (void)analysis; + (void)pitch_change; +#endif + + if (has_surround_mask&&!lfe) + { + opus_int32 surround_target = target + (opus_int32)SHR32(MULT16_16(surround_masking,coded_bins<end, st->intensity, surround_target, target, st->bitrate);*/ + target = IMAX(target/4, surround_target); + } + + { + opus_int32 floor_depth; + int bins; + bins = eBands[nbEBands-2]<>2); + target = IMIN(target, floor_depth); + /*printf("%f %d\n", maxDepth, floor_depth);*/ + } + + /* Make VBR less aggressive for constrained VBR because we can't keep a higher bitrate + for long. Needs tuning. */ + if ((!has_surround_mask||lfe) && constrained_vbr) + { + target = base_target + (opus_int32)MULT16_32_Q15(QCONST16(0.67f, 15), target-base_target); + } + + if (!has_surround_mask && tf_estimate < QCONST16(.2f, 14)) + { + opus_val16 amount; + opus_val16 tvbr_factor; + amount = MULT16_16_Q15(QCONST16(.0000031f, 30), IMAX(0, IMIN(32000, 96000-bitrate))); + tvbr_factor = SHR32(MULT16_16(temporal_vbr, amount), DB_SHIFT); + target += (opus_int32)MULT16_32_Q15(tvbr_factor, target); + } + + /* Don't allow more than doubling the rate */ + target = IMIN(2*base_target, target); + + return target; +} + +int celt_encode_with_ec(CELTEncoder * OPUS_RESTRICT st, const opus_val16 * pcm, int frame_size, unsigned char *compressed, int nbCompressedBytes, ec_enc *enc) +{ + int i, c, N; + opus_int32 bits; + ec_enc _enc; + VARDECL(celt_sig, in); + VARDECL(celt_sig, freq); + VARDECL(celt_norm, X); + VARDECL(celt_ener, bandE); + VARDECL(opus_val16, bandLogE); + VARDECL(opus_val16, bandLogE2); + VARDECL(int, fine_quant); + VARDECL(opus_val16, error); + VARDECL(int, pulses); + VARDECL(int, cap); + VARDECL(int, offsets); + VARDECL(int, importance); + VARDECL(int, spread_weight); + VARDECL(int, fine_priority); + VARDECL(int, tf_res); + VARDECL(unsigned char, collapse_masks); + celt_sig *prefilter_mem; + opus_val16 *oldBandE, *oldLogE, *oldLogE2, *energyError; + int shortBlocks=0; + int isTransient=0; + const int CC = st->channels; + const int C = st->stream_channels; + int LM, M; + int tf_select; + int nbFilledBytes, nbAvailableBytes; + int start; + int end; + int effEnd; + int codedBands; + int alloc_trim; + int pitch_index=COMBFILTER_MINPERIOD; + opus_val16 gain1 = 0; + int dual_stereo=0; + int effectiveBytes; + int dynalloc_logp; + opus_int32 vbr_rate; + opus_int32 total_bits; + opus_int32 total_boost; + opus_int32 balance; + opus_int32 tell; + opus_int32 tell0_frac; + int prefilter_tapset=0; + int pf_on; + int anti_collapse_rsv; + int anti_collapse_on=0; + int silence=0; + int tf_chan = 0; + opus_val16 tf_estimate; + int pitch_change=0; + opus_int32 tot_boost; + opus_val32 sample_max; + opus_val16 maxDepth; + const OpusCustomMode *mode; + int nbEBands; + int overlap; + const opus_int16 *eBands; + int secondMdct; + int signalBandwidth; + int transient_got_disabled=0; + opus_val16 surround_masking=0; + opus_val16 temporal_vbr=0; + opus_val16 surround_trim = 0; + opus_int32 equiv_rate; + int hybrid; + int weak_transient = 0; + int enable_tf_analysis; + VARDECL(opus_val16, surround_dynalloc); + ALLOC_STACK; + + mode = st->mode; + nbEBands = mode->nbEBands; + overlap = mode->overlap; + eBands = mode->eBands; + start = st->start; + end = st->end; + hybrid = start != 0; + tf_estimate = 0; + if (nbCompressedBytes<2 || pcm==NULL) + { + RESTORE_STACK; + return OPUS_BAD_ARG; + } + + frame_size *= st->upsample; + for (LM=0;LM<=mode->maxLM;LM++) + if (mode->shortMdctSize<mode->maxLM) + { + RESTORE_STACK; + return OPUS_BAD_ARG; + } + M=1<shortMdctSize; + + prefilter_mem = st->in_mem+CC*(overlap); + oldBandE = (opus_val16*)(st->in_mem+CC*(overlap+COMBFILTER_MAXPERIOD)); + oldLogE = oldBandE + CC*nbEBands; + oldLogE2 = oldLogE + CC*nbEBands; + energyError = oldLogE2 + CC*nbEBands; + + if (enc==NULL) + { + tell0_frac=tell=1; + nbFilledBytes=0; + } else { + tell0_frac=ec_tell_frac(enc); + tell=ec_tell(enc); + nbFilledBytes=(tell+4)>>3; + } + +#ifdef CUSTOM_MODES + if (st->signalling && enc==NULL) + { + int tmp = (mode->effEBands-end)>>1; + end = st->end = IMAX(1, mode->effEBands-tmp); + compressed[0] = tmp<<5; + compressed[0] |= LM<<3; + compressed[0] |= (C==2)<<2; + /* Convert "standard mode" to Opus header */ + if (mode->Fs==48000 && mode->shortMdctSize==120) + { + int c0 = toOpus(compressed[0]); + if (c0<0) + { + RESTORE_STACK; + return OPUS_BAD_ARG; + } + compressed[0] = c0; + } + compressed++; + nbCompressedBytes--; + } +#else + celt_assert(st->signalling==0); +#endif + + /* Can't produce more than 1275 output bytes */ + nbCompressedBytes = IMIN(nbCompressedBytes,1275); + nbAvailableBytes = nbCompressedBytes - nbFilledBytes; + + if (st->vbr && st->bitrate!=OPUS_BITRATE_MAX) + { + opus_int32 den=mode->Fs>>BITRES; + vbr_rate=(st->bitrate*frame_size+(den>>1))/den; +#ifdef CUSTOM_MODES + if (st->signalling) + vbr_rate -= 8<>(3+BITRES); + } else { + opus_int32 tmp; + vbr_rate = 0; + tmp = st->bitrate*frame_size; + if (tell>1) + tmp += tell*mode->Fs; + if (st->bitrate!=OPUS_BITRATE_MAX) + { + nbCompressedBytes = IMAX(2, IMIN(nbCompressedBytes, + (tmp+4*mode->Fs)/(8*mode->Fs)-!!st->signalling)); + ec_enc_shrink(enc, nbCompressedBytes); + } + effectiveBytes = nbCompressedBytes - nbFilledBytes; + } + equiv_rate = ((opus_int32)nbCompressedBytes*8*50 << (3-LM)) - (40*C+20)*((400>>LM) - 50); + if (st->bitrate != OPUS_BITRATE_MAX) + equiv_rate = IMIN(equiv_rate, st->bitrate - (40*C+20)*((400>>LM) - 50)); + + if (enc==NULL) + { + ec_enc_init(&_enc, compressed, nbCompressedBytes); + enc = &_enc; + } + + if (vbr_rate>0) + { + /* Computes the max bit-rate allowed in VBR mode to avoid violating the + target rate and buffering. + We must do this up front so that bust-prevention logic triggers + correctly if we don't have enough bits. */ + if (st->constrained_vbr) + { + opus_int32 vbr_bound; + opus_int32 max_allowed; + /* We could use any multiple of vbr_rate as bound (depending on the + delay). + This is clamped to ensure we use at least two bytes if the encoder + was entirely empty, but to allow 0 in hybrid mode. */ + vbr_bound = vbr_rate; + max_allowed = IMIN(IMAX(tell==1?2:0, + (vbr_rate+vbr_bound-st->vbr_reservoir)>>(BITRES+3)), + nbAvailableBytes); + if(max_allowed < nbAvailableBytes) + { + nbCompressedBytes = nbFilledBytes+max_allowed; + nbAvailableBytes = max_allowed; + ec_enc_shrink(enc, nbCompressedBytes); + } + } + } + total_bits = nbCompressedBytes*8; + + effEnd = end; + if (effEnd > mode->effEBands) + effEnd = mode->effEBands; + + ALLOC(in, CC*(N+overlap), celt_sig); + + sample_max=MAX32(st->overlap_max, celt_maxabs16(pcm, C*(N-overlap)/st->upsample)); + st->overlap_max=celt_maxabs16(pcm+C*(N-overlap)/st->upsample, C*overlap/st->upsample); + sample_max=MAX32(sample_max, st->overlap_max); +#ifdef FIXED_POINT + silence = (sample_max==0); +#else + silence = (sample_max <= (opus_val16)1/(1<lsb_depth)); +#endif +#ifdef FUZZING + if ((rand()&0x3F)==0) + silence = 1; +#endif + if (tell==1) + ec_enc_bit_logp(enc, silence, 15); + else + silence=0; + if (silence) + { + /*In VBR mode there is no need to send more than the minimum. */ + if (vbr_rate>0) + { + effectiveBytes=nbCompressedBytes=IMIN(nbCompressedBytes, nbFilledBytes+2); + total_bits=nbCompressedBytes*8; + nbAvailableBytes=2; + ec_enc_shrink(enc, nbCompressedBytes); + } + /* Pretend we've filled all the remaining bits with zeros + (that's what the initialiser did anyway) */ + tell = nbCompressedBytes*8; + enc->nbits_total+=tell-ec_tell(enc); + } + c=0; do { + int need_clip=0; +#ifndef FIXED_POINT + need_clip = st->clip && sample_max>65536.f; +#endif + celt_preemphasis(pcm+c, in+c*(N+overlap)+overlap, N, CC, st->upsample, + mode->preemph, st->preemph_memE+c, need_clip); + } while (++clfe&&nbAvailableBytes>3) || nbAvailableBytes>12*C) && !hybrid && !silence && !st->disable_pf + && st->complexity >= 5; + + prefilter_tapset = st->tapset_decision; + pf_on = run_prefilter(st, in, prefilter_mem, CC, N, prefilter_tapset, &pitch_index, &gain1, &qg, enabled, nbAvailableBytes, &st->analysis); + if ((gain1 > QCONST16(.4f,15) || st->prefilter_gain > QCONST16(.4f,15)) && (!st->analysis.valid || st->analysis.tonality > .3) + && (pitch_index > 1.26*st->prefilter_period || pitch_index < .79*st->prefilter_period)) + pitch_change = 1; + if (pf_on==0) + { + if(!hybrid && tell+16<=total_bits) + ec_enc_bit_logp(enc, 0, 1); + } else { + /*This block is not gated by a total bits check only because + of the nbAvailableBytes check above.*/ + int octave; + ec_enc_bit_logp(enc, 1, 1); + pitch_index += 1; + octave = EC_ILOG(pitch_index)-5; + ec_enc_uint(enc, octave, 6); + ec_enc_bits(enc, pitch_index-(16<complexity >= 1 && !st->lfe) + { + /* Reduces the likelihood of energy instability on fricatives at low bitrate + in hybrid mode. It seems like we still want to have real transients on vowels + though (small SILK quantization offset value). */ + int allow_weak_transients = hybrid && effectiveBytes<15 && st->silk_info.signalType != 2; + isTransient = transient_analysis(in, N+overlap, CC, + &tf_estimate, &tf_chan, allow_weak_transients, &weak_transient); + } + if (LM>0 && ec_tell(enc)+3<=total_bits) + { + if (isTransient) + shortBlocks = M; + } else { + isTransient = 0; + transient_got_disabled=1; + } + + ALLOC(freq, CC*N, celt_sig); /**< Interleaved signal MDCTs */ + ALLOC(bandE,nbEBands*CC, celt_ener); + ALLOC(bandLogE,nbEBands*CC, opus_val16); + + secondMdct = shortBlocks && st->complexity>=8; + ALLOC(bandLogE2, C*nbEBands, opus_val16); + if (secondMdct) + { + compute_mdcts(mode, 0, in, freq, C, CC, LM, st->upsample, st->arch); + compute_band_energies(mode, freq, bandE, effEnd, C, LM, st->arch); + amp2Log2(mode, effEnd, end, bandE, bandLogE2, C); + for (c=0;cupsample, st->arch); + /* This should catch any NaN in the CELT input. Since we're not supposed to see any (they're filtered + at the Opus layer), just abort. */ + celt_assert(!celt_isnan(freq[0]) && (C==1 || !celt_isnan(freq[N]))); + if (CC==2&&C==1) + tf_chan = 0; + compute_band_energies(mode, freq, bandE, effEnd, C, LM, st->arch); + + if (st->lfe) + { + for (i=2;ienergy_mask&&!st->lfe) + { + int mask_end; + int midband; + int count_dynalloc; + opus_val32 mask_avg=0; + opus_val32 diff=0; + int count=0; + mask_end = IMAX(2,st->lastCodedBands); + for (c=0;cenergy_mask[nbEBands*c+i], + QCONST16(.25f, DB_SHIFT)), -QCONST16(2.0f, DB_SHIFT)); + if (mask > 0) + mask = HALF16(mask); + mask_avg += MULT16_16(mask, eBands[i+1]-eBands[i]); + count += eBands[i+1]-eBands[i]; + diff += MULT16_16(mask, 1+2*i-mask_end); + } + } + celt_assert(count>0); + mask_avg = DIV32_16(mask_avg,count); + mask_avg += QCONST16(.2f, DB_SHIFT); + diff = diff*6/(C*(mask_end-1)*(mask_end+1)*mask_end); + /* Again, being conservative */ + diff = HALF32(diff); + diff = MAX32(MIN32(diff, QCONST32(.031f, DB_SHIFT)), -QCONST32(.031f, DB_SHIFT)); + /* Find the band that's in the middle of the coded spectrum */ + for (midband=0;eBands[midband+1] < eBands[mask_end]/2;midband++); + count_dynalloc=0; + for(i=0;ienergy_mask[i], st->energy_mask[nbEBands+i]); + else + unmask = st->energy_mask[i]; + unmask = MIN16(unmask, QCONST16(.0f, DB_SHIFT)); + unmask -= lin; + if (unmask > QCONST16(.25f, DB_SHIFT)) + { + surround_dynalloc[i] = unmask - QCONST16(.25f, DB_SHIFT); + count_dynalloc++; + } + } + if (count_dynalloc>=3) + { + /* If we need dynalloc in many bands, it's probably because our + initial masking rate was too low. */ + mask_avg += QCONST16(.25f, DB_SHIFT); + if (mask_avg>0) + { + /* Something went really wrong in the original calculations, + disabling masking. */ + mask_avg = 0; + diff = 0; + OPUS_CLEAR(surround_dynalloc, mask_end); + } else { + for(i=0;ilfe) + { + opus_val16 follow=-QCONST16(10.0f,DB_SHIFT); + opus_val32 frame_avg=0; + opus_val16 offset = shortBlocks?HALF16(SHL16(LM, DB_SHIFT)):0; + for(i=start;ispec_avg); + temporal_vbr = MIN16(QCONST16(3.f, DB_SHIFT), MAX16(-QCONST16(1.5f, DB_SHIFT), temporal_vbr)); + st->spec_avg += MULT16_16_Q15(QCONST16(.02f, 15), temporal_vbr); + } + /*for (i=0;i<21;i++) + printf("%f ", bandLogE[i]); + printf("\n");*/ + + if (!secondMdct) + { + OPUS_COPY(bandLogE2, bandLogE, C*nbEBands); + } + + /* Last chance to catch any transient we might have missed in the + time-domain analysis */ + if (LM>0 && ec_tell(enc)+3<=total_bits && !isTransient && st->complexity>=5 && !st->lfe && !hybrid) + { + if (patch_transient_decision(bandLogE, oldBandE, nbEBands, start, end, C)) + { + isTransient = 1; + shortBlocks = M; + compute_mdcts(mode, shortBlocks, in, freq, C, CC, LM, st->upsample, st->arch); + compute_band_energies(mode, freq, bandE, effEnd, C, LM, st->arch); + amp2Log2(mode, effEnd, end, bandE, bandLogE, C); + /* Compensate for the scaling of short vs long mdcts */ + for (c=0;c0 && ec_tell(enc)+3<=total_bits) + ec_enc_bit_logp(enc, isTransient, 3); + + ALLOC(X, C*N, celt_norm); /**< Interleaved normalised MDCTs */ + + /* Band normalisation */ + normalise_bands(mode, freq, X, bandE, effEnd, C, M); + + enable_tf_analysis = effectiveBytes>=15*C && !hybrid && st->complexity>=2 && !st->lfe; + + ALLOC(offsets, nbEBands, int); + ALLOC(importance, nbEBands, int); + ALLOC(spread_weight, nbEBands, int); + + maxDepth = dynalloc_analysis(bandLogE, bandLogE2, oldBandE, nbEBands, start, end, C, offsets, + st->lsb_depth, mode->logN, isTransient, st->vbr, st->constrained_vbr, + eBands, LM, effectiveBytes, &tot_boost, st->lfe, surround_dynalloc, &st->analysis, importance, spread_weight); + + ALLOC(tf_res, nbEBands, int); + /* Disable variable tf resolution for hybrid and at very low bitrate */ + if (enable_tf_analysis) + { + int lambda; + lambda = IMAX(80, 20480/effectiveBytes + 2); + tf_select = tf_analysis(mode, effEnd, isTransient, tf_res, lambda, X, N, LM, tf_estimate, tf_chan, importance); + for (i=effEnd;isilk_info.signalType != 2) + { + /* For low bitrate hybrid, we force temporal resolution to 5 ms rather than 2.5 ms. */ + for (i=0;iforce_intra, + &st->delayedIntra, st->complexity >= 4, st->loss_rate, st->lfe); + + tf_encode(start, end, isTransient, tf_res, LM, tf_select, enc); + + if (ec_tell(enc)+4<=total_bits) + { + if (st->lfe) + { + st->tapset_decision = 0; + st->spread_decision = SPREAD_NORMAL; + } else if (hybrid) + { + if (st->complexity == 0) + st->spread_decision = SPREAD_NONE; + else if (isTransient) + st->spread_decision = SPREAD_NORMAL; + else + st->spread_decision = SPREAD_AGGRESSIVE; + } else if (shortBlocks || st->complexity < 3 || nbAvailableBytes < 10*C) + { + if (st->complexity == 0) + st->spread_decision = SPREAD_NONE; + else + st->spread_decision = SPREAD_NORMAL; + } else { + /* Disable new spreading+tapset estimator until we can show it works + better than the old one. So far it seems like spreading_decision() + works best. */ +#if 0 + if (st->analysis.valid) + { + static const opus_val16 spread_thresholds[3] = {-QCONST16(.6f, 15), -QCONST16(.2f, 15), -QCONST16(.07f, 15)}; + static const opus_val16 spread_histeresis[3] = {QCONST16(.15f, 15), QCONST16(.07f, 15), QCONST16(.02f, 15)}; + static const opus_val16 tapset_thresholds[2] = {QCONST16(.0f, 15), QCONST16(.15f, 15)}; + static const opus_val16 tapset_histeresis[2] = {QCONST16(.1f, 15), QCONST16(.05f, 15)}; + st->spread_decision = hysteresis_decision(-st->analysis.tonality, spread_thresholds, spread_histeresis, 3, st->spread_decision); + st->tapset_decision = hysteresis_decision(st->analysis.tonality_slope, tapset_thresholds, tapset_histeresis, 2, st->tapset_decision); + } else +#endif + { + st->spread_decision = spreading_decision(mode, X, + &st->tonal_average, st->spread_decision, &st->hf_average, + &st->tapset_decision, pf_on&&!shortBlocks, effEnd, C, M, spread_weight); + } + /*printf("%d %d\n", st->tapset_decision, st->spread_decision);*/ + /*printf("%f %d %f %d\n\n", st->analysis.tonality, st->spread_decision, st->analysis.tonality_slope, st->tapset_decision);*/ + } + ec_enc_icdf(enc, st->spread_decision, spread_icdf, 5); + } + + /* For LFE, everything interesting is in the first band */ + if (st->lfe) + offsets[0] = IMIN(8, effectiveBytes/3); + ALLOC(cap, nbEBands, int); + init_caps(mode,cap,LM,C); + + dynalloc_logp = 6; + total_bits<<=BITRES; + total_boost = 0; + tell = ec_tell_frac(enc); + for (i=start;iintensity = hysteresis_decision((opus_val16)(equiv_rate/1000), + intensity_thresholds, intensity_histeresis, 21, st->intensity); + st->intensity = IMIN(end,IMAX(start, st->intensity)); + } + + alloc_trim = 5; + if (tell+(6< 0 || st->lfe) + { + st->stereo_saving = 0; + alloc_trim = 5; + } else { + alloc_trim = alloc_trim_analysis(mode, X, bandLogE, + end, LM, C, N, &st->analysis, &st->stereo_saving, tf_estimate, + st->intensity, surround_trim, equiv_rate, st->arch); + } + ec_enc_icdf(enc, alloc_trim, trim_icdf, 7); + tell = ec_tell_frac(enc); + } + + /* Variable bitrate */ + if (vbr_rate>0) + { + opus_val16 alpha; + opus_int32 delta; + /* The target rate in 8th bits per frame */ + opus_int32 target, base_target; + opus_int32 min_allowed; + int lm_diff = mode->maxLM - LM; + + /* Don't attempt to use more than 510 kb/s, even for frames smaller than 20 ms. + The CELT allocator will just not be able to use more than that anyway. */ + nbCompressedBytes = IMIN(nbCompressedBytes,1275>>(3-LM)); + if (!hybrid) + { + base_target = vbr_rate - ((40*C+20)<constrained_vbr) + base_target += (st->vbr_offset>>lm_diff); + + if (!hybrid) + { + target = compute_vbr(mode, &st->analysis, base_target, LM, equiv_rate, + st->lastCodedBands, C, st->intensity, st->constrained_vbr, + st->stereo_saving, tot_boost, tf_estimate, pitch_change, maxDepth, + st->lfe, st->energy_mask!=NULL, surround_masking, + temporal_vbr); + } else { + target = base_target; + /* Tonal frames (offset<100) need more bits than noisy (offset>100) ones. */ + if (st->silk_info.offset < 100) target += 12 << BITRES >> (3-LM); + if (st->silk_info.offset > 100) target -= 18 << BITRES >> (3-LM); + /* Boosting bitrate on transients and vowels with significant temporal + spikes. */ + target += (opus_int32)MULT16_16_Q14(tf_estimate-QCONST16(.25f,14), (50< QCONST16(.7f,14)) + target = IMAX(target, 50<>(BITRES+3)) + 2; + /* Take into account the 37 bits we need to have left in the packet to + signal a redundant frame in hybrid mode. Creating a shorter packet would + create an entropy coder desync. */ + if (hybrid) + min_allowed = IMAX(min_allowed, (tell0_frac+(37<>(BITRES+3)); + + nbAvailableBytes = (target+(1<<(BITRES+2)))>>(BITRES+3); + nbAvailableBytes = IMAX(min_allowed,nbAvailableBytes); + nbAvailableBytes = IMIN(nbCompressedBytes,nbAvailableBytes); + + /* By how much did we "miss" the target on that frame */ + delta = target - vbr_rate; + + target=nbAvailableBytes<<(BITRES+3); + + /*If the frame is silent we don't adjust our drift, otherwise + the encoder will shoot to very high rates after hitting a + span of silence, but we do allow the bitres to refill. + This means that we'll undershoot our target in CVBR/VBR modes + on files with lots of silence. */ + if(silence) + { + nbAvailableBytes = 2; + target = 2*8<vbr_count < 970) + { + st->vbr_count++; + alpha = celt_rcp(SHL32(EXTEND32(st->vbr_count+20),16)); + } else + alpha = QCONST16(.001f,15); + /* How many bits have we used in excess of what we're allowed */ + if (st->constrained_vbr) + st->vbr_reservoir += target - vbr_rate; + /*printf ("%d\n", st->vbr_reservoir);*/ + + /* Compute the offset we need to apply in order to reach the target */ + if (st->constrained_vbr) + { + st->vbr_drift += (opus_int32)MULT16_32_Q15(alpha,(delta*(1<vbr_offset-st->vbr_drift); + st->vbr_offset = -st->vbr_drift; + } + /*printf ("%d\n", st->vbr_drift);*/ + + if (st->constrained_vbr && st->vbr_reservoir < 0) + { + /* We're under the min value -- increase rate */ + int adjust = (-st->vbr_reservoir)/(8<vbr_reservoir = 0; + /*printf ("+%d\n", adjust);*/ + } + nbCompressedBytes = IMIN(nbCompressedBytes,nbAvailableBytes); + /*printf("%d\n", nbCompressedBytes*50*8);*/ + /* This moves the raw bits to take into account the new compressed size */ + ec_enc_shrink(enc, nbCompressedBytes); + } + + /* Bit allocation */ + ALLOC(fine_quant, nbEBands, int); + ALLOC(pulses, nbEBands, int); + ALLOC(fine_priority, nbEBands, int); + + /* bits = packet size - where we are - safety*/ + bits = (((opus_int32)nbCompressedBytes*8)<=2&&bits>=((LM+2)<analysis.valid) + { + int min_bandwidth; + if (equiv_rate < (opus_int32)32000*C) + min_bandwidth = 13; + else if (equiv_rate < (opus_int32)48000*C) + min_bandwidth = 16; + else if (equiv_rate < (opus_int32)60000*C) + min_bandwidth = 18; + else if (equiv_rate < (opus_int32)80000*C) + min_bandwidth = 19; + else + min_bandwidth = 20; + signalBandwidth = IMAX(st->analysis.bandwidth, min_bandwidth); + } +#endif + if (st->lfe) + signalBandwidth = 1; + codedBands = clt_compute_allocation(mode, start, end, offsets, cap, + alloc_trim, &st->intensity, &dual_stereo, bits, &balance, pulses, + fine_quant, fine_priority, C, LM, enc, 1, st->lastCodedBands, signalBandwidth); + if (st->lastCodedBands) + st->lastCodedBands = IMIN(st->lastCodedBands+1,IMAX(st->lastCodedBands-1,codedBands)); + else + st->lastCodedBands = codedBands; + + quant_fine_energy(mode, start, end, oldBandE, error, fine_quant, enc, C); + + /* Residual quantisation */ + ALLOC(collapse_masks, C*nbEBands, unsigned char); + quant_all_bands(1, mode, start, end, X, C==2 ? X+N : NULL, collapse_masks, + bandE, pulses, shortBlocks, st->spread_decision, + dual_stereo, st->intensity, tf_res, nbCompressedBytes*(8<rng, st->complexity, st->arch, st->disable_inv); + + if (anti_collapse_rsv > 0) + { + anti_collapse_on = st->consec_transient<2; +#ifdef FUZZING + anti_collapse_on = rand()&0x1; +#endif + ec_enc_bits(enc, anti_collapse_on, 1); + } + quant_energy_finalise(mode, start, end, oldBandE, error, fine_quant, fine_priority, nbCompressedBytes*8-ec_tell(enc), enc, C); + OPUS_CLEAR(energyError, nbEBands*CC); + c=0; + do { + for (i=start;irng, st->arch); + } + + c=0; do { + OPUS_MOVE(st->syn_mem[c], st->syn_mem[c]+N, 2*MAX_PERIOD-N+overlap/2); + } while (++csyn_mem[c]+2*MAX_PERIOD-N; + } while (++cupsample, silence, st->arch); + + c=0; do { + st->prefilter_period=IMAX(st->prefilter_period, COMBFILTER_MINPERIOD); + st->prefilter_period_old=IMAX(st->prefilter_period_old, COMBFILTER_MINPERIOD); + comb_filter(out_mem[c], out_mem[c], st->prefilter_period_old, st->prefilter_period, mode->shortMdctSize, + st->prefilter_gain_old, st->prefilter_gain, st->prefilter_tapset_old, st->prefilter_tapset, + mode->window, overlap, st->arch); + if (LM!=0) + comb_filter(out_mem[c]+mode->shortMdctSize, out_mem[c]+mode->shortMdctSize, st->prefilter_period, pitch_index, N-mode->shortMdctSize, + st->prefilter_gain, gain1, st->prefilter_tapset, prefilter_tapset, + mode->window, overlap, st->arch); + } while (++cupsample, mode->preemph, st->preemph_memD, 0); + st->prefilter_period_old = st->prefilter_period; + st->prefilter_gain_old = st->prefilter_gain; + st->prefilter_tapset_old = st->prefilter_tapset; + } +#endif + + st->prefilter_period = pitch_index; + st->prefilter_gain = gain1; + st->prefilter_tapset = prefilter_tapset; +#ifdef RESYNTH + if (LM!=0) + { + st->prefilter_period_old = st->prefilter_period; + st->prefilter_gain_old = st->prefilter_gain; + st->prefilter_tapset_old = st->prefilter_tapset; + } +#endif + + if (CC==2&&C==1) { + OPUS_COPY(&oldBandE[nbEBands], oldBandE, nbEBands); + } + + if (!isTransient) + { + OPUS_COPY(oldLogE2, oldLogE, CC*nbEBands); + OPUS_COPY(oldLogE, oldBandE, CC*nbEBands); + } else { + for (i=0;iconsec_transient++; + else + st->consec_transient=0; + st->rng = enc->rng; + + /* If there's any room left (can only happen for very high rates), + it's already filled with zeros */ + ec_enc_done(enc); + +#ifdef CUSTOM_MODES + if (st->signalling) + nbCompressedBytes++; +#endif + + RESTORE_STACK; + if (ec_get_error(enc)) + return OPUS_INTERNAL_ERROR; + else + return nbCompressedBytes; +} + + +#ifdef CUSTOM_MODES + +#ifdef FIXED_POINT +int opus_custom_encode(CELTEncoder * OPUS_RESTRICT st, const opus_int16 * pcm, int frame_size, unsigned char *compressed, int nbCompressedBytes) +{ + return celt_encode_with_ec(st, pcm, frame_size, compressed, nbCompressedBytes, NULL); +} + +#ifndef DISABLE_FLOAT_API +int opus_custom_encode_float(CELTEncoder * OPUS_RESTRICT st, const float * pcm, int frame_size, unsigned char *compressed, int nbCompressedBytes) +{ + int j, ret, C, N; + VARDECL(opus_int16, in); + ALLOC_STACK; + + if (pcm==NULL) + return OPUS_BAD_ARG; + + C = st->channels; + N = frame_size; + ALLOC(in, C*N, opus_int16); + + for (j=0;jchannels; + N=frame_size; + ALLOC(in, C*N, celt_sig); + for (j=0;j10) + goto bad_arg; + st->complexity = value; + } + break; + case CELT_SET_START_BAND_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<0 || value>=st->mode->nbEBands) + goto bad_arg; + st->start = value; + } + break; + case CELT_SET_END_BAND_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<1 || value>st->mode->nbEBands) + goto bad_arg; + st->end = value; + } + break; + case CELT_SET_PREDICTION_REQUEST: + { + int value = va_arg(ap, opus_int32); + if (value<0 || value>2) + goto bad_arg; + st->disable_pf = value<=1; + st->force_intra = value==0; + } + break; + case OPUS_SET_PACKET_LOSS_PERC_REQUEST: + { + int value = va_arg(ap, opus_int32); + if (value<0 || value>100) + goto bad_arg; + st->loss_rate = value; + } + break; + case OPUS_SET_VBR_CONSTRAINT_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + st->constrained_vbr = value; + } + break; + case OPUS_SET_VBR_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + st->vbr = value; + } + break; + case OPUS_SET_BITRATE_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<=500 && value!=OPUS_BITRATE_MAX) + goto bad_arg; + value = IMIN(value, 260000*st->channels); + st->bitrate = value; + } + break; + case CELT_SET_CHANNELS_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<1 || value>2) + goto bad_arg; + st->stream_channels = value; + } + break; + case OPUS_SET_LSB_DEPTH_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<8 || value>24) + goto bad_arg; + st->lsb_depth=value; + } + break; + case OPUS_GET_LSB_DEPTH_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + *value=st->lsb_depth; + } + break; + case OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>1) + { + goto bad_arg; + } + st->disable_inv = value; + } + break; + case OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->disable_inv; + } + break; + case OPUS_RESET_STATE: + { + int i; + opus_val16 *oldBandE, *oldLogE, *oldLogE2; + oldBandE = (opus_val16*)(st->in_mem+st->channels*(st->mode->overlap+COMBFILTER_MAXPERIOD)); + oldLogE = oldBandE + st->channels*st->mode->nbEBands; + oldLogE2 = oldLogE + st->channels*st->mode->nbEBands; + OPUS_CLEAR((char*)&st->ENCODER_RESET_START, + opus_custom_encoder_get_size(st->mode, st->channels)- + ((char*)&st->ENCODER_RESET_START - (char*)st)); + for (i=0;ichannels*st->mode->nbEBands;i++) + oldLogE[i]=oldLogE2[i]=-QCONST16(28.f,DB_SHIFT); + st->vbr_offset = 0; + st->delayedIntra = 1; + st->spread_decision = SPREAD_NORMAL; + st->tonal_average = 256; + st->hf_average = 0; + st->tapset_decision = 0; + } + break; +#ifdef CUSTOM_MODES + case CELT_SET_INPUT_CLIPPING_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + st->clip = value; + } + break; +#endif + case CELT_SET_SIGNALLING_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + st->signalling = value; + } + break; + case CELT_SET_ANALYSIS_REQUEST: + { + AnalysisInfo *info = va_arg(ap, AnalysisInfo *); + if (info) + OPUS_COPY(&st->analysis, info, 1); + } + break; + case CELT_SET_SILK_INFO_REQUEST: + { + SILKInfo *info = va_arg(ap, SILKInfo *); + if (info) + OPUS_COPY(&st->silk_info, info, 1); + } + break; + case CELT_GET_MODE_REQUEST: + { + const CELTMode ** value = va_arg(ap, const CELTMode**); + if (value==0) + goto bad_arg; + *value=st->mode; + } + break; + case OPUS_GET_FINAL_RANGE_REQUEST: + { + opus_uint32 * value = va_arg(ap, opus_uint32 *); + if (value==0) + goto bad_arg; + *value=st->rng; + } + break; + case OPUS_SET_LFE_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + st->lfe = value; + } + break; + case OPUS_SET_ENERGY_MASK_REQUEST: + { + opus_val16 *value = va_arg(ap, opus_val16*); + st->energy_mask = value; + } + break; + default: + goto bad_request; + } + va_end(ap); + return OPUS_OK; +bad_arg: + va_end(ap); + return OPUS_BAD_ARG; +bad_request: + va_end(ap); + return OPUS_UNIMPLEMENTED; +} diff --git a/port/third_party/opus/celt/celt_lpc.c b/port/third_party/opus/celt/celt_lpc.c new file mode 100644 index 00000000..fabca65c --- /dev/null +++ b/port/third_party/opus/celt/celt_lpc.c @@ -0,0 +1,363 @@ +/* Copyright (c) 2009-2010 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "celt_lpc.h" +#include "stack_alloc.h" +#include "mathops.h" +#include "pitch.h" + +void _celt_lpc( + opus_val16 *_lpc, /* out: [0...p-1] LPC coefficients */ +const opus_val32 *ac, /* in: [0...p] autocorrelation values */ +int p +) +{ + int i, j; + opus_val32 r; + opus_val32 error = ac[0]; +#ifdef FIXED_POINT + opus_val32 lpc[CELT_LPC_ORDER]; +#else + float *lpc = _lpc; +#endif + + OPUS_CLEAR(lpc, p); +#ifdef FIXED_POINT + if (ac[0] != 0) +#else + if (ac[0] > 1e-10f) +#endif + { + for (i = 0; i < p; i++) { + /* Sum up this iteration's reflection coefficient */ + opus_val32 rr = 0; + for (j = 0; j < i; j++) + rr += MULT32_32_Q31(lpc[j],ac[i - j]); + rr += SHR32(ac[i + 1],6); + r = -frac_div32(SHL32(rr,6), error); + /* Update LPC coefficients and total error */ + lpc[i] = SHR32(r,6); + for (j = 0; j < (i+1)>>1; j++) + { + opus_val32 tmp1, tmp2; + tmp1 = lpc[j]; + tmp2 = lpc[i-1-j]; + lpc[j] = tmp1 + MULT32_32_Q31(r,tmp2); + lpc[i-1-j] = tmp2 + MULT32_32_Q31(r,tmp1); + } + + error = error - MULT32_32_Q31(MULT32_32_Q31(r,r),error); + /* Bail out once we get 30 dB gain */ +#ifdef FIXED_POINT + if (error<=SHR32(ac[0],10)) + break; +#else + if (error<=.001f*ac[0]) + break; +#endif + } + } +#ifdef FIXED_POINT + { + /* Convert the int32 lpcs to int16 and ensure there are no wrap-arounds. + This reuses the logic in silk_LPC_fit() and silk_bwexpander_32(). Any bug + fixes should also be applied there. */ + int iter, idx = 0; + opus_val32 maxabs, absval, chirp_Q16, chirp_minus_one_Q16; + + for (iter = 0; iter < 10; iter++) { + maxabs = 0; + for (i = 0; i < p; i++) { + absval = ABS32(lpc[i]); + if (absval > maxabs) { + maxabs = absval; + idx = i; + } + } + maxabs = PSHR32(maxabs, 13); /* Q25->Q12 */ + + if (maxabs > 32767) { + maxabs = MIN32(maxabs, 163838); + chirp_Q16 = QCONST32(0.999, 16) - DIV32(SHL32(maxabs - 32767, 14), + SHR32(MULT32_32_32(maxabs, idx + 1), 2)); + chirp_minus_one_Q16 = chirp_Q16 - 65536; + + /* Apply bandwidth expansion. */ + for (i = 0; i < p - 1; i++) { + lpc[i] = MULT32_32_Q16(chirp_Q16, lpc[i]); + chirp_Q16 += PSHR32(MULT32_32_32(chirp_Q16, chirp_minus_one_Q16), 16); + } + lpc[p - 1] = MULT32_32_Q16(chirp_Q16, lpc[p - 1]); + } else { + break; + } + } + + if (iter == 10) { + /* If the coeffs still do not fit into the 16 bit range after 10 iterations, + fall back to the A(z)=1 filter. */ + OPUS_CLEAR(lpc, p); + _lpc[0] = 4096; /* Q12 */ + } else { + for (i = 0; i < p; i++) { + _lpc[i] = EXTRACT16(PSHR32(lpc[i], 13)); /* Q25->Q12 */ + } + } + } +#endif +} + + +void celt_fir_c( + const opus_val16 *x, + const opus_val16 *num, + opus_val16 *y, + int N, + int ord, + int arch) +{ + int i,j; + VARDECL(opus_val16, rnum); + SAVE_STACK; + celt_assert(x != y); + ALLOC(rnum, ord, opus_val16); + for(i=0;i=1;j--) + { + mem[j]=mem[j-1]; + } + mem[0] = SROUND16(sum, SIG_SHIFT); + _y[i] = sum; + } +#else + int i,j; + VARDECL(opus_val16, rden); + VARDECL(opus_val16, y); + SAVE_STACK; + + celt_assert((ord&3)==0); + ALLOC(rden, ord, opus_val16); + ALLOC(y, N+ord, opus_val16); + for(i=0;i0); + celt_assert(overlap>=0); + if (overlap == 0) + { + xptr = x; + } else { + for (i=0;i0) + { + for(i=0;i= 536870912) + { + int shift2=1; + if (ac[0] >= 1073741824) + shift2++; + for (i=0;i<=lag;i++) + ac[i] = SHR32(ac[i], shift2); + shift += shift2; + } +#endif + + RESTORE_STACK; + return shift; +} diff --git a/port/third_party/opus/celt/celt_lpc.h b/port/third_party/opus/celt/celt_lpc.h new file mode 100644 index 00000000..97dee82f --- /dev/null +++ b/port/third_party/opus/celt/celt_lpc.h @@ -0,0 +1,66 @@ +/* Copyright (c) 2009-2010 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef PLC_H +#define PLC_H + +#include "arch.h" +#include "cpu_support.h" + +#if defined(OPUS_X86_MAY_HAVE_SSE4_1) +#include "x86/celt_lpc_sse.h" +#endif + +#define CELT_LPC_ORDER 24 + +void _celt_lpc(opus_val16 *_lpc, const opus_val32 *ac, int p); + +void celt_fir_c( + const opus_val16 *x, + const opus_val16 *num, + opus_val16 *y, + int N, + int ord, + int arch); + +#if !defined(OVERRIDE_CELT_FIR) +#define celt_fir(x, num, y, N, ord, arch) \ + (celt_fir_c(x, num, y, N, ord, arch)) +#endif + +void celt_iir(const opus_val32 *x, + const opus_val16 *den, + opus_val32 *y, + int N, + int ord, + opus_val16 *mem, + int arch); + +int _celt_autocorr(const opus_val16 *x, opus_val32 *ac, + const opus_val16 *window, int overlap, int lag, int n, int arch); + +#endif /* PLC_H */ diff --git a/port/third_party/opus/celt/cpu_support.h b/port/third_party/opus/celt/cpu_support.h new file mode 100644 index 00000000..9f13d8ae --- /dev/null +++ b/port/third_party/opus/celt/cpu_support.h @@ -0,0 +1,72 @@ +/* Copyright (c) 2010 Xiph.Org Foundation + * Copyright (c) 2013 Parrot */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef CPU_SUPPORT_H +#define CPU_SUPPORT_H + +#include "opus_types.h" +#include "opus_defines.h" + +#if defined(OPUS_HAVE_RTCD) && \ + (defined(OPUS_ARM_ASM) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)) +#include "arm/armcpu.h" + +/* We currently support 5 ARM variants: + * arch[0] -> ARMv4 + * arch[1] -> ARMv5E + * arch[2] -> ARMv6 + * arch[3] -> NEON + * arch[4] -> NEON+DOTPROD + */ +#define OPUS_ARCHMASK 7 + +#elif defined(OPUS_HAVE_RTCD) && \ + ((defined(OPUS_X86_MAY_HAVE_SSE) && !defined(OPUS_X86_PRESUME_SSE)) || \ + (defined(OPUS_X86_MAY_HAVE_SSE2) && !defined(OPUS_X86_PRESUME_SSE2)) || \ + (defined(OPUS_X86_MAY_HAVE_SSE4_1) && !defined(OPUS_X86_PRESUME_SSE4_1)) || \ + (defined(OPUS_X86_MAY_HAVE_AVX2) && !defined(OPUS_X86_PRESUME_AVX2))) + +#include "x86/x86cpu.h" +/* We currently support 5 x86 variants: + * arch[0] -> non-sse + * arch[1] -> sse + * arch[2] -> sse2 + * arch[3] -> sse4.1 + * arch[4] -> avx + */ +#define OPUS_ARCHMASK 7 +int opus_select_arch(void); + +#else +#define OPUS_ARCHMASK 0 + +static OPUS_INLINE int opus_select_arch(void) +{ + return 0; +} +#endif +#endif diff --git a/port/third_party/opus/celt/cwrs.c b/port/third_party/opus/celt/cwrs.c new file mode 100644 index 00000000..a552e4f0 --- /dev/null +++ b/port/third_party/opus/celt/cwrs.c @@ -0,0 +1,715 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Copyright (c) 2007-2009 Timothy B. Terriberry + Written by Timothy B. Terriberry and Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "os_support.h" +#include "cwrs.h" +#include "mathops.h" +#include "arch.h" + +#ifdef CUSTOM_MODES + +/*Guaranteed to return a conservatively large estimate of the binary logarithm + with frac bits of fractional precision. + Tested for all possible 32-bit inputs with frac=4, where the maximum + overestimation is 0.06254243 bits.*/ +int log2_frac(opus_uint32 val, int frac) +{ + int l; + l=EC_ILOG(val); + if(val&(val-1)){ + /*This is (val>>l-16), but guaranteed to round up, even if adding a bias + before the shift would cause overflow (e.g., for 0xFFFFxxxx). + Doesn't work for val=0, but that case fails the test above.*/ + if(l>16)val=((val-1)>>(l-16))+1; + else val<<=16-l; + l=(l-1)<>16); + l+=b<>b; + val=(val*val+0x7FFF)>>15; + } + while(frac-->0); + /*If val is not exactly 0x8000, then we have to round up the remainder.*/ + return l+(val>0x8000); + } + /*Exact powers of two require no rounding.*/ + else return (l-1)<0 ? sum(k=1...K,2**k*choose(N,k)*choose(K-1,k-1)) : 1, + where choose() is the binomial function. + A table of values for N<10 and K<10 looks like: + V[10][10] = { + {1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {1, 2, 2, 2, 2, 2, 2, 2, 2, 2}, + {1, 4, 8, 12, 16, 20, 24, 28, 32, 36}, + {1, 6, 18, 38, 66, 102, 146, 198, 258, 326}, + {1, 8, 32, 88, 192, 360, 608, 952, 1408, 1992}, + {1, 10, 50, 170, 450, 1002, 1970, 3530, 5890, 9290}, + {1, 12, 72, 292, 912, 2364, 5336, 10836, 20256, 35436}, + {1, 14, 98, 462, 1666, 4942, 12642, 28814, 59906, 115598}, + {1, 16, 128, 688, 2816, 9424, 27008, 68464, 157184, 332688}, + {1, 18, 162, 978, 4482, 16722, 53154, 148626, 374274, 864146} + }; + + U(N,K) = the number of such combinations wherein N-1 objects are taken at + most K-1 at a time. + This is given by + U(N,K) = sum(k=0...K-1,V(N-1,k)) + = K>0 ? (V(N-1,K-1) + V(N,K-1))/2 : 0. + The latter expression also makes clear that U(N,K) is half the number of such + combinations wherein the first object is taken at least once. + Although it may not be clear from either of these definitions, U(N,K) is the + natural function to work with when enumerating the pulse vector codebooks, + not V(N,K). + U(N,K) is not well-defined for N=0, but with the extension + U(0,K) = K>0 ? 0 : 1, + the function becomes symmetric: U(N,K) = U(K,N), with a similar table: + U[10][10] = { + {1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + {0, 1, 3, 5, 7, 9, 11, 13, 15, 17}, + {0, 1, 5, 13, 25, 41, 61, 85, 113, 145}, + {0, 1, 7, 25, 63, 129, 231, 377, 575, 833}, + {0, 1, 9, 41, 129, 321, 681, 1289, 2241, 3649}, + {0, 1, 11, 61, 231, 681, 1683, 3653, 7183, 13073}, + {0, 1, 13, 85, 377, 1289, 3653, 8989, 19825, 40081}, + {0, 1, 15, 113, 575, 2241, 7183, 19825, 48639, 108545}, + {0, 1, 17, 145, 833, 3649, 13073, 40081, 108545, 265729} + }; + + With this extension, V(N,K) may be written in terms of U(N,K): + V(N,K) = U(N,K) + U(N,K+1) + for all N>=0, K>=0. + Thus U(N,K+1) represents the number of combinations where the first element + is positive or zero, and U(N,K) represents the number of combinations where + it is negative. + With a large enough table of U(N,K) values, we could write O(N) encoding + and O(min(N*log(K),N+K)) decoding routines, but such a table would be + prohibitively large for small embedded devices (K may be as large as 32767 + for small N, and N may be as large as 200). + + Both functions obey the same recurrence relation: + V(N,K) = V(N-1,K) + V(N,K-1) + V(N-1,K-1), + U(N,K) = U(N-1,K) + U(N,K-1) + U(N-1,K-1), + for all N>0, K>0, with different initial conditions at N=0 or K=0. + This allows us to construct a row of one of the tables above given the + previous row or the next row. + Thus we can derive O(NK) encoding and decoding routines with O(K) memory + using only addition and subtraction. + + When encoding, we build up from the U(2,K) row and work our way forwards. + When decoding, we need to start at the U(N,K) row and work our way backwards, + which requires a means of computing U(N,K). + U(N,K) may be computed from two previous values with the same N: + U(N,K) = ((2*N-1)*U(N,K-1) - U(N,K-2))/(K-1) + U(N,K-2) + for all N>1, and since U(N,K) is symmetric, a similar relation holds for two + previous values with the same K: + U(N,K>1) = ((2*K-1)*U(N-1,K) - U(N-2,K))/(N-1) + U(N-2,K) + for all K>1. + This allows us to construct an arbitrary row of the U(N,K) table by starting + with the first two values, which are constants. + This saves roughly 2/3 the work in our O(NK) decoding routine, but costs O(K) + multiplications. + Similar relations can be derived for V(N,K), but are not used here. + + For N>0 and K>0, U(N,K) and V(N,K) take on the form of an (N-1)-degree + polynomial for fixed N. + The first few are + U(1,K) = 1, + U(2,K) = 2*K-1, + U(3,K) = (2*K-2)*K+1, + U(4,K) = (((4*K-6)*K+8)*K-3)/3, + U(5,K) = ((((2*K-4)*K+10)*K-8)*K+3)/3, + and + V(1,K) = 2, + V(2,K) = 4*K, + V(3,K) = 4*K*K+2, + V(4,K) = 8*(K*K+2)*K/3, + V(5,K) = ((4*K*K+20)*K*K+6)/3, + for all K>0. + This allows us to derive O(N) encoding and O(N*log(K)) decoding routines for + small N (and indeed decoding is also O(N) for N<3). + + @ARTICLE{Fis86, + author="Thomas R. Fischer", + title="A Pyramid Vector Quantizer", + journal="IEEE Transactions on Information Theory", + volume="IT-32", + number=4, + pages="568--583", + month=Jul, + year=1986 + }*/ + +#if !defined(SMALL_FOOTPRINT) + +/*U(N,K) = U(K,N) := N>0?K>0?U(N-1,K)+U(N,K-1)+U(N-1,K-1):0:K>0?1:0*/ +# define CELT_PVQ_U(_n,_k) (CELT_PVQ_U_ROW[IMIN(_n,_k)][IMAX(_n,_k)]) +/*V(N,K) := U(N,K)+U(N,K+1) = the number of PVQ codewords for a band of size N + with K pulses allocated to it.*/ +# define CELT_PVQ_V(_n,_k) (CELT_PVQ_U(_n,_k)+CELT_PVQ_U(_n,(_k)+1)) + +/*For each V(N,K) supported, we will access element U(min(N,K+1),max(N,K+1)). + Thus, the number of entries in row I is the larger of the maximum number of + pulses we will ever allocate for a given N=I (K=128, or however many fit in + 32 bits, whichever is smaller), plus one, and the maximum N for which + K=I-1 pulses fit in 32 bits. + The largest band size in an Opus Custom mode is 208. + Otherwise, we can limit things to the set of N which can be achieved by + splitting a band from a standard Opus mode: 176, 144, 96, 88, 72, 64, 48, + 44, 36, 32, 24, 22, 18, 16, 8, 4, 2).*/ +#if defined(CUSTOM_MODES) +static const opus_uint32 CELT_PVQ_U_DATA[1488]={ +#else +static const opus_uint32 CELT_PVQ_U_DATA[1272]={ +#endif + /*N=0, K=0...176:*/ + 1, 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, 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, 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, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +#if defined(CUSTOM_MODES) + /*...208:*/ + 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, +#endif + /*N=1, K=1...176:*/ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +#if defined(CUSTOM_MODES) + /*...208:*/ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, +#endif + /*N=2, K=2...176:*/ + 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, + 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, + 81, 83, 85, 87, 89, 91, 93, 95, 97, 99, 101, 103, 105, 107, 109, 111, 113, + 115, 117, 119, 121, 123, 125, 127, 129, 131, 133, 135, 137, 139, 141, 143, + 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, + 175, 177, 179, 181, 183, 185, 187, 189, 191, 193, 195, 197, 199, 201, 203, + 205, 207, 209, 211, 213, 215, 217, 219, 221, 223, 225, 227, 229, 231, 233, + 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, 255, 257, 259, 261, 263, + 265, 267, 269, 271, 273, 275, 277, 279, 281, 283, 285, 287, 289, 291, 293, + 295, 297, 299, 301, 303, 305, 307, 309, 311, 313, 315, 317, 319, 321, 323, + 325, 327, 329, 331, 333, 335, 337, 339, 341, 343, 345, 347, 349, 351, +#if defined(CUSTOM_MODES) + /*...208:*/ + 353, 355, 357, 359, 361, 363, 365, 367, 369, 371, 373, 375, 377, 379, 381, + 383, 385, 387, 389, 391, 393, 395, 397, 399, 401, 403, 405, 407, 409, 411, + 413, 415, +#endif + /*N=3, K=3...176:*/ + 13, 25, 41, 61, 85, 113, 145, 181, 221, 265, 313, 365, 421, 481, 545, 613, + 685, 761, 841, 925, 1013, 1105, 1201, 1301, 1405, 1513, 1625, 1741, 1861, + 1985, 2113, 2245, 2381, 2521, 2665, 2813, 2965, 3121, 3281, 3445, 3613, 3785, + 3961, 4141, 4325, 4513, 4705, 4901, 5101, 5305, 5513, 5725, 5941, 6161, 6385, + 6613, 6845, 7081, 7321, 7565, 7813, 8065, 8321, 8581, 8845, 9113, 9385, 9661, + 9941, 10225, 10513, 10805, 11101, 11401, 11705, 12013, 12325, 12641, 12961, + 13285, 13613, 13945, 14281, 14621, 14965, 15313, 15665, 16021, 16381, 16745, + 17113, 17485, 17861, 18241, 18625, 19013, 19405, 19801, 20201, 20605, 21013, + 21425, 21841, 22261, 22685, 23113, 23545, 23981, 24421, 24865, 25313, 25765, + 26221, 26681, 27145, 27613, 28085, 28561, 29041, 29525, 30013, 30505, 31001, + 31501, 32005, 32513, 33025, 33541, 34061, 34585, 35113, 35645, 36181, 36721, + 37265, 37813, 38365, 38921, 39481, 40045, 40613, 41185, 41761, 42341, 42925, + 43513, 44105, 44701, 45301, 45905, 46513, 47125, 47741, 48361, 48985, 49613, + 50245, 50881, 51521, 52165, 52813, 53465, 54121, 54781, 55445, 56113, 56785, + 57461, 58141, 58825, 59513, 60205, 60901, 61601, +#if defined(CUSTOM_MODES) + /*...208:*/ + 62305, 63013, 63725, 64441, 65161, 65885, 66613, 67345, 68081, 68821, 69565, + 70313, 71065, 71821, 72581, 73345, 74113, 74885, 75661, 76441, 77225, 78013, + 78805, 79601, 80401, 81205, 82013, 82825, 83641, 84461, 85285, 86113, +#endif + /*N=4, K=4...176:*/ + 63, 129, 231, 377, 575, 833, 1159, 1561, 2047, 2625, 3303, 4089, 4991, 6017, + 7175, 8473, 9919, 11521, 13287, 15225, 17343, 19649, 22151, 24857, 27775, + 30913, 34279, 37881, 41727, 45825, 50183, 54809, 59711, 64897, 70375, 76153, + 82239, 88641, 95367, 102425, 109823, 117569, 125671, 134137, 142975, 152193, + 161799, 171801, 182207, 193025, 204263, 215929, 228031, 240577, 253575, + 267033, 280959, 295361, 310247, 325625, 341503, 357889, 374791, 392217, + 410175, 428673, 447719, 467321, 487487, 508225, 529543, 551449, 573951, + 597057, 620775, 645113, 670079, 695681, 721927, 748825, 776383, 804609, + 833511, 863097, 893375, 924353, 956039, 988441, 1021567, 1055425, 1090023, + 1125369, 1161471, 1198337, 1235975, 1274393, 1313599, 1353601, 1394407, + 1436025, 1478463, 1521729, 1565831, 1610777, 1656575, 1703233, 1750759, + 1799161, 1848447, 1898625, 1949703, 2001689, 2054591, 2108417, 2163175, + 2218873, 2275519, 2333121, 2391687, 2451225, 2511743, 2573249, 2635751, + 2699257, 2763775, 2829313, 2895879, 2963481, 3032127, 3101825, 3172583, + 3244409, 3317311, 3391297, 3466375, 3542553, 3619839, 3698241, 3777767, + 3858425, 3940223, 4023169, 4107271, 4192537, 4278975, 4366593, 4455399, + 4545401, 4636607, 4729025, 4822663, 4917529, 5013631, 5110977, 5209575, + 5309433, 5410559, 5512961, 5616647, 5721625, 5827903, 5935489, 6044391, + 6154617, 6266175, 6379073, 6493319, 6608921, 6725887, 6844225, 6963943, + 7085049, 7207551, +#if defined(CUSTOM_MODES) + /*...208:*/ + 7331457, 7456775, 7583513, 7711679, 7841281, 7972327, 8104825, 8238783, + 8374209, 8511111, 8649497, 8789375, 8930753, 9073639, 9218041, 9363967, + 9511425, 9660423, 9810969, 9963071, 10116737, 10271975, 10428793, 10587199, + 10747201, 10908807, 11072025, 11236863, 11403329, 11571431, 11741177, + 11912575, +#endif + /*N=5, K=5...176:*/ + 321, 681, 1289, 2241, 3649, 5641, 8361, 11969, 16641, 22569, 29961, 39041, + 50049, 63241, 78889, 97281, 118721, 143529, 172041, 204609, 241601, 283401, + 330409, 383041, 441729, 506921, 579081, 658689, 746241, 842249, 947241, + 1061761, 1186369, 1321641, 1468169, 1626561, 1797441, 1981449, 2179241, + 2391489, 2618881, 2862121, 3121929, 3399041, 3694209, 4008201, 4341801, + 4695809, 5071041, 5468329, 5888521, 6332481, 6801089, 7295241, 7815849, + 8363841, 8940161, 9545769, 10181641, 10848769, 11548161, 12280841, 13047849, + 13850241, 14689089, 15565481, 16480521, 17435329, 18431041, 19468809, + 20549801, 21675201, 22846209, 24064041, 25329929, 26645121, 28010881, + 29428489, 30899241, 32424449, 34005441, 35643561, 37340169, 39096641, + 40914369, 42794761, 44739241, 46749249, 48826241, 50971689, 53187081, + 55473921, 57833729, 60268041, 62778409, 65366401, 68033601, 70781609, + 73612041, 76526529, 79526721, 82614281, 85790889, 89058241, 92418049, + 95872041, 99421961, 103069569, 106816641, 110664969, 114616361, 118672641, + 122835649, 127107241, 131489289, 135983681, 140592321, 145317129, 150160041, + 155123009, 160208001, 165417001, 170752009, 176215041, 181808129, 187533321, + 193392681, 199388289, 205522241, 211796649, 218213641, 224775361, 231483969, + 238341641, 245350569, 252512961, 259831041, 267307049, 274943241, 282741889, + 290705281, 298835721, 307135529, 315607041, 324252609, 333074601, 342075401, + 351257409, 360623041, 370174729, 379914921, 389846081, 399970689, 410291241, + 420810249, 431530241, 442453761, 453583369, 464921641, 476471169, 488234561, + 500214441, 512413449, 524834241, 537479489, 550351881, 563454121, 576788929, + 590359041, 604167209, 618216201, 632508801, +#if defined(CUSTOM_MODES) + /*...208:*/ + 647047809, 661836041, 676876329, 692171521, 707724481, 723538089, 739615241, + 755958849, 772571841, 789457161, 806617769, 824056641, 841776769, 859781161, + 878072841, 896654849, 915530241, 934702089, 954173481, 973947521, 994027329, + 1014416041, 1035116809, 1056132801, 1077467201, 1099123209, 1121104041, + 1143412929, 1166053121, 1189027881, 1212340489, 1235994241, +#endif + /*N=6, K=6...96:*/ + 1683, 3653, 7183, 13073, 22363, 36365, 56695, 85305, 124515, 177045, 246047, + 335137, 448427, 590557, 766727, 982729, 1244979, 1560549, 1937199, 2383409, + 2908411, 3522221, 4235671, 5060441, 6009091, 7095093, 8332863, 9737793, + 11326283, 13115773, 15124775, 17372905, 19880915, 22670725, 25765455, + 29189457, 32968347, 37129037, 41699767, 46710137, 52191139, 58175189, + 64696159, 71789409, 79491819, 87841821, 96879431, 106646281, 117185651, + 128542501, 140763503, 153897073, 167993403, 183104493, 199284183, 216588185, + 235074115, 254801525, 275831935, 298228865, 322057867, 347386557, 374284647, + 402823977, 433078547, 465124549, 499040399, 534906769, 572806619, 612825229, + 655050231, 699571641, 746481891, 795875861, 847850911, 902506913, 959946283, + 1020274013, 1083597703, 1150027593, 1219676595, 1292660325, 1369097135, + 1449108145, 1532817275, 1620351277, 1711839767, 1807415257, 1907213187, + 2011371957, 2120032959, +#if defined(CUSTOM_MODES) + /*...109:*/ + 2233340609U, 2351442379U, 2474488829U, 2602633639U, 2736033641U, 2874848851U, + 3019242501U, 3169381071U, 3325434321U, 3487575323U, 3655980493U, 3830829623U, + 4012305913U, +#endif + /*N=7, K=7...54*/ + 8989, 19825, 40081, 75517, 134245, 227305, 369305, 579125, 880685, 1303777, + 1884961, 2668525, 3707509, 5064793, 6814249, 9041957, 11847485, 15345233, + 19665841, 24957661, 31388293, 39146185, 48442297, 59511829, 72616013, + 88043969, 106114625, 127178701, 151620757, 179861305, 212358985, 249612805, + 292164445, 340600625, 395555537, 457713341, 527810725, 606639529, 695049433, + 793950709, 904317037, 1027188385, 1163673953, 1314955181, 1482288821, + 1667010073, 1870535785, 2094367717, +#if defined(CUSTOM_MODES) + /*...60:*/ + 2340095869U, 2609401873U, 2904062449U, 3225952925U, 3577050821U, 3959439497U, +#endif + /*N=8, K=8...37*/ + 48639, 108545, 224143, 433905, 795455, 1392065, 2340495, 3800305, 5984767, + 9173505, 13726991, 20103025, 28875327, 40754369, 56610575, 77500017, + 104692735, 139703809, 184327311, 240673265, 311207743, 398796225, 506750351, + 638878193, 799538175, 993696769, 1226990095, 1505789553, 1837271615, + 2229491905U, +#if defined(CUSTOM_MODES) + /*...40:*/ + 2691463695U, 3233240945U, 3866006015U, +#endif + /*N=9, K=9...28:*/ + 265729, 598417, 1256465, 2485825, 4673345, 8405905, 14546705, 24331777, + 39490049, 62390545, 96220561, 145198913, 214828609, 312193553, 446304145, + 628496897, 872893441, 1196924561, 1621925137, 2173806145U, +#if defined(CUSTOM_MODES) + /*...29:*/ + 2883810113U, +#endif + /*N=10, K=10...24:*/ + 1462563, 3317445, 7059735, 14218905, 27298155, 50250765, 89129247, 152951073, + 254831667, 413442773, 654862247, 1014889769, 1541911931, 2300409629U, + 3375210671U, + /*N=11, K=11...19:*/ + 8097453, 18474633, 39753273, 81270333, 158819253, 298199265, 540279585, + 948062325, 1616336765, +#if defined(CUSTOM_MODES) + /*...20:*/ + 2684641785U, +#endif + /*N=12, K=12...18:*/ + 45046719, 103274625, 224298231, 464387817, 921406335, 1759885185, + 3248227095U, + /*N=13, K=13...16:*/ + 251595969, 579168825, 1267854873, 2653649025U, + /*N=14, K=14:*/ + 1409933619 +}; + +#if defined(CUSTOM_MODES) +static const opus_uint32 *const CELT_PVQ_U_ROW[15]={ + CELT_PVQ_U_DATA+ 0,CELT_PVQ_U_DATA+ 208,CELT_PVQ_U_DATA+ 415, + CELT_PVQ_U_DATA+ 621,CELT_PVQ_U_DATA+ 826,CELT_PVQ_U_DATA+1030, + CELT_PVQ_U_DATA+1233,CELT_PVQ_U_DATA+1336,CELT_PVQ_U_DATA+1389, + CELT_PVQ_U_DATA+1421,CELT_PVQ_U_DATA+1441,CELT_PVQ_U_DATA+1455, + CELT_PVQ_U_DATA+1464,CELT_PVQ_U_DATA+1470,CELT_PVQ_U_DATA+1473 +}; +#else +static const opus_uint32 *const CELT_PVQ_U_ROW[15]={ + CELT_PVQ_U_DATA+ 0,CELT_PVQ_U_DATA+ 176,CELT_PVQ_U_DATA+ 351, + CELT_PVQ_U_DATA+ 525,CELT_PVQ_U_DATA+ 698,CELT_PVQ_U_DATA+ 870, + CELT_PVQ_U_DATA+1041,CELT_PVQ_U_DATA+1131,CELT_PVQ_U_DATA+1178, + CELT_PVQ_U_DATA+1207,CELT_PVQ_U_DATA+1226,CELT_PVQ_U_DATA+1240, + CELT_PVQ_U_DATA+1248,CELT_PVQ_U_DATA+1254,CELT_PVQ_U_DATA+1257 +}; +#endif + +#if defined(CUSTOM_MODES) +void get_required_bits(opus_int16 *_bits,int _n,int _maxk,int _frac){ + int k; + /*_maxk==0 => there's nothing to do.*/ + celt_assert(_maxk>0); + _bits[0]=0; + for(k=1;k<=_maxk;k++)_bits[k]=log2_frac(CELT_PVQ_V(_n,k),_frac); +} +#endif + +static opus_uint32 icwrs(int _n,const int *_y){ + opus_uint32 i; + int j; + int k; + celt_assert(_n>=2); + j=_n-1; + i=_y[j]<0; + k=abs(_y[j]); + do{ + j--; + i+=CELT_PVQ_U(_n-j,k); + k+=abs(_y[j]); + if(_y[j]<0)i+=CELT_PVQ_U(_n-j,k+1); + } + while(j>0); + return i; +} + +void encode_pulses(const int *_y,int _n,int _k,ec_enc *_enc){ + celt_assert(_k>0); + ec_enc_uint(_enc,icwrs(_n,_y),CELT_PVQ_V(_n,_k)); +} + +static opus_val32 cwrsi(int _n,int _k,opus_uint32 _i,int *_y){ + opus_uint32 p; + int s; + int k0; + opus_int16 val; + opus_val32 yy=0; + celt_assert(_k>0); + celt_assert(_n>1); + while(_n>2){ + opus_uint32 q; + /*Lots of pulses case:*/ + if(_k>=_n){ + const opus_uint32 *row; + row=CELT_PVQ_U_ROW[_n]; + /*Are the pulses in this dimension negative?*/ + p=row[_k+1]; + s=-(_i>=p); + _i-=p&s; + /*Count how many pulses were placed in this dimension.*/ + k0=_k; + q=row[_n]; + if(q>_i){ + celt_sig_assert(p>q); + _k=_n; + do p=CELT_PVQ_U_ROW[--_k][_n]; + while(p>_i); + } + else for(p=row[_k];p>_i;p=row[_k])_k--; + _i-=p; + val=(k0-_k+s)^s; + *_y++=val; + yy=MAC16_16(yy,val,val); + } + /*Lots of dimensions case:*/ + else{ + /*Are there any pulses in this dimension at all?*/ + p=CELT_PVQ_U_ROW[_k][_n]; + q=CELT_PVQ_U_ROW[_k+1][_n]; + if(p<=_i&&_i=q); + _i-=q&s; + /*Count how many pulses were placed in this dimension.*/ + k0=_k; + do p=CELT_PVQ_U_ROW[--_k][_n]; + while(p>_i); + _i-=p; + val=(k0-_k+s)^s; + *_y++=val; + yy=MAC16_16(yy,val,val); + } + } + _n--; + } + /*_n==2*/ + p=2*_k+1; + s=-(_i>=p); + _i-=p&s; + k0=_k; + _k=(_i+1)>>1; + if(_k)_i-=2*_k-1; + val=(k0-_k+s)^s; + *_y++=val; + yy=MAC16_16(yy,val,val); + /*_n==1*/ + s=-(int)_i; + val=(_k+s)^s; + *_y=val; + yy=MAC16_16(yy,val,val); + return yy; +} + +opus_val32 decode_pulses(int *_y,int _n,int _k,ec_dec *_dec){ + return cwrsi(_n,_k,ec_dec_uint(_dec,CELT_PVQ_V(_n,_k)),_y); +} + +#else /* SMALL_FOOTPRINT */ + +/*Computes the next row/column of any recurrence that obeys the relation + u[i][j]=u[i-1][j]+u[i][j-1]+u[i-1][j-1]. + _ui0 is the base case for the new row/column.*/ +static OPUS_INLINE void unext(opus_uint32 *_ui,unsigned _len,opus_uint32 _ui0){ + opus_uint32 ui1; + unsigned j; + /*This do-while will overrun the array if we don't have storage for at least + 2 values.*/ + j=1; do { + ui1=UADD32(UADD32(_ui[j],_ui[j-1]),_ui0); + _ui[j-1]=_ui0; + _ui0=ui1; + } while (++j<_len); + _ui[j-1]=_ui0; +} + +/*Computes the previous row/column of any recurrence that obeys the relation + u[i-1][j]=u[i][j]-u[i][j-1]-u[i-1][j-1]. + _ui0 is the base case for the new row/column.*/ +static OPUS_INLINE void uprev(opus_uint32 *_ui,unsigned _n,opus_uint32 _ui0){ + opus_uint32 ui1; + unsigned j; + /*This do-while will overrun the array if we don't have storage for at least + 2 values.*/ + j=1; do { + ui1=USUB32(USUB32(_ui[j],_ui[j-1]),_ui0); + _ui[j-1]=_ui0; + _ui0=ui1; + } while (++j<_n); + _ui[j-1]=_ui0; +} + +/*Compute V(_n,_k), as well as U(_n,0..._k+1). + _u: On exit, _u[i] contains U(_n,i) for i in [0..._k+1].*/ +static opus_uint32 ncwrs_urow(unsigned _n,unsigned _k,opus_uint32 *_u){ + opus_uint32 um2; + unsigned len; + unsigned k; + len=_k+2; + /*We require storage at least 3 values (e.g., _k>0).*/ + celt_assert(len>=3); + _u[0]=0; + _u[1]=um2=1; + /*If _n==0, _u[0] should be 1 and the rest should be 0.*/ + /*If _n==1, _u[i] should be 1 for i>1.*/ + celt_assert(_n>=2); + /*If _k==0, the following do-while loop will overflow the buffer.*/ + celt_assert(_k>0); + k=2; + do _u[k]=(k<<1)-1; + while(++k0); + j=0; + do{ + opus_uint32 p; + int s; + int yj; + p=_u[_k+1]; + s=-(_i>=p); + _i-=p&s; + yj=_k; + p=_u[_k]; + while(p>_i)p=_u[--_k]; + _i-=p; + yj-=_k; + val=(yj+s)^s; + _y[j]=val; + yy=MAC16_16(yy,val,val); + uprev(_u,_k+2,0); + } + while(++j<_n); + return yy; +} + +/*Returns the index of the given combination of K elements chosen from a set + of size 1 with associated sign bits. + _y: The vector of pulses, whose sum of absolute values is K. + _k: Returns K.*/ +static OPUS_INLINE opus_uint32 icwrs1(const int *_y,int *_k){ + *_k=abs(_y[0]); + return _y[0]<0; +} + +/*Returns the index of the given combination of K elements chosen from a set + of size _n with associated sign bits. + _y: The vector of pulses, whose sum of absolute values must be _k. + _nc: Returns V(_n,_k).*/ +static OPUS_INLINE opus_uint32 icwrs(int _n,int _k,opus_uint32 *_nc,const int *_y, + opus_uint32 *_u){ + opus_uint32 i; + int j; + int k; + /*We can't unroll the first two iterations of the loop unless _n>=2.*/ + celt_assert(_n>=2); + _u[0]=0; + for(k=1;k<=_k+1;k++)_u[k]=(k<<1)-1; + i=icwrs1(_y+_n-1,&k); + j=_n-2; + i+=_u[k]; + k+=abs(_y[j]); + if(_y[j]<0)i+=_u[k+1]; + while(j-->0){ + unext(_u,_k+2,0); + i+=_u[k]; + k+=abs(_y[j]); + if(_y[j]<0)i+=_u[k+1]; + } + *_nc=_u[k]+_u[k+1]; + return i; +} + +#ifdef CUSTOM_MODES +void get_required_bits(opus_int16 *_bits,int _n,int _maxk,int _frac){ + int k; + /*_maxk==0 => there's nothing to do.*/ + celt_assert(_maxk>0); + _bits[0]=0; + if (_n==1) + { + for (k=1;k<=_maxk;k++) + _bits[k] = 1<<_frac; + } + else { + VARDECL(opus_uint32,u); + SAVE_STACK; + ALLOC(u,_maxk+2U,opus_uint32); + ncwrs_urow(_n,_maxk,u); + for(k=1;k<=_maxk;k++) + _bits[k]=log2_frac(u[k]+u[k+1],_frac); + RESTORE_STACK; + } +} +#endif /* CUSTOM_MODES */ + +void encode_pulses(const int *_y,int _n,int _k,ec_enc *_enc){ + opus_uint32 i; + VARDECL(opus_uint32,u); + opus_uint32 nc; + SAVE_STACK; + celt_assert(_k>0); + ALLOC(u,_k+2U,opus_uint32); + i=icwrs(_n,_k,&nc,_y,u); + ec_enc_uint(_enc,i,nc); + RESTORE_STACK; +} + +opus_val32 decode_pulses(int *_y,int _n,int _k,ec_dec *_dec){ + VARDECL(opus_uint32,u); + int ret; + SAVE_STACK; + celt_assert(_k>0); + ALLOC(u,_k+2U,opus_uint32); + ret = cwrsi(_n,_k,ec_dec_uint(_dec,ncwrs_urow(_n,_k,u)),_y,u); + RESTORE_STACK; + return ret; +} + +#endif /* SMALL_FOOTPRINT */ diff --git a/port/third_party/opus/celt/cwrs.h b/port/third_party/opus/celt/cwrs.h new file mode 100644 index 00000000..7cd47174 --- /dev/null +++ b/port/third_party/opus/celt/cwrs.h @@ -0,0 +1,48 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Copyright (c) 2007-2009 Timothy B. Terriberry + Written by Timothy B. Terriberry and Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef CWRS_H +#define CWRS_H + +#include "arch.h" +#include "stack_alloc.h" +#include "entenc.h" +#include "entdec.h" + +#ifdef CUSTOM_MODES +int log2_frac(opus_uint32 val, int frac); +#endif + +void get_required_bits(opus_int16 *bits, int N, int K, int frac); + +void encode_pulses(const int *_y, int N, int K, ec_enc *enc); + +opus_val32 decode_pulses(int *_y, int N, int K, ec_dec *dec); + +#endif /* CWRS_H */ diff --git a/port/third_party/opus/celt/ecintrin.h b/port/third_party/opus/celt/ecintrin.h new file mode 100644 index 00000000..66a4c36e --- /dev/null +++ b/port/third_party/opus/celt/ecintrin.h @@ -0,0 +1,91 @@ +/* Copyright (c) 2003-2008 Timothy B. Terriberry + Copyright (c) 2008 Xiph.Org Foundation */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/*Some common macros for potential platform-specific optimization.*/ +#include "opus_types.h" +#include +#include +#include "arch.h" +#if !defined(_ecintrin_H) +# define _ecintrin_H (1) + +/*Some specific platforms may have optimized intrinsic or OPUS_INLINE assembly + versions of these functions which can substantially improve performance. + We define macros for them to allow easy incorporation of these non-ANSI + features.*/ + +/*Modern gcc (4.x) can compile the naive versions of min and max with cmov if + given an appropriate architecture, but the branchless bit-twiddling versions + are just as fast, and do not require any special target architecture. + Earlier gcc versions (3.x) compiled both code to the same assembly + instructions, because of the way they represented ((_b)>(_a)) internally.*/ +# define EC_MINI(_a,_b) ((_a)+(((_b)-(_a))&-((_b)<(_a)))) + +/*Count leading zeros. + This macro should only be used for implementing ec_ilog(), if it is defined. + All other code should use EC_ILOG() instead.*/ +#if defined(_MSC_VER) && (_MSC_VER >= 1400) +#if defined(_MSC_VER) && (_MSC_VER >= 1910) +# include /* Improve compiler throughput. */ +#else +# include +#endif +/*In _DEBUG mode this is not an intrinsic by default.*/ +# pragma intrinsic(_BitScanReverse) + +static __inline int ec_bsr(unsigned long _x){ + unsigned long ret; + _BitScanReverse(&ret,_x); + return (int)ret; +} +# define EC_CLZ0 (1) +# define EC_CLZ(_x) (-ec_bsr(_x)) +#elif defined(ENABLE_TI_DSPLIB) +# include "dsplib.h" +# define EC_CLZ0 (31) +# define EC_CLZ(_x) (_lnorm(_x)) +#elif __GNUC_PREREQ(3,4) +# if INT_MAX>=2147483647 +# define EC_CLZ0 ((int)sizeof(unsigned)*CHAR_BIT) +# define EC_CLZ(_x) (__builtin_clz(_x)) +# elif LONG_MAX>=2147483647L +# define EC_CLZ0 ((int)sizeof(unsigned long)*CHAR_BIT) +# define EC_CLZ(_x) (__builtin_clzl(_x)) +# endif +#endif + +#if defined(EC_CLZ) +/*Note that __builtin_clz is not defined when _x==0, according to the gcc + documentation (and that of the BSR instruction that implements it on x86). + The majority of the time we can never pass it zero. + When we need to, it can be special cased.*/ +# define EC_ILOG(_x) (EC_CLZ0-EC_CLZ(_x)) +#else +int ec_ilog(opus_uint32 _v); +# define EC_ILOG(_x) (ec_ilog(_x)) +#endif +#endif diff --git a/port/third_party/opus/celt/entcode.c b/port/third_party/opus/celt/entcode.c new file mode 100644 index 00000000..70f32016 --- /dev/null +++ b/port/third_party/opus/celt/entcode.c @@ -0,0 +1,153 @@ +/* Copyright (c) 2001-2011 Timothy B. Terriberry +*/ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "entcode.h" +#include "arch.h" + +#if !defined(EC_CLZ) +/*This is a fallback for systems where we don't know how to access + a BSR or CLZ instruction (see ecintrin.h). + If you are optimizing Opus on a new platform and it has a native CLZ or + BZR (e.g. cell, MIPS, x86, etc) then making it available to Opus will be + an easy performance win.*/ +int ec_ilog(opus_uint32 _v){ + /*On a Pentium M, this branchless version tested as the fastest on + 1,000,000,000 random 32-bit integers, edging out a similar version with + branches, and a 256-entry LUT version.*/ + int ret; + int m; + ret=!!_v; + m=!!(_v&0xFFFF0000)<<4; + _v>>=m; + ret|=m; + m=!!(_v&0xFF00)<<3; + _v>>=m; + ret|=m; + m=!!(_v&0xF0)<<2; + _v>>=m; + ret|=m; + m=!!(_v&0xC)<<1; + _v>>=m; + ret|=m; + ret+=!!(_v&0x2); + return ret; +} +#endif + +#if 1 +/* This is a faster version of ec_tell_frac() that takes advantage + of the low (1/8 bit) resolution to use just a linear function + followed by a lookup to determine the exact transition thresholds. */ +opus_uint32 ec_tell_frac(ec_ctx *_this){ + static const unsigned correction[8] = + {35733, 38967, 42495, 46340, + 50535, 55109, 60097, 65535}; + opus_uint32 nbits; + opus_uint32 r; + int l; + unsigned b; + nbits=_this->nbits_total<rng); + r=_this->rng>>(l-16); + b = (r>>12)-8; + b += r>correction[b]; + l = (l<<3)+b; + return nbits-l; +} +#else +opus_uint32 ec_tell_frac(ec_ctx *_this){ + opus_uint32 nbits; + opus_uint32 r; + int l; + int i; + /*To handle the non-integral number of bits still left in the encoder/decoder + state, we compute the worst-case number of bits of val that must be + encoded to ensure that the value is inside the range for any possible + subsequent bits. + The computation here is independent of val itself (the decoder does not + even track that value), even though the real number of bits used after + ec_enc_done() may be 1 smaller if rng is a power of two and the + corresponding trailing bits of val are all zeros. + If we did try to track that special case, then coding a value with a + probability of 1/(1<nbits_total<rng); + r=_this->rng>>(l-16); + for(i=BITRES;i-->0;){ + int b; + r=r*r>>15; + b=(int)(r>>16); + l=l<<1|b; + r>>=b; + } + return nbits-l; +} +#endif + +#ifdef USE_SMALL_DIV_TABLE +/* Result of 2^32/(2*i+1), except for i=0. */ +const opus_uint32 SMALL_DIV_TABLE[129] = { + 0xFFFFFFFF, 0x55555555, 0x33333333, 0x24924924, + 0x1C71C71C, 0x1745D174, 0x13B13B13, 0x11111111, + 0x0F0F0F0F, 0x0D79435E, 0x0C30C30C, 0x0B21642C, + 0x0A3D70A3, 0x097B425E, 0x08D3DCB0, 0x08421084, + 0x07C1F07C, 0x07507507, 0x06EB3E45, 0x06906906, + 0x063E7063, 0x05F417D0, 0x05B05B05, 0x0572620A, + 0x05397829, 0x05050505, 0x04D4873E, 0x04A7904A, + 0x047DC11F, 0x0456C797, 0x04325C53, 0x04104104, + 0x03F03F03, 0x03D22635, 0x03B5CC0E, 0x039B0AD1, + 0x0381C0E0, 0x0369D036, 0x03531DEC, 0x033D91D2, + 0x0329161F, 0x03159721, 0x03030303, 0x02F14990, + 0x02E05C0B, 0x02D02D02, 0x02C0B02C, 0x02B1DA46, + 0x02A3A0FD, 0x0295FAD4, 0x0288DF0C, 0x027C4597, + 0x02702702, 0x02647C69, 0x02593F69, 0x024E6A17, + 0x0243F6F0, 0x0239E0D5, 0x02302302, 0x0226B902, + 0x021D9EAD, 0x0214D021, 0x020C49BA, 0x02040810, + 0x01FC07F0, 0x01F44659, 0x01ECC07B, 0x01E573AC, + 0x01DE5D6E, 0x01D77B65, 0x01D0CB58, 0x01CA4B30, + 0x01C3F8F0, 0x01BDD2B8, 0x01B7D6C3, 0x01B20364, + 0x01AC5701, 0x01A6D01A, 0x01A16D3F, 0x019C2D14, + 0x01970E4F, 0x01920FB4, 0x018D3018, 0x01886E5F, + 0x0183C977, 0x017F405F, 0x017AD220, 0x01767DCE, + 0x01724287, 0x016E1F76, 0x016A13CD, 0x01661EC6, + 0x01623FA7, 0x015E75BB, 0x015AC056, 0x01571ED3, + 0x01539094, 0x01501501, 0x014CAB88, 0x0149539E, + 0x01460CBC, 0x0142D662, 0x013FB013, 0x013C995A, + 0x013991C2, 0x013698DF, 0x0133AE45, 0x0130D190, + 0x012E025C, 0x012B404A, 0x01288B01, 0x0125E227, + 0x01234567, 0x0120B470, 0x011E2EF3, 0x011BB4A4, + 0x01194538, 0x0116E068, 0x011485F0, 0x0112358E, + 0x010FEF01, 0x010DB20A, 0x010B7E6E, 0x010953F3, + 0x01073260, 0x0105197F, 0x0103091B, 0x01010101 +}; +#endif diff --git a/port/third_party/opus/celt/entcode.h b/port/third_party/opus/celt/entcode.h new file mode 100644 index 00000000..3763e3f2 --- /dev/null +++ b/port/third_party/opus/celt/entcode.h @@ -0,0 +1,152 @@ +/* Copyright (c) 2001-2011 Timothy B. Terriberry + Copyright (c) 2008-2009 Xiph.Org Foundation */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "opus_types.h" +#include "opus_defines.h" + +#if !defined(_entcode_H) +# define _entcode_H (1) +# include +# include +# include "ecintrin.h" + +extern const opus_uint32 SMALL_DIV_TABLE[129]; + +#ifdef OPUS_ARM_ASM +#define USE_SMALL_DIV_TABLE +#endif + +/*OPT: ec_window must be at least 32 bits, but if you have fast arithmetic on a + larger type, you can speed up the decoder by using it here.*/ +typedef opus_uint32 ec_window; +typedef struct ec_ctx ec_ctx; +typedef struct ec_ctx ec_enc; +typedef struct ec_ctx ec_dec; + +# define EC_WINDOW_SIZE ((int)sizeof(ec_window)*CHAR_BIT) + +/*The number of bits to use for the range-coded part of unsigned integers.*/ +# define EC_UINT_BITS (8) + +/*The resolution of fractional-precision bit usage measurements, i.e., + 3 => 1/8th bits.*/ +# define BITRES 3 + +/*The entropy encoder/decoder context. + We use the same structure for both, so that common functions like ec_tell() + can be used on either one.*/ +struct ec_ctx{ + /*Buffered input/output.*/ + unsigned char *buf; + /*The size of the buffer.*/ + opus_uint32 storage; + /*The offset at which the last byte containing raw bits was read/written.*/ + opus_uint32 end_offs; + /*Bits that will be read from/written at the end.*/ + ec_window end_window; + /*Number of valid bits in end_window.*/ + int nend_bits; + /*The total number of whole bits read/written. + This does not include partial bits currently in the range coder.*/ + int nbits_total; + /*The offset at which the next range coder byte will be read/written.*/ + opus_uint32 offs; + /*The number of values in the current range.*/ + opus_uint32 rng; + /*In the decoder: the difference between the top of the current range and + the input value, minus one. + In the encoder: the low end of the current range.*/ + opus_uint32 val; + /*In the decoder: the saved normalization factor from ec_decode(). + In the encoder: the number of oustanding carry propagating symbols.*/ + opus_uint32 ext; + /*A buffered input/output symbol, awaiting carry propagation.*/ + int rem; + /*Nonzero if an error occurred.*/ + int error; +}; + +static OPUS_INLINE opus_uint32 ec_range_bytes(ec_ctx *_this){ + return _this->offs; +} + +static OPUS_INLINE unsigned char *ec_get_buffer(ec_ctx *_this){ + return _this->buf; +} + +static OPUS_INLINE int ec_get_error(ec_ctx *_this){ + return _this->error; +} + +/*Returns the number of bits "used" by the encoded or decoded symbols so far. + This same number can be computed in either the encoder or the decoder, and is + suitable for making coding decisions. + Return: The number of bits. + This will always be slightly larger than the exact value (e.g., all + rounding error is in the positive direction).*/ +static OPUS_INLINE int ec_tell(ec_ctx *_this){ + return _this->nbits_total-EC_ILOG(_this->rng); +} + +/*Returns the number of bits "used" by the encoded or decoded symbols so far. + This same number can be computed in either the encoder or the decoder, and is + suitable for making coding decisions. + Return: The number of bits scaled by 2**BITRES. + This will always be slightly larger than the exact value (e.g., all + rounding error is in the positive direction).*/ +opus_uint32 ec_tell_frac(ec_ctx *_this); + +/* Tested exhaustively for all n and for 1<=d<=256 */ +static OPUS_INLINE opus_uint32 celt_udiv(opus_uint32 n, opus_uint32 d) { + celt_sig_assert(d>0); +#ifdef USE_SMALL_DIV_TABLE + if (d>256) + return n/d; + else { + opus_uint32 t, q; + t = EC_ILOG(d&-d); + q = (opus_uint64)SMALL_DIV_TABLE[d>>t]*(n>>(t-1))>>32; + return q+(n-q*d >= d); + } +#else + return n/d; +#endif +} + +static OPUS_INLINE opus_int32 celt_sudiv(opus_int32 n, opus_int32 d) { + celt_sig_assert(d>0); +#ifdef USE_SMALL_DIV_TABLE + if (n<0) + return -(opus_int32)celt_udiv(-n, d); + else + return celt_udiv(n, d); +#else + return n/d; +#endif +} + +#endif diff --git a/port/third_party/opus/celt/entdec.c b/port/third_party/opus/celt/entdec.c new file mode 100644 index 00000000..027aa24b --- /dev/null +++ b/port/third_party/opus/celt/entdec.c @@ -0,0 +1,266 @@ +/* Copyright (c) 2001-2011 Timothy B. Terriberry + Copyright (c) 2008-2009 Xiph.Org Foundation */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "os_support.h" +#include "arch.h" +#include "entdec.h" +#include "mfrngcod.h" + +/*A range decoder. + This is an entropy decoder based upon \cite{Mar79}, which is itself a + rediscovery of the FIFO arithmetic code introduced by \cite{Pas76}. + It is very similar to arithmetic encoding, except that encoding is done with + digits in any base, instead of with bits, and so it is faster when using + larger bases (i.e.: a byte). + The author claims an average waste of $\frac{1}{2}\log_b(2b)$ bits, where $b$ + is the base, longer than the theoretical optimum, but to my knowledge there + is no published justification for this claim. + This only seems true when using near-infinite precision arithmetic so that + the process is carried out with no rounding errors. + + An excellent description of implementation details is available at + http://www.arturocampos.com/ac_range.html + A recent work \cite{MNW98} which proposes several changes to arithmetic + encoding for efficiency actually re-discovers many of the principles + behind range encoding, and presents a good theoretical analysis of them. + + End of stream is handled by writing out the smallest number of bits that + ensures that the stream will be correctly decoded regardless of the value of + any subsequent bits. + ec_tell() can be used to determine how many bits were needed to decode + all the symbols thus far; other data can be packed in the remaining bits of + the input buffer. + @PHDTHESIS{Pas76, + author="Richard Clark Pasco", + title="Source coding algorithms for fast data compression", + school="Dept. of Electrical Engineering, Stanford University", + address="Stanford, CA", + month=May, + year=1976 + } + @INPROCEEDINGS{Mar79, + author="Martin, G.N.N.", + title="Range encoding: an algorithm for removing redundancy from a digitised + message", + booktitle="Video & Data Recording Conference", + year=1979, + address="Southampton", + month=Jul + } + @ARTICLE{MNW98, + author="Alistair Moffat and Radford Neal and Ian H. Witten", + title="Arithmetic Coding Revisited", + journal="{ACM} Transactions on Information Systems", + year=1998, + volume=16, + number=3, + pages="256--294", + month=Jul, + URL="http://www.stanford.edu/class/ee398a/handouts/papers/Moffat98ArithmCoding.pdf" + }*/ + +static int ec_read_byte(ec_dec *_this){ + return _this->offs<_this->storage?_this->buf[_this->offs++]:0; +} + +static int ec_read_byte_from_end(ec_dec *_this){ + return _this->end_offs<_this->storage? + _this->buf[_this->storage-++(_this->end_offs)]:0; +} + +/*Normalizes the contents of val and rng so that rng lies entirely in the + high-order symbol.*/ +static void ec_dec_normalize(ec_dec *_this){ + /*If the range is too small, rescale it and input some bits.*/ + while(_this->rng<=EC_CODE_BOT){ + int sym; + _this->nbits_total+=EC_SYM_BITS; + _this->rng<<=EC_SYM_BITS; + /*Use up the remaining bits from our last symbol.*/ + sym=_this->rem; + /*Read the next value from the input.*/ + _this->rem=ec_read_byte(_this); + /*Take the rest of the bits we need from this new symbol.*/ + sym=(sym<rem)>>(EC_SYM_BITS-EC_CODE_EXTRA); + /*And subtract them from val, capped to be less than EC_CODE_TOP.*/ + _this->val=((_this->val<buf=_buf; + _this->storage=_storage; + _this->end_offs=0; + _this->end_window=0; + _this->nend_bits=0; + /*This is the offset from which ec_tell() will subtract partial bits. + The final value after the ec_dec_normalize() call will be the same as in + the encoder, but we have to compensate for the bits that are added there.*/ + _this->nbits_total=EC_CODE_BITS+1 + -((EC_CODE_BITS-EC_CODE_EXTRA)/EC_SYM_BITS)*EC_SYM_BITS; + _this->offs=0; + _this->rng=1U<rem=ec_read_byte(_this); + _this->val=_this->rng-1-(_this->rem>>(EC_SYM_BITS-EC_CODE_EXTRA)); + _this->error=0; + /*Normalize the interval.*/ + ec_dec_normalize(_this); +} + +unsigned ec_decode(ec_dec *_this,unsigned _ft){ + unsigned s; + _this->ext=celt_udiv(_this->rng,_ft); + s=(unsigned)(_this->val/_this->ext); + return _ft-EC_MINI(s+1,_ft); +} + +unsigned ec_decode_bin(ec_dec *_this,unsigned _bits){ + unsigned s; + _this->ext=_this->rng>>_bits; + s=(unsigned)(_this->val/_this->ext); + return (1U<<_bits)-EC_MINI(s+1U,1U<<_bits); +} + +void ec_dec_update(ec_dec *_this,unsigned _fl,unsigned _fh,unsigned _ft){ + opus_uint32 s; + s=IMUL32(_this->ext,_ft-_fh); + _this->val-=s; + _this->rng=_fl>0?IMUL32(_this->ext,_fh-_fl):_this->rng-s; + ec_dec_normalize(_this); +} + +/*The probability of having a "one" is 1/(1<<_logp).*/ +int ec_dec_bit_logp(ec_dec *_this,unsigned _logp){ + opus_uint32 r; + opus_uint32 d; + opus_uint32 s; + int ret; + r=_this->rng; + d=_this->val; + s=r>>_logp; + ret=dval=d-s; + _this->rng=ret?s:r-s; + ec_dec_normalize(_this); + return ret; +} + +int ec_dec_icdf(ec_dec *_this,const unsigned char *_icdf,unsigned _ftb){ + opus_uint32 r; + opus_uint32 d; + opus_uint32 s; + opus_uint32 t; + int ret; + s=_this->rng; + d=_this->val; + r=s>>_ftb; + ret=-1; + do{ + t=s; + s=IMUL32(r,_icdf[++ret]); + } + while(dval=d-s; + _this->rng=t-s; + ec_dec_normalize(_this); + return ret; +} + +int ec_dec_icdf16(ec_dec *_this,const opus_uint16 *_icdf,unsigned _ftb){ + opus_uint32 r; + opus_uint32 d; + opus_uint32 s; + opus_uint32 t; + int ret; + s=_this->rng; + d=_this->val; + r=s>>_ftb; + ret=-1; + do{ + t=s; + s=IMUL32(r,_icdf[++ret]); + } + while(dval=d-s; + _this->rng=t-s; + ec_dec_normalize(_this); + return ret; +} + +opus_uint32 ec_dec_uint(ec_dec *_this,opus_uint32 _ft){ + unsigned ft; + unsigned s; + int ftb; + /*In order to optimize EC_ILOG(), it is undefined for the value 0.*/ + celt_assert(_ft>1); + _ft--; + ftb=EC_ILOG(_ft); + if(ftb>EC_UINT_BITS){ + opus_uint32 t; + ftb-=EC_UINT_BITS; + ft=(unsigned)(_ft>>ftb)+1; + s=ec_decode(_this,ft); + ec_dec_update(_this,s,s+1,ft); + t=(opus_uint32)s<error=1; + return _ft; + } + else{ + _ft++; + s=ec_decode(_this,(unsigned)_ft); + ec_dec_update(_this,s,s+1,(unsigned)_ft); + return s; + } +} + +opus_uint32 ec_dec_bits(ec_dec *_this,unsigned _bits){ + ec_window window; + int available; + opus_uint32 ret; + window=_this->end_window; + available=_this->nend_bits; + if((unsigned)available<_bits){ + do{ + window|=(ec_window)ec_read_byte_from_end(_this)<>=_bits; + available-=_bits; + _this->end_window=window; + _this->nend_bits=available; + _this->nbits_total+=_bits; + return ret; +} diff --git a/port/third_party/opus/celt/entdec.h b/port/third_party/opus/celt/entdec.h new file mode 100644 index 00000000..c81f26fd --- /dev/null +++ b/port/third_party/opus/celt/entdec.h @@ -0,0 +1,110 @@ +/* Copyright (c) 2001-2011 Timothy B. Terriberry + Copyright (c) 2008-2009 Xiph.Org Foundation */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#if !defined(_entdec_H) +# define _entdec_H (1) +# include +# include "entcode.h" + +/*Initializes the decoder. + _buf: The input buffer to use. + Return: 0 on success, or a negative value on error.*/ +void ec_dec_init(ec_dec *_this,unsigned char *_buf,opus_uint32 _storage); + +/*Calculates the cumulative frequency for the next symbol. + This can then be fed into the probability model to determine what that + symbol is, and the additional frequency information required to advance to + the next symbol. + This function cannot be called more than once without a corresponding call to + ec_dec_update(), or decoding will not proceed correctly. + _ft: The total frequency of the symbols in the alphabet the next symbol was + encoded with. + Return: A cumulative frequency representing the encoded symbol. + If the cumulative frequency of all the symbols before the one that + was encoded was fl, and the cumulative frequency of all the symbols + up to and including the one encoded is fh, then the returned value + will fall in the range [fl,fh).*/ +unsigned ec_decode(ec_dec *_this,unsigned _ft); + +/*Equivalent to ec_decode() with _ft==1<<_bits.*/ +unsigned ec_decode_bin(ec_dec *_this,unsigned _bits); + +/*Advance the decoder past the next symbol using the frequency information the + symbol was encoded with. + Exactly one call to ec_decode() must have been made so that all necessary + intermediate calculations are performed. + _fl: The cumulative frequency of all symbols that come before the symbol + decoded. + _fh: The cumulative frequency of all symbols up to and including the symbol + decoded. + Together with _fl, this defines the range [_fl,_fh) in which the value + returned above must fall. + _ft: The total frequency of the symbols in the alphabet the symbol decoded + was encoded in. + This must be the same as passed to the preceding call to ec_decode().*/ +void ec_dec_update(ec_dec *_this,unsigned _fl,unsigned _fh,unsigned _ft); + +/* Decode a bit that has a 1/(1<<_logp) probability of being a one */ +int ec_dec_bit_logp(ec_dec *_this,unsigned _logp); + +/*Decodes a symbol given an "inverse" CDF table. + No call to ec_dec_update() is necessary after this call. + _icdf: The "inverse" CDF, such that symbol s falls in the range + [s>0?ft-_icdf[s-1]:0,ft-_icdf[s]), where ft=1<<_ftb. + The values must be monotonically non-increasing, and the last value + must be 0. + _ftb: The number of bits of precision in the cumulative distribution. + Return: The decoded symbol s.*/ +int ec_dec_icdf(ec_dec *_this,const unsigned char *_icdf,unsigned _ftb); + +/*Decodes a symbol given an "inverse" CDF table. + No call to ec_dec_update() is necessary after this call. + _icdf: The "inverse" CDF, such that symbol s falls in the range + [s>0?ft-_icdf[s-1]:0,ft-_icdf[s]), where ft=1<<_ftb. + The values must be monotonically non-increasing, and the last value + must be 0. + _ftb: The number of bits of precision in the cumulative distribution. + Return: The decoded symbol s.*/ +int ec_dec_icdf16(ec_dec *_this,const opus_uint16 *_icdf,unsigned _ftb); + +/*Extracts a raw unsigned integer with a non-power-of-2 range from the stream. + The bits must have been encoded with ec_enc_uint(). + No call to ec_dec_update() is necessary after this call. + _ft: The number of integers that can be decoded (one more than the max). + This must be at least 2, and no more than 2**32-1. + Return: The decoded bits.*/ +opus_uint32 ec_dec_uint(ec_dec *_this,opus_uint32 _ft); + +/*Extracts a sequence of raw bits from the stream. + The bits must have been encoded with ec_enc_bits(). + No call to ec_dec_update() is necessary after this call. + _ftb: The number of bits to extract. + This must be between 0 and 25, inclusive. + Return: The decoded bits.*/ +opus_uint32 ec_dec_bits(ec_dec *_this,unsigned _ftb); + +#endif diff --git a/port/third_party/opus/celt/entenc.c b/port/third_party/opus/celt/entenc.c new file mode 100644 index 00000000..69c6f835 --- /dev/null +++ b/port/third_party/opus/celt/entenc.c @@ -0,0 +1,305 @@ +/* Copyright (c) 2001-2011 Timothy B. Terriberry + Copyright (c) 2008-2009 Xiph.Org Foundation */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif +#include "os_support.h" +#include "arch.h" +#include "entenc.h" +#include "mfrngcod.h" + +/*A range encoder. + See entdec.c and the references for implementation details \cite{Mar79,MNW98}. + + @INPROCEEDINGS{Mar79, + author="Martin, G.N.N.", + title="Range encoding: an algorithm for removing redundancy from a digitised + message", + booktitle="Video \& Data Recording Conference", + year=1979, + address="Southampton", + month=Jul + } + @ARTICLE{MNW98, + author="Alistair Moffat and Radford Neal and Ian H. Witten", + title="Arithmetic Coding Revisited", + journal="{ACM} Transactions on Information Systems", + year=1998, + volume=16, + number=3, + pages="256--294", + month=Jul, + URL="http://www.stanford.edu/class/ee398/handouts/papers/Moffat98ArithmCoding.pdf" + }*/ + +static int ec_write_byte(ec_enc *_this,unsigned _value){ + if(_this->offs+_this->end_offs>=_this->storage)return -1; + _this->buf[_this->offs++]=(unsigned char)_value; + return 0; +} + +static int ec_write_byte_at_end(ec_enc *_this,unsigned _value){ + if(_this->offs+_this->end_offs>=_this->storage)return -1; + _this->buf[_this->storage-++(_this->end_offs)]=(unsigned char)_value; + return 0; +} + +/*Outputs a symbol, with a carry bit. + If there is a potential to propagate a carry over several symbols, they are + buffered until it can be determined whether or not an actual carry will + occur. + If the counter for the buffered symbols overflows, then the stream becomes + undecodable. + This gives a theoretical limit of a few billion symbols in a single packet on + 32-bit systems. + The alternative is to truncate the range in order to force a carry, but + requires similar carry tracking in the decoder, needlessly slowing it down.*/ +static void ec_enc_carry_out(ec_enc *_this,int _c){ + if(_c!=EC_SYM_MAX){ + /*No further carry propagation possible, flush buffer.*/ + int carry; + carry=_c>>EC_SYM_BITS; + /*Don't output a byte on the first write. + This compare should be taken care of by branch-prediction thereafter.*/ + if(_this->rem>=0)_this->error|=ec_write_byte(_this,_this->rem+carry); + if(_this->ext>0){ + unsigned sym; + sym=(EC_SYM_MAX+carry)&EC_SYM_MAX; + do _this->error|=ec_write_byte(_this,sym); + while(--(_this->ext)>0); + } + _this->rem=_c&EC_SYM_MAX; + } + else _this->ext++; +} + +static OPUS_INLINE void ec_enc_normalize(ec_enc *_this){ + /*If the range is too small, output some bits and rescale it.*/ + while(_this->rng<=EC_CODE_BOT){ + ec_enc_carry_out(_this,(int)(_this->val>>EC_CODE_SHIFT)); + /*Move the next-to-high-order symbol into the high-order position.*/ + _this->val=(_this->val<rng<<=EC_SYM_BITS; + _this->nbits_total+=EC_SYM_BITS; + } +} + +void ec_enc_init(ec_enc *_this,unsigned char *_buf,opus_uint32 _size){ + _this->buf=_buf; + _this->end_offs=0; + _this->end_window=0; + _this->nend_bits=0; + /*This is the offset from which ec_tell() will subtract partial bits.*/ + _this->nbits_total=EC_CODE_BITS+1; + _this->offs=0; + _this->rng=EC_CODE_TOP; + _this->rem=-1; + _this->val=0; + _this->ext=0; + _this->storage=_size; + _this->error=0; +} + +void ec_encode(ec_enc *_this,unsigned _fl,unsigned _fh,unsigned _ft){ + opus_uint32 r; + r=celt_udiv(_this->rng,_ft); + if(_fl>0){ + _this->val+=_this->rng-IMUL32(r,(_ft-_fl)); + _this->rng=IMUL32(r,(_fh-_fl)); + } + else _this->rng-=IMUL32(r,(_ft-_fh)); + ec_enc_normalize(_this); +} + +void ec_encode_bin(ec_enc *_this,unsigned _fl,unsigned _fh,unsigned _bits){ + opus_uint32 r; + r=_this->rng>>_bits; + if(_fl>0){ + _this->val+=_this->rng-IMUL32(r,((1U<<_bits)-_fl)); + _this->rng=IMUL32(r,(_fh-_fl)); + } + else _this->rng-=IMUL32(r,((1U<<_bits)-_fh)); + ec_enc_normalize(_this); +} + +/*The probability of having a "one" is 1/(1<<_logp).*/ +void ec_enc_bit_logp(ec_enc *_this,int _val,unsigned _logp){ + opus_uint32 r; + opus_uint32 s; + opus_uint32 l; + r=_this->rng; + l=_this->val; + s=r>>_logp; + r-=s; + if(_val)_this->val=l+r; + _this->rng=_val?s:r; + ec_enc_normalize(_this); +} + +void ec_enc_icdf(ec_enc *_this,int _s,const unsigned char *_icdf,unsigned _ftb){ + opus_uint32 r; + r=_this->rng>>_ftb; + if(_s>0){ + _this->val+=_this->rng-IMUL32(r,_icdf[_s-1]); + _this->rng=IMUL32(r,_icdf[_s-1]-_icdf[_s]); + } + else _this->rng-=IMUL32(r,_icdf[_s]); + ec_enc_normalize(_this); +} + +void ec_enc_icdf16(ec_enc *_this,int _s,const opus_uint16 *_icdf,unsigned _ftb){ + opus_uint32 r; + r=_this->rng>>_ftb; + if(_s>0){ + _this->val+=_this->rng-IMUL32(r,_icdf[_s-1]); + _this->rng=IMUL32(r,_icdf[_s-1]-_icdf[_s]); + } + else _this->rng-=IMUL32(r,_icdf[_s]); + ec_enc_normalize(_this); +} + +void ec_enc_uint(ec_enc *_this,opus_uint32 _fl,opus_uint32 _ft){ + unsigned ft; + unsigned fl; + int ftb; + /*In order to optimize EC_ILOG(), it is undefined for the value 0.*/ + celt_assert(_ft>1); + _ft--; + ftb=EC_ILOG(_ft); + if(ftb>EC_UINT_BITS){ + ftb-=EC_UINT_BITS; + ft=(_ft>>ftb)+1; + fl=(unsigned)(_fl>>ftb); + ec_encode(_this,fl,fl+1,ft); + ec_enc_bits(_this,_fl&(((opus_uint32)1<end_window; + used=_this->nend_bits; + celt_assert(_bits>0); + if(used+_bits>EC_WINDOW_SIZE){ + do{ + _this->error|=ec_write_byte_at_end(_this,(unsigned)window&EC_SYM_MAX); + window>>=EC_SYM_BITS; + used-=EC_SYM_BITS; + } + while(used>=EC_SYM_BITS); + } + window|=(ec_window)_fl<end_window=window; + _this->nend_bits=used; + _this->nbits_total+=_bits; +} + +void ec_enc_patch_initial_bits(ec_enc *_this,unsigned _val,unsigned _nbits){ + int shift; + unsigned mask; + celt_assert(_nbits<=EC_SYM_BITS); + shift=EC_SYM_BITS-_nbits; + mask=((1<<_nbits)-1)<offs>0){ + /*The first byte has been finalized.*/ + _this->buf[0]=(unsigned char)((_this->buf[0]&~mask)|_val<rem>=0){ + /*The first byte is still awaiting carry propagation.*/ + _this->rem=(_this->rem&~mask)|_val<rng<=(EC_CODE_TOP>>_nbits)){ + /*The renormalization loop has never been run.*/ + _this->val=(_this->val&~((opus_uint32)mask<error=-1; +} + +void ec_enc_shrink(ec_enc *_this,opus_uint32 _size){ + celt_assert(_this->offs+_this->end_offs<=_size); + OPUS_MOVE(_this->buf+_size-_this->end_offs, + _this->buf+_this->storage-_this->end_offs,_this->end_offs); + _this->storage=_size; +} + +void ec_enc_done(ec_enc *_this){ + ec_window window; + int used; + opus_uint32 msk; + opus_uint32 end; + int l; + /*We output the minimum number of bits that ensures that the symbols encoded + thus far will be decoded correctly regardless of the bits that follow.*/ + l=EC_CODE_BITS-EC_ILOG(_this->rng); + msk=(EC_CODE_TOP-1)>>l; + end=(_this->val+msk)&~msk; + if((end|msk)>=_this->val+_this->rng){ + l++; + msk>>=1; + end=(_this->val+msk)&~msk; + } + while(l>0){ + ec_enc_carry_out(_this,(int)(end>>EC_CODE_SHIFT)); + end=(end<rem>=0||_this->ext>0)ec_enc_carry_out(_this,0); + /*If we have buffered extra bits, flush them as well.*/ + window=_this->end_window; + used=_this->nend_bits; + while(used>=EC_SYM_BITS){ + _this->error|=ec_write_byte_at_end(_this,(unsigned)window&EC_SYM_MAX); + window>>=EC_SYM_BITS; + used-=EC_SYM_BITS; + } + /*Clear any excess space and add any remaining extra bits to the last byte.*/ + if(!_this->error){ + OPUS_CLEAR(_this->buf+_this->offs, + _this->storage-_this->offs-_this->end_offs); + if(used>0){ + /*If there's no range coder data at all, give up.*/ + if(_this->end_offs>=_this->storage)_this->error=-1; + else{ + l=-l; + /*If we've busted, don't add too many extra bits to the last byte; it + would corrupt the range coder data, and that's more important.*/ + if(_this->offs+_this->end_offs>=_this->storage&&lerror=-1; + } + _this->buf[_this->storage-_this->end_offs-1]|=(unsigned char)window; + } + } + } +} diff --git a/port/third_party/opus/celt/entenc.h b/port/third_party/opus/celt/entenc.h new file mode 100644 index 00000000..010874bb --- /dev/null +++ b/port/third_party/opus/celt/entenc.h @@ -0,0 +1,119 @@ +/* Copyright (c) 2001-2011 Timothy B. Terriberry + Copyright (c) 2008-2009 Xiph.Org Foundation */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#if !defined(_entenc_H) +# define _entenc_H (1) +# include +# include "entcode.h" + +/*Initializes the encoder. + _buf: The buffer to store output bytes in. + _size: The size of the buffer, in chars.*/ +void ec_enc_init(ec_enc *_this,unsigned char *_buf,opus_uint32 _size); +/*Encodes a symbol given its frequency information. + The frequency information must be discernable by the decoder, assuming it + has read only the previous symbols from the stream. + It is allowable to change the frequency information, or even the entire + source alphabet, so long as the decoder can tell from the context of the + previously encoded information that it is supposed to do so as well. + _fl: The cumulative frequency of all symbols that come before the one to be + encoded. + _fh: The cumulative frequency of all symbols up to and including the one to + be encoded. + Together with _fl, this defines the range [_fl,_fh) in which the + decoded value will fall. + _ft: The sum of the frequencies of all the symbols*/ +void ec_encode(ec_enc *_this,unsigned _fl,unsigned _fh,unsigned _ft); + +/*Equivalent to ec_encode() with _ft==1<<_bits.*/ +void ec_encode_bin(ec_enc *_this,unsigned _fl,unsigned _fh,unsigned _bits); + +/* Encode a bit that has a 1/(1<<_logp) probability of being a one */ +void ec_enc_bit_logp(ec_enc *_this,int _val,unsigned _logp); + +/*Encodes a symbol given an "inverse" CDF table. + _s: The index of the symbol to encode. + _icdf: The "inverse" CDF, such that symbol _s falls in the range + [_s>0?ft-_icdf[_s-1]:0,ft-_icdf[_s]), where ft=1<<_ftb. + The values must be monotonically non-increasing, and the last value + must be 0. + _ftb: The number of bits of precision in the cumulative distribution.*/ +void ec_enc_icdf(ec_enc *_this,int _s,const unsigned char *_icdf,unsigned _ftb); + +/*Encodes a symbol given an "inverse" CDF table. + _s: The index of the symbol to encode. + _icdf: The "inverse" CDF, such that symbol _s falls in the range + [_s>0?ft-_icdf[_s-1]:0,ft-_icdf[_s]), where ft=1<<_ftb. + The values must be monotonically non-increasing, and the last value + must be 0. + _ftb: The number of bits of precision in the cumulative distribution.*/ +void ec_enc_icdf16(ec_enc *_this,int _s,const opus_uint16 *_icdf,unsigned _ftb); + +/*Encodes a raw unsigned integer in the stream. + _fl: The integer to encode. + _ft: The number of integers that can be encoded (one more than the max). + This must be at least 2, and no more than 2**32-1.*/ +void ec_enc_uint(ec_enc *_this,opus_uint32 _fl,opus_uint32 _ft); + +/*Encodes a sequence of raw bits in the stream. + _fl: The bits to encode. + _ftb: The number of bits to encode. + This must be between 1 and 25, inclusive.*/ +void ec_enc_bits(ec_enc *_this,opus_uint32 _fl,unsigned _ftb); + +/*Overwrites a few bits at the very start of an existing stream, after they + have already been encoded. + This makes it possible to have a few flags up front, where it is easy for + decoders to access them without parsing the whole stream, even if their + values are not determined until late in the encoding process, without having + to buffer all the intermediate symbols in the encoder. + In order for this to work, at least _nbits bits must have already been + encoded using probabilities that are an exact power of two. + The encoder can verify the number of encoded bits is sufficient, but cannot + check this latter condition. + _val: The bits to encode (in the least _nbits significant bits). + They will be decoded in order from most-significant to least. + _nbits: The number of bits to overwrite. + This must be no more than 8.*/ +void ec_enc_patch_initial_bits(ec_enc *_this,unsigned _val,unsigned _nbits); + +/*Compacts the data to fit in the target size. + This moves up the raw bits at the end of the current buffer so they are at + the end of the new buffer size. + The caller must ensure that the amount of data that's already been written + will fit in the new size. + _size: The number of bytes in the new buffer. + This must be large enough to contain the bits already written, and + must be no larger than the existing size.*/ +void ec_enc_shrink(ec_enc *_this,opus_uint32 _size); + +/*Indicates that there are no more symbols to encode. + All reamining output bytes are flushed to the output buffer. + ec_enc_init() must be called before the encoder can be used again.*/ +void ec_enc_done(ec_enc *_this); + +#endif diff --git a/port/third_party/opus/celt/fixed_debug.h b/port/third_party/opus/celt/fixed_debug.h new file mode 100644 index 00000000..ef2e5d02 --- /dev/null +++ b/port/third_party/opus/celt/fixed_debug.h @@ -0,0 +1,836 @@ +/* Copyright (C) 2003-2008 Jean-Marc Valin + Copyright (C) 2007-2012 Xiph.Org Foundation */ +/** + @file fixed_debug.h + @brief Fixed-point operations with debugging +*/ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef FIXED_DEBUG_H +#define FIXED_DEBUG_H + +#include +#include "opus_defines.h" + +#ifdef CELT_C +OPUS_EXPORT opus_int64 celt_mips=0; +#else +extern opus_int64 celt_mips; +#endif + +#define MULT16_16SU(a,b) ((opus_val32)(opus_val16)(a)*(opus_val32)(opus_uint16)(b)) +#define MULT32_32_Q31(a,b) ADD32(ADD32(SHL32(MULT16_16(SHR32((a),16),SHR((b),16)),1), SHR32(MULT16_16SU(SHR32((a),16),((b)&0x0000ffff)),15)), SHR32(MULT16_16SU(SHR32((b),16),((a)&0x0000ffff)),15)) + +/** 16x32 multiplication, followed by a 16-bit shift right. Results fits in 32 bits */ +#define MULT16_32_Q16(a,b) ADD32(MULT16_16((a),SHR32((b),16)), SHR32(MULT16_16SU((a),((b)&0x0000ffff)),16)) + +#define MULT16_32_P16(a,b) MULT16_32_PX(a,b,16) + +#define QCONST16(x,bits) ((opus_val16)(.5+(x)*(((opus_val32)1)<<(bits)))) +#define QCONST32(x,bits) ((opus_val32)(.5+(x)*(((opus_val32)1)<<(bits)))) + +#define VERIFY_SHORT(x) ((x)<=32767&&(x)>=-32768) +#define VERIFY_INT(x) ((x)<=2147483647LL&&(x)>=-2147483648LL) +#define VERIFY_UINT(x) ((x)<=(2147483647LLU<<1)) + +#define SHR(a,b) SHR32(a,b) +#define PSHR(a,b) PSHR32(a,b) + +/** Add two 32-bit values, ignore any overflows */ +#define ADD32_ovflw(a,b) (celt_mips+=2,(opus_val32)((opus_uint32)(a)+(opus_uint32)(b))) +/** Subtract two 32-bit values, ignore any overflows */ +#define SUB32_ovflw(a,b) (celt_mips+=2,(opus_val32)((opus_uint32)(a)-(opus_uint32)(b))) +/* Avoid MSVC warning C4146: unary minus operator applied to unsigned type */ +/** Negate 32-bit value, ignore any overflows */ +#define NEG32_ovflw(a) (celt_mips+=2,(opus_val32)(0-(opus_uint32)(a))) + +static OPUS_INLINE short NEG16(int x) +{ + int res; + if (!VERIFY_SHORT(x)) + { + fprintf (stderr, "NEG16: input is not short: %d\n", (int)x); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = -x; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "NEG16: output is not short: %d\n", (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips++; + return res; +} +static OPUS_INLINE int NEG32(opus_int64 x) +{ + opus_int64 res; + if (!VERIFY_INT(x)) + { + fprintf (stderr, "NEG16: input is not int: %d\n", (int)x); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = -x; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "NEG16: output is not int: %d\n", (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=2; + return res; +} + +#define EXTRACT16(x) EXTRACT16_(x, __FILE__, __LINE__) +static OPUS_INLINE short EXTRACT16_(int x, char *file, int line) +{ + int res; + if (!VERIFY_SHORT(x)) + { + fprintf (stderr, "EXTRACT16: input is not short: %d in %s: line %d\n", x, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = x; + celt_mips++; + return res; +} + +#define EXTEND32(x) EXTEND32_(x, __FILE__, __LINE__) +static OPUS_INLINE int EXTEND32_(int x, char *file, int line) +{ + int res; + if (!VERIFY_SHORT(x)) + { + fprintf (stderr, "EXTEND32: input is not short: %d in %s: line %d\n", x, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = x; + celt_mips++; + return res; +} + +#define SHR16(a, shift) SHR16_(a, shift, __FILE__, __LINE__) +static OPUS_INLINE short SHR16_(int a, int shift, char *file, int line) +{ + int res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(shift)) + { + fprintf (stderr, "SHR16: inputs are not short: %d >> %d in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = a>>shift; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "SHR16: output is not short: %d in %s: line %d\n", res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips++; + return res; +} +#define SHL16(a, shift) SHL16_(a, shift, __FILE__, __LINE__) +static OPUS_INLINE short SHL16_(int a, int shift, char *file, int line) +{ + opus_int32 res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(shift)) + { + fprintf (stderr, "SHL16: inputs are not short: %d %d in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = (opus_int32)((opus_uint32)a<>shift; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "SHR32: output is not int: %d\n", (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=2; + return res; +} +#define SHL32(a, shift) SHL32_(a, shift, __FILE__, __LINE__) +static OPUS_INLINE int SHL32_(opus_int64 a, int shift, char *file, int line) +{ + opus_int64 res; + if (!VERIFY_INT(a) || !VERIFY_SHORT(shift)) + { + fprintf (stderr, "SHL32: inputs are not int: %lld %d in %s: line %d\n", (long long)a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = (opus_int64)((opus_uint64)a<>1))),shift)) +#define VSHR32(a, shift) (((shift)>0) ? SHR32(a, shift) : SHL32(a, -(shift))) + +#define ROUND16(x,a) (celt_mips--,EXTRACT16(PSHR32((x),(a)))) +#define SROUND16(x,a) (celt_mips--,EXTRACT16(SATURATE(PSHR32(x,a), 32767))); + +#define HALF16(x) (SHR16(x,1)) +#define HALF32(x) (SHR32(x,1)) + +#define ADD16(a, b) ADD16_(a, b, __FILE__, __LINE__) +static OPUS_INLINE short ADD16_(int a, int b, char *file, int line) +{ + int res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "ADD16: inputs are not short: %d %d in %s: line %d\n", a, b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = a+b; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "ADD16: output is not short: %d+%d=%d in %s: line %d\n", a,b,res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips++; + return res; +} + +#define SUB16(a, b) SUB16_(a, b, __FILE__, __LINE__) +static OPUS_INLINE short SUB16_(int a, int b, char *file, int line) +{ + int res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "SUB16: inputs are not short: %d %d in %s: line %d\n", a, b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = a-b; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "SUB16: output is not short: %d in %s: line %d\n", res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips++; + return res; +} + +#define ADD32(a, b) ADD32_(a, b, __FILE__, __LINE__) +static OPUS_INLINE int ADD32_(opus_int64 a, opus_int64 b, char *file, int line) +{ + opus_int64 res; + if (!VERIFY_INT(a) || !VERIFY_INT(b)) + { + fprintf (stderr, "ADD32: inputs are not int: %d %d in %s: line %d\n", (int)a, (int)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = a+b; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "ADD32: output is not int: %d in %s: line %d\n", (int)res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=2; + return res; +} + +#define SUB32(a, b) SUB32_(a, b, __FILE__, __LINE__) +static OPUS_INLINE int SUB32_(opus_int64 a, opus_int64 b, char *file, int line) +{ + opus_int64 res; + if (!VERIFY_INT(a) || !VERIFY_INT(b)) + { + fprintf (stderr, "SUB32: inputs are not int: %d %d in %s: line %d\n", (int)a, (int)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = a-b; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "SUB32: output is not int: %d in %s: line %d\n", (int)res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=2; + return res; +} + +#undef UADD32 +#define UADD32(a, b) UADD32_(a, b, __FILE__, __LINE__) +static OPUS_INLINE unsigned int UADD32_(opus_uint64 a, opus_uint64 b, char *file, int line) +{ + opus_uint64 res; + if (!VERIFY_UINT(a) || !VERIFY_UINT(b)) + { + fprintf (stderr, "UADD32: inputs are not uint32: %llu %llu in %s: line %d\n", (unsigned long long)a, (unsigned long long)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = a+b; + if (!VERIFY_UINT(res)) + { + fprintf (stderr, "UADD32: output is not uint32: %llu in %s: line %d\n", (unsigned long long)res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=2; + return res; +} + +#undef USUB32 +#define USUB32(a, b) USUB32_(a, b, __FILE__, __LINE__) +static OPUS_INLINE unsigned int USUB32_(opus_uint64 a, opus_uint64 b, char *file, int line) +{ + opus_uint64 res; + if (!VERIFY_UINT(a) || !VERIFY_UINT(b)) + { + fprintf (stderr, "USUB32: inputs are not uint32: %llu %llu in %s: line %d\n", (unsigned long long)a, (unsigned long long)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + if (a> 16; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "MULT32_32_Q16: output is not int: %lld*%lld=%lld\n", (long long)a, (long long)b, (long long)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=5; + return res; +} + +#define MULT16_16(a, b) MULT16_16_(a, b, __FILE__, __LINE__) +static OPUS_INLINE int MULT16_16_(int a, int b, char *file, int line) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "MULT16_16: inputs are not short: %d %d in %s: line %d\n", a, b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = ((opus_int64)a)*b; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "MULT16_16: output is not int: %d in %s: line %d\n", (int)res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips++; + return res; +} + +#define MAC16_16(c,a,b) (celt_mips-=2,ADD32((c),MULT16_16((a),(b)))) + +#define MULT16_32_QX(a, b, Q) MULT16_32_QX_(a, b, Q, __FILE__, __LINE__) +static OPUS_INLINE int MULT16_32_QX_(int a, opus_int64 b, int Q, char *file, int line) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_INT(b)) + { + fprintf (stderr, "MULT16_32_Q%d: inputs are not short+int: %d %d in %s: line %d\n", Q, (int)a, (int)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + if (ABS32(b)>=((opus_int64)(1)<<(16+Q))) + { + fprintf (stderr, "MULT16_32_Q%d: second operand too large: %d %d in %s: line %d\n", Q, (int)a, (int)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = (((opus_int64)a)*(opus_int64)b) >> Q; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "MULT16_32_Q%d: output is not int: %d*%d=%d in %s: line %d\n", Q, (int)a, (int)b,(int)res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + if (Q==15) + celt_mips+=3; + else + celt_mips+=4; + return res; +} + +#define MULT16_32_PX(a, b, Q) MULT16_32_PX_(a, b, Q, __FILE__, __LINE__) +static OPUS_INLINE int MULT16_32_PX_(int a, opus_int64 b, int Q, char *file, int line) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_INT(b)) + { + fprintf (stderr, "MULT16_32_P%d: inputs are not short+int: %d %d in %s: line %d\n\n", Q, (int)a, (int)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + if (ABS32(b)>=((opus_int64)(1)<<(16+Q))) + { + fprintf (stderr, "MULT16_32_Q%d: second operand too large: %d %d in %s: line %d\n\n", Q, (int)a, (int)b,file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = ((((opus_int64)a)*(opus_int64)b) + (((opus_val32)(1)<>1))>> Q; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "MULT16_32_P%d: output is not int: %d*%d=%d in %s: line %d\n\n", Q, (int)a, (int)b,(int)res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + if (Q==15) + celt_mips+=4; + else + celt_mips+=5; + return res; +} + +#define MULT16_32_Q15(a,b) MULT16_32_QX(a,b,15) +#define MAC16_32_Q15(c,a,b) (celt_mips-=2,ADD32((c),MULT16_32_Q15((a),(b)))) +#define MAC16_32_Q16(c,a,b) (celt_mips-=2,ADD32((c),MULT16_32_Q16((a),(b)))) + +static OPUS_INLINE int SATURATE(int a, int b) +{ + if (a>b) + a=b; + if (a<-b) + a = -b; + celt_mips+=3; + return a; +} + +static OPUS_INLINE opus_int16 SATURATE16(opus_int32 a) +{ + celt_mips+=3; + if (a>32767) + return 32767; + else if (a<-32768) + return -32768; + else return a; +} + +static OPUS_INLINE int MULT16_16_Q11_32(int a, int b) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "MULT16_16_Q11: inputs are not short: %d %d\n", a, b); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = ((opus_int64)a)*b; + res >>= 11; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "MULT16_16_Q11: output is not short: %d*%d=%d\n", (int)a, (int)b, (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=3; + return res; +} +static OPUS_INLINE short MULT16_16_Q13(int a, int b) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "MULT16_16_Q13: inputs are not short: %d %d\n", a, b); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = ((opus_int64)a)*b; + res >>= 13; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "MULT16_16_Q13: output is not short: %d*%d=%d\n", a, b, (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=3; + return res; +} +static OPUS_INLINE short MULT16_16_Q14(int a, int b) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "MULT16_16_Q14: inputs are not short: %d %d\n", a, b); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = ((opus_int64)a)*b; + res >>= 14; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "MULT16_16_Q14: output is not short: %d\n", (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=3; + return res; +} + +#define MULT16_16_Q15(a, b) MULT16_16_Q15_(a, b, __FILE__, __LINE__) +static OPUS_INLINE short MULT16_16_Q15_(int a, int b, char *file, int line) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "MULT16_16_Q15: inputs are not short: %d %d in %s: line %d\n", a, b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = ((opus_int64)a)*b; + res >>= 15; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "MULT16_16_Q15: output is not short: %d in %s: line %d\n", (int)res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=1; + return res; +} + +static OPUS_INLINE short MULT16_16_P13(int a, int b) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "MULT16_16_P13: inputs are not short: %d %d\n", a, b); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = ((opus_int64)a)*b; + res += 4096; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "MULT16_16_P13: overflow: %d*%d=%d\n", a, b, (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res >>= 13; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "MULT16_16_P13: output is not short: %d*%d=%d\n", a, b, (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=4; + return res; +} +static OPUS_INLINE short MULT16_16_P14(int a, int b) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "MULT16_16_P14: inputs are not short: %d %d\n", a, b); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = ((opus_int64)a)*b; + res += 8192; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "MULT16_16_P14: overflow: %d*%d=%d\n", a, b, (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res >>= 14; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "MULT16_16_P14: output is not short: %d*%d=%d\n", a, b, (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=4; + return res; +} +static OPUS_INLINE short MULT16_16_P15(int a, int b) +{ + opus_int64 res; + if (!VERIFY_SHORT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "MULT16_16_P15: inputs are not short: %d %d\n", a, b); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = ((opus_int64)a)*b; + res += 16384; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "MULT16_16_P15: overflow: %d*%d=%d\n", a, b, (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res >>= 15; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "MULT16_16_P15: output is not short: %d*%d=%d\n", a, b, (int)res); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=2; + return res; +} + +#define DIV32_16(a, b) DIV32_16_(a, b, __FILE__, __LINE__) + +static OPUS_INLINE int DIV32_16_(opus_int64 a, opus_int64 b, char *file, int line) +{ + opus_int64 res; + if (b==0) + { + fprintf(stderr, "DIV32_16: divide by zero: %d/%d in %s: line %d\n", (int)a, (int)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + return 0; + } + if (!VERIFY_INT(a) || !VERIFY_SHORT(b)) + { + fprintf (stderr, "DIV32_16: inputs are not int/short: %d %d in %s: line %d\n", (int)a, (int)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = a/b; + if (!VERIFY_SHORT(res)) + { + fprintf (stderr, "DIV32_16: output is not short: %d / %d = %d in %s: line %d\n", (int)a,(int)b,(int)res, file, line); + if (res>32767) + res = 32767; + if (res<-32768) + res = -32768; +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=35; + return res; +} + +#define DIV32(a, b) DIV32_(a, b, __FILE__, __LINE__) +static OPUS_INLINE int DIV32_(opus_int64 a, opus_int64 b, char *file, int line) +{ + opus_int64 res; + if (b==0) + { + fprintf(stderr, "DIV32: divide by zero: %d/%d in %s: line %d\n", (int)a, (int)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + return 0; + } + + if (!VERIFY_INT(a) || !VERIFY_INT(b)) + { + fprintf (stderr, "DIV32: inputs are not int/short: %d %d in %s: line %d\n", (int)a, (int)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + res = a/b; + if (!VERIFY_INT(res)) + { + fprintf (stderr, "DIV32: output is not int: %d in %s: line %d\n", (int)res, file, line); +#ifdef FIXED_DEBUG_ASSERT + celt_assert(0); +#endif + } + celt_mips+=70; + return res; +} + +static OPUS_INLINE opus_val16 SIG2WORD16_generic(celt_sig x) +{ + x = PSHR32(x, SIG_SHIFT); + x = MAX32(x, -32768); + x = MIN32(x, 32767); + return EXTRACT16(x); +} +#define SIG2WORD16(x) (SIG2WORD16_generic(x)) + + +#undef PRINT_MIPS +#define PRINT_MIPS(file) do {fprintf (file, "total complexity = %llu MIPS\n", (unsigned long long)celt_mips);} while (0); + +#endif diff --git a/port/third_party/opus/celt/fixed_generic.h b/port/third_party/opus/celt/fixed_generic.h new file mode 100644 index 00000000..8f29d46b --- /dev/null +++ b/port/third_party/opus/celt/fixed_generic.h @@ -0,0 +1,188 @@ +/* Copyright (C) 2007-2009 Xiph.Org Foundation + Copyright (C) 2003-2008 Jean-Marc Valin + Copyright (C) 2007-2008 CSIRO */ +/** + @file fixed_generic.h + @brief Generic fixed-point operations +*/ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef FIXED_GENERIC_H +#define FIXED_GENERIC_H + +/** Multiply a 16-bit signed value by a 16-bit unsigned value. The result is a 32-bit signed value */ +#define MULT16_16SU(a,b) ((opus_val32)(opus_val16)(a)*(opus_val32)(opus_uint16)(b)) + +/** 16x32 multiplication, followed by a 16-bit shift right. Results fits in 32 bits */ +#if OPUS_FAST_INT64 +#define MULT16_32_Q16(a,b) ((opus_val32)SHR((opus_int64)((opus_val16)(a))*(b),16)) +#else +#define MULT16_32_Q16(a,b) ADD32(MULT16_16((a),SHR((b),16)), SHR(MULT16_16SU((a),((b)&0x0000ffff)),16)) +#endif + +/** 16x32 multiplication, followed by a 16-bit shift right (round-to-nearest). Results fits in 32 bits */ +#if OPUS_FAST_INT64 +#define MULT16_32_P16(a,b) ((opus_val32)PSHR((opus_int64)((opus_val16)(a))*(b),16)) +#else +#define MULT16_32_P16(a,b) ADD32(MULT16_16((a),SHR((b),16)), PSHR(MULT16_16SU((a),((b)&0x0000ffff)),16)) +#endif + +/** 16x32 multiplication, followed by a 15-bit shift right. Results fits in 32 bits */ +#if OPUS_FAST_INT64 +#define MULT16_32_Q15(a,b) ((opus_val32)SHR((opus_int64)((opus_val16)(a))*(b),15)) +#else +#define MULT16_32_Q15(a,b) ADD32(SHL(MULT16_16((a),SHR((b),16)),1), SHR(MULT16_16SU((a),((b)&0x0000ffff)),15)) +#endif + +/** 32x32 multiplication, followed by a 16-bit shift right. Results fits in 32 bits */ +#if OPUS_FAST_INT64 +#define MULT32_32_Q16(a,b) ((opus_val32)SHR((opus_int64)(a)*(opus_int64)(b),16)) +#else +#define MULT32_32_Q16(a,b) (ADD32(ADD32(ADD32((opus_val32)(SHR32(((opus_uint32)((a)&0x0000ffff)*(opus_uint32)((b)&0x0000ffff)),16)), MULT16_16SU(SHR32(a,16),((b)&0x0000ffff))), MULT16_16SU(SHR32(b,16),((a)&0x0000ffff))), SHL32(MULT16_16(SHR32(a,16),SHR32(b,16)),16))) +#endif + +/** 32x32 multiplication, followed by a 31-bit shift right. Results fits in 32 bits */ +#if OPUS_FAST_INT64 +#define MULT32_32_Q31(a,b) ((opus_val32)SHR((opus_int64)(a)*(opus_int64)(b),31)) +#else +#define MULT32_32_Q31(a,b) ADD32(ADD32(SHL(MULT16_16(SHR((a),16),SHR((b),16)),1), SHR(MULT16_16SU(SHR((a),16),((b)&0x0000ffff)),15)), SHR(MULT16_16SU(SHR((b),16),((a)&0x0000ffff)),15)) +#endif + +/** Compile-time conversion of float constant to 16-bit value */ +#define QCONST16(x,bits) ((opus_val16)(.5+(x)*(((opus_val32)1)<<(bits)))) + +/** Compile-time conversion of float constant to 32-bit value */ +#define QCONST32(x,bits) ((opus_val32)(.5+(x)*(((opus_val32)1)<<(bits)))) + +/** Negate a 16-bit value */ +#define NEG16(x) (-(x)) +/** Negate a 32-bit value */ +#define NEG32(x) (-(x)) + +/** Change a 32-bit value into a 16-bit value. The value is assumed to fit in 16-bit, otherwise the result is undefined */ +#define EXTRACT16(x) ((opus_val16)(x)) +/** Change a 16-bit value into a 32-bit value */ +#define EXTEND32(x) ((opus_val32)(x)) + +/** Arithmetic shift-right of a 16-bit value */ +#define SHR16(a,shift) ((a) >> (shift)) +/** Arithmetic shift-left of a 16-bit value */ +#define SHL16(a,shift) ((opus_int16)((opus_uint16)(a)<<(shift))) +/** Arithmetic shift-right of a 32-bit value */ +#define SHR32(a,shift) ((a) >> (shift)) +/** Arithmetic shift-left of a 32-bit value */ +#define SHL32(a,shift) ((opus_int32)((opus_uint32)(a)<<(shift))) + +/** 32-bit arithmetic shift right with rounding-to-nearest instead of rounding down */ +#define PSHR32(a,shift) (SHR32((a)+((EXTEND32(1)<<((shift))>>1)),shift)) +/** 32-bit arithmetic shift right where the argument can be negative */ +#define VSHR32(a, shift) (((shift)>0) ? SHR32(a, shift) : SHL32(a, -(shift))) + +/** "RAW" macros, should not be used outside of this header file */ +#define SHR(a,shift) ((a) >> (shift)) +#define SHL(a,shift) SHL32(a,shift) +#define PSHR(a,shift) (SHR((a)+((EXTEND32(1)<<((shift))>>1)),shift)) +#define SATURATE(x,a) (((x)>(a) ? (a) : (x)<-(a) ? -(a) : (x))) + +#define SATURATE16(x) (EXTRACT16((x)>32767 ? 32767 : (x)<-32768 ? -32768 : (x))) + +/** Shift by a and round-to-nearest 32-bit value. Result is a 16-bit value */ +#define ROUND16(x,a) (EXTRACT16(PSHR32((x),(a)))) +/** Shift by a and round-to-nearest 32-bit value. Result is a saturated 16-bit value */ +#define SROUND16(x,a) EXTRACT16(SATURATE(PSHR32(x,a), 32767)); + +/** Divide by two */ +#define HALF16(x) (SHR16(x,1)) +#define HALF32(x) (SHR32(x,1)) + +/** Add two 16-bit values */ +#define ADD16(a,b) ((opus_val16)((opus_val16)(a)+(opus_val16)(b))) +/** Subtract two 16-bit values */ +#define SUB16(a,b) ((opus_val16)(a)-(opus_val16)(b)) +/** Add two 32-bit values */ +#define ADD32(a,b) ((opus_val32)(a)+(opus_val32)(b)) +/** Subtract two 32-bit values */ +#define SUB32(a,b) ((opus_val32)(a)-(opus_val32)(b)) + +/** Add two 32-bit values, ignore any overflows */ +#define ADD32_ovflw(a,b) ((opus_val32)((opus_uint32)(a)+(opus_uint32)(b))) +/** Subtract two 32-bit values, ignore any overflows */ +#define SUB32_ovflw(a,b) ((opus_val32)((opus_uint32)(a)-(opus_uint32)(b))) +/* Avoid MSVC warning C4146: unary minus operator applied to unsigned type */ +/** Negate 32-bit value, ignore any overflows */ +#define NEG32_ovflw(a) ((opus_val32)(0-(opus_uint32)(a))) + +/** 16x16 multiplication where the result fits in 16 bits */ +#define MULT16_16_16(a,b) ((((opus_val16)(a))*((opus_val16)(b)))) + +/** 32x32 multiplication where the result fits in 32 bits */ +#define MULT32_32_32(a,b) ((((opus_val32)(a))*((opus_val32)(b)))) + +/* (opus_val32)(opus_val16) gives TI compiler a hint that it's 16x16->32 multiply */ +/** 16x16 multiplication where the result fits in 32 bits */ +#define MULT16_16(a,b) (((opus_val32)(opus_val16)(a))*((opus_val32)(opus_val16)(b))) + +/** 16x16 multiply-add where the result fits in 32 bits */ +#define MAC16_16(c,a,b) (ADD32((c),MULT16_16((a),(b)))) +/** 16x32 multiply, followed by a 15-bit shift right and 32-bit add. + b must fit in 31 bits. + Result fits in 32 bits. */ +#define MAC16_32_Q15(c,a,b) ADD32((c),ADD32(MULT16_16((a),SHR((b),15)), SHR(MULT16_16((a),((b)&0x00007fff)),15))) + +/** 16x32 multiplication, followed by a 16-bit shift right and 32-bit add. + Results fits in 32 bits */ +#define MAC16_32_Q16(c,a,b) ADD32((c),ADD32(MULT16_16((a),SHR((b),16)), SHR(MULT16_16SU((a),((b)&0x0000ffff)),16))) + +#define MULT16_16_Q11_32(a,b) (SHR(MULT16_16((a),(b)),11)) +#define MULT16_16_Q11(a,b) (SHR(MULT16_16((a),(b)),11)) +#define MULT16_16_Q13(a,b) (SHR(MULT16_16((a),(b)),13)) +#define MULT16_16_Q14(a,b) (SHR(MULT16_16((a),(b)),14)) +#define MULT16_16_Q15(a,b) (SHR(MULT16_16((a),(b)),15)) + +#define MULT16_16_P13(a,b) (SHR(ADD32(4096,MULT16_16((a),(b))),13)) +#define MULT16_16_P14(a,b) (SHR(ADD32(8192,MULT16_16((a),(b))),14)) +#define MULT16_16_P15(a,b) (SHR(ADD32(16384,MULT16_16((a),(b))),15)) + +/** Divide a 32-bit value by a 16-bit value. Result fits in 16 bits */ +#define DIV32_16(a,b) ((opus_val16)(((opus_val32)(a))/((opus_val16)(b)))) + +/** Divide a 32-bit value by a 32-bit value. Result fits in 32 bits */ +#define DIV32(a,b) (((opus_val32)(a))/((opus_val32)(b))) + +#if defined(MIPSr1_ASM) +#include "mips/fixed_generic_mipsr1.h" +#endif + +static OPUS_INLINE opus_val16 SIG2WORD16_generic(celt_sig x) +{ + x = PSHR32(x, SIG_SHIFT); + x = MAX32(x, -32768); + x = MIN32(x, 32767); + return EXTRACT16(x); +} +#define SIG2WORD16(x) (SIG2WORD16_generic(x)) + +#endif diff --git a/port/third_party/opus/celt/float_cast.h b/port/third_party/opus/celt/float_cast.h new file mode 100644 index 00000000..8915a5fd --- /dev/null +++ b/port/third_party/opus/celt/float_cast.h @@ -0,0 +1,152 @@ +/* Copyright (C) 2001 Erik de Castro Lopo */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* Version 1.1 */ + +#ifndef FLOAT_CAST_H +#define FLOAT_CAST_H + + +#include "arch.h" + +/*============================================================================ +** On Intel Pentium processors (especially PIII and probably P4), converting +** from float to int is very slow. To meet the C specs, the code produced by +** most C compilers targeting Pentium needs to change the FPU rounding mode +** before the float to int conversion is performed. +** +** Changing the FPU rounding mode causes the FPU pipeline to be flushed. It +** is this flushing of the pipeline which is so slow. +** +** Fortunately the ISO C99 specifications define the functions lrint, lrintf, +** llrint and llrintf which fix this problem as a side effect. +** +** On Unix-like systems, the configure process should have detected the +** presence of these functions. If they weren't found we have to replace them +** here with a standard C cast. +*/ + +/* +** The C99 prototypes for lrint and lrintf are as follows: +** +** long int lrintf (float x) ; +** long int lrint (double x) ; +*/ + +/* The presence of the required functions are detected during the configure +** process and the values HAVE_LRINT and HAVE_LRINTF are set accordingly in +** the config.h file. +*/ + +/* With GCC, when SSE is available, the fastest conversion is cvtss2si. */ +#if defined(__GNUC__) && defined(__SSE__) + +#include +static OPUS_INLINE opus_int32 float2int(float x) {return _mm_cvt_ss2si(_mm_set_ss(x));} + +#elif (defined(_MSC_VER) && _MSC_VER >= 1400) && (defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 1)) + + #include + static OPUS_INLINE opus_int32 float2int(float value) + { + /* _mm_load_ss will generate same code as _mm_set_ss + ** in _MSC_VER >= 1914 /02 so keep __mm_load__ss + ** for backward compatibility. + */ + return _mm_cvtss_si32(_mm_load_ss(&value)); + } + +#elif (defined(_MSC_VER) && _MSC_VER >= 1400) && defined (_M_IX86) + + #include + + /* Win32 doesn't seem to have these functions. + ** Therefore implement OPUS_INLINE versions of these functions here. + */ + + static OPUS_INLINE opus_int32 + float2int (float flt) + { int intgr; + + _asm + { fld flt + fistp intgr + } ; + + return intgr ; + } + +#elif defined(HAVE_LRINTF) && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L + +/* These defines enable functionality introduced with the 1999 ISO C +** standard. They must be defined before the inclusion of math.h to +** engage them. If optimisation is enabled, these functions will be +** inlined. With optimisation switched off, you have to link in the +** maths library using -lm. +*/ + +#define _ISOC9X_SOURCE 1 +#define _ISOC99_SOURCE 1 + +#define __USE_ISOC9X 1 +#define __USE_ISOC99 1 + +#include +#define float2int(x) lrintf(x) + +#elif defined(HAVE_LRINT) && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L + +#define _ISOC9X_SOURCE 1 +#define _ISOC99_SOURCE 1 + +#define __USE_ISOC9X 1 +#define __USE_ISOC99 1 + +#include +#define float2int(x) lrint(x) + +#else + +#if (defined(__GNUC__) && defined(__STDC__) && __STDC__ && __STDC_VERSION__ >= 199901L) + /* supported by gcc in C99 mode, but not by all other compilers */ + #warning "Don't have the functions lrint() and lrintf ()." + #warning "Replacing these functions with a standard C cast." +#endif /* __STDC_VERSION__ >= 199901L */ + #include + #define float2int(flt) ((int)(floor(.5+flt))) +#endif + +#ifndef DISABLE_FLOAT_API +static OPUS_INLINE opus_int16 FLOAT2INT16(float x) +{ + x = x*CELT_SIG_SCALE; + x = MAX32(x, -32768); + x = MIN32(x, 32767); + return (opus_int16)float2int(x); +} +#endif /* DISABLE_FLOAT_API */ + +#endif /* FLOAT_CAST_H */ diff --git a/port/third_party/opus/celt/kiss_fft.c b/port/third_party/opus/celt/kiss_fft.c new file mode 100644 index 00000000..83775165 --- /dev/null +++ b/port/third_party/opus/celt/kiss_fft.c @@ -0,0 +1,604 @@ +/*Copyright (c) 2003-2004, Mark Borgerding + Lots of modifications by Jean-Marc Valin + Copyright (c) 2005-2007, Xiph.Org Foundation + Copyright (c) 2008, Xiph.Org Foundation, CSIRO + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE.*/ + +/* This code is originally from Mark Borgerding's KISS-FFT but has been + heavily modified to better suit Opus */ + +#ifndef SKIP_CONFIG_H +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif +#endif + +#include "_kiss_fft_guts.h" +#include "arch.h" +#include "os_support.h" +#include "mathops.h" +#include "stack_alloc.h" + +/* The guts header contains all the multiplication and addition macros that are defined for + complex numbers. It also delares the kf_ internal functions. +*/ + +static void kf_bfly2( + kiss_fft_cpx * Fout, + int m, + int N + ) +{ + kiss_fft_cpx * Fout2; + int i; + (void)m; +#ifdef CUSTOM_MODES + if (m==1) + { + celt_assert(m==1); + for (i=0;itwiddles; + /* m is guaranteed to be a multiple of 4. */ + for (j=0;jtwiddles[fstride*m]; +#endif + for (i=0;itwiddles; + /* For non-custom modes, m is guaranteed to be a multiple of 4. */ + k=m; + do { + + C_MUL(scratch[1],Fout[m] , *tw1); + C_MUL(scratch[2],Fout[m2] , *tw2); + + C_ADD(scratch[3],scratch[1],scratch[2]); + C_SUB(scratch[0],scratch[1],scratch[2]); + tw1 += fstride; + tw2 += fstride*2; + + Fout[m].r = SUB32_ovflw(Fout->r, HALF_OF(scratch[3].r)); + Fout[m].i = SUB32_ovflw(Fout->i, HALF_OF(scratch[3].i)); + + C_MULBYSCALAR( scratch[0] , epi3.i ); + + C_ADDTO(*Fout,scratch[3]); + + Fout[m2].r = ADD32_ovflw(Fout[m].r, scratch[0].i); + Fout[m2].i = SUB32_ovflw(Fout[m].i, scratch[0].r); + + Fout[m].r = SUB32_ovflw(Fout[m].r, scratch[0].i); + Fout[m].i = ADD32_ovflw(Fout[m].i, scratch[0].r); + + ++Fout; + } while(--k); + } +} + + +#ifndef OVERRIDE_kf_bfly5 +static void kf_bfly5( + kiss_fft_cpx * Fout, + const size_t fstride, + const kiss_fft_state *st, + int m, + int N, + int mm + ) +{ + kiss_fft_cpx *Fout0,*Fout1,*Fout2,*Fout3,*Fout4; + int i, u; + kiss_fft_cpx scratch[13]; + const kiss_twiddle_cpx *tw; + kiss_twiddle_cpx ya,yb; + kiss_fft_cpx * Fout_beg = Fout; + +#ifdef FIXED_POINT + ya.r = 10126; + ya.i = -31164; + yb.r = -26510; + yb.i = -19261; +#else + ya = st->twiddles[fstride*m]; + yb = st->twiddles[fstride*2*m]; +#endif + tw=st->twiddles; + + for (i=0;ir = ADD32_ovflw(Fout0->r, ADD32_ovflw(scratch[7].r, scratch[8].r)); + Fout0->i = ADD32_ovflw(Fout0->i, ADD32_ovflw(scratch[7].i, scratch[8].i)); + + scratch[5].r = ADD32_ovflw(scratch[0].r, ADD32_ovflw(S_MUL(scratch[7].r,ya.r), S_MUL(scratch[8].r,yb.r))); + scratch[5].i = ADD32_ovflw(scratch[0].i, ADD32_ovflw(S_MUL(scratch[7].i,ya.r), S_MUL(scratch[8].i,yb.r))); + + scratch[6].r = ADD32_ovflw(S_MUL(scratch[10].i,ya.i), S_MUL(scratch[9].i,yb.i)); + scratch[6].i = NEG32_ovflw(ADD32_ovflw(S_MUL(scratch[10].r,ya.i), S_MUL(scratch[9].r,yb.i))); + + C_SUB(*Fout1,scratch[5],scratch[6]); + C_ADD(*Fout4,scratch[5],scratch[6]); + + scratch[11].r = ADD32_ovflw(scratch[0].r, ADD32_ovflw(S_MUL(scratch[7].r,yb.r), S_MUL(scratch[8].r,ya.r))); + scratch[11].i = ADD32_ovflw(scratch[0].i, ADD32_ovflw(S_MUL(scratch[7].i,yb.r), S_MUL(scratch[8].i,ya.r))); + scratch[12].r = SUB32_ovflw(S_MUL(scratch[9].i,ya.i), S_MUL(scratch[10].i,yb.i)); + scratch[12].i = SUB32_ovflw(S_MUL(scratch[10].r,yb.i), S_MUL(scratch[9].r,ya.i)); + + C_ADD(*Fout2,scratch[11],scratch[12]); + C_SUB(*Fout3,scratch[11],scratch[12]); + + ++Fout0;++Fout1;++Fout2;++Fout3;++Fout4; + } + } +} +#endif /* OVERRIDE_kf_bfly5 */ + + +#endif + + +#ifdef CUSTOM_MODES + +static +void compute_bitrev_table( + int Fout, + opus_int16 *f, + const size_t fstride, + int in_stride, + opus_int16 * factors, + const kiss_fft_state *st + ) +{ + const int p=*factors++; /* the radix */ + const int m=*factors++; /* stage's fft length/p */ + + /*printf ("fft %d %d %d %d %d %d\n", p*m, m, p, s2, fstride*in_stride, N);*/ + if (m==1) + { + int j; + for (j=0;j32000 || (opus_int32)p*(opus_int32)p > n) + p = n; /* no more factors, skip to end */ + } + n /= p; +#ifdef RADIX_TWO_ONLY + if (p!=2 && p != 4) +#else + if (p>5) +#endif + { + return 0; + } + facbuf[2*stages] = p; + if (p==2 && stages > 1) + { + facbuf[2*stages] = 4; + facbuf[2] = 2; + } + stages++; + } while (n > 1); + n = nbak; + /* Reverse the order to get the radix 4 at the end, so we can use the + fast degenerate case. It turns out that reversing the order also + improves the noise behaviour. */ + for (i=0;i= memneeded) + st = (kiss_fft_state*)mem; + *lenmem = memneeded; + } + if (st) { + opus_int16 *bitrev; + kiss_twiddle_cpx *twiddles; + + st->nfft=nfft; +#ifdef FIXED_POINT + st->scale_shift = celt_ilog2(st->nfft); + if (st->nfft == 1<scale_shift) + st->scale = Q15ONE; + else + st->scale = (1073741824+st->nfft/2)/st->nfft>>(15-st->scale_shift); +#else + st->scale = 1.f/nfft; +#endif + if (base != NULL) + { + st->twiddles = base->twiddles; + st->shift = 0; + while (st->shift < 32 && nfft<shift != base->nfft) + st->shift++; + if (st->shift>=32) + goto fail; + } else { + st->twiddles = twiddles = (kiss_twiddle_cpx*)KISS_FFT_MALLOC(sizeof(kiss_twiddle_cpx)*nfft); + compute_twiddles(twiddles, nfft); + st->shift = -1; + } + if (!kf_factor(nfft,st->factors)) + { + goto fail; + } + + /* bitrev */ + st->bitrev = bitrev = (opus_int16*)KISS_FFT_MALLOC(sizeof(opus_int16)*nfft); + if (st->bitrev==NULL) + goto fail; + compute_bitrev_table(0, bitrev, 1,1, st->factors,st); + + /* Initialize architecture specific fft parameters */ + if (opus_fft_alloc_arch(st, arch)) + goto fail; + } + return st; +fail: + opus_fft_free(st, arch); + return NULL; +} + +kiss_fft_state *opus_fft_alloc(int nfft,void * mem,size_t * lenmem, int arch) +{ + return opus_fft_alloc_twiddles(nfft, mem, lenmem, NULL, arch); +} + +void opus_fft_free_arch_c(kiss_fft_state *st) { + (void)st; +} + +void opus_fft_free(const kiss_fft_state *cfg, int arch) +{ + if (cfg) + { + opus_fft_free_arch((kiss_fft_state *)cfg, arch); + opus_free((opus_int16*)cfg->bitrev); + if (cfg->shift < 0) + opus_free((kiss_twiddle_cpx*)cfg->twiddles); + opus_free((kiss_fft_state*)cfg); + } +} + +#endif /* CUSTOM_MODES */ + +void opus_fft_impl(const kiss_fft_state *st,kiss_fft_cpx *fout) +{ + int m2, m; + int p; + int L; + int fstride[MAXFACTORS]; + int i; + int shift; + + /* st->shift can be -1 */ + shift = st->shift>0 ? st->shift : 0; + + fstride[0] = 1; + L=0; + do { + p = st->factors[2*L]; + m = st->factors[2*L+1]; + fstride[L+1] = fstride[L]*p; + L++; + } while(m!=1); + m = st->factors[2*L-1]; + for (i=L-1;i>=0;i--) + { + if (i!=0) + m2 = st->factors[2*i-1]; + else + m2 = 1; + switch (st->factors[2*i]) + { + case 2: + kf_bfly2(fout, m, fstride[i]); + break; + case 4: + kf_bfly4(fout,fstride[i]<scale_shift-1; +#endif + scale = st->scale; + + celt_assert2 (fin != fout, "In-place FFT not supported"); + /* Bit-reverse the input */ + for (i=0;infft;i++) + { + kiss_fft_cpx x = fin[i]; + fout[st->bitrev[i]].r = SHR32(MULT16_32_Q16(scale, x.r), scale_shift); + fout[st->bitrev[i]].i = SHR32(MULT16_32_Q16(scale, x.i), scale_shift); + } + opus_fft_impl(st, fout); +} + + +void opus_ifft_c(const kiss_fft_state *st,const kiss_fft_cpx *fin,kiss_fft_cpx *fout) +{ + int i; + celt_assert2 (fin != fout, "In-place FFT not supported"); + /* Bit-reverse the input */ + for (i=0;infft;i++) + fout[st->bitrev[i]] = fin[i]; + for (i=0;infft;i++) + fout[i].i = -fout[i].i; + opus_fft_impl(st, fout); + for (i=0;infft;i++) + fout[i].i = -fout[i].i; +} diff --git a/port/third_party/opus/celt/kiss_fft.h b/port/third_party/opus/celt/kiss_fft.h new file mode 100644 index 00000000..267f72f9 --- /dev/null +++ b/port/third_party/opus/celt/kiss_fft.h @@ -0,0 +1,210 @@ +/*Copyright (c) 2003-2004, Mark Borgerding + Lots of modifications by Jean-Marc Valin + Copyright (c) 2005-2007, Xiph.Org Foundation + Copyright (c) 2008, Xiph.Org Foundation, CSIRO + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE.*/ + +#ifndef KISS_FFT_H +#define KISS_FFT_H + +#include +#include +#include "arch.h" +#include "cpu_support.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef USE_SIMD +# include +# define kiss_fft_scalar __m128 +#define KISS_FFT_MALLOC(nbytes) memalign(16,nbytes) +#else +#define KISS_FFT_MALLOC opus_alloc +#endif + +#ifdef FIXED_POINT +#include "arch.h" + +# define kiss_fft_scalar opus_int32 +# define kiss_twiddle_scalar opus_int16 + +/* Some 32-bit CPUs would load/store a kiss_twiddle_cpx with a single memory + * access, and could benefit from additional alignment. + */ +# define KISS_TWIDDLE_CPX_ALIGNMENT (sizeof(opus_int32)) + +#else +# ifndef kiss_fft_scalar +/* default is float */ +# define kiss_fft_scalar float +# define kiss_twiddle_scalar float +# define KF_SUFFIX _celt_single +# endif +#endif + +#if defined(__GNUC__) && defined(KISS_TWIDDLE_CPX_ALIGNMENT) +#define KISS_TWIDDLE_CPX_ALIGNED __attribute__((aligned(KISS_TWIDDLE_CPX_ALIGNMENT))) +#else +#define KISS_TWIDDLE_CPX_ALIGNED +#endif + +typedef struct { + kiss_fft_scalar r; + kiss_fft_scalar i; +}kiss_fft_cpx; + +typedef struct { + kiss_twiddle_scalar r; + kiss_twiddle_scalar i; +} KISS_TWIDDLE_CPX_ALIGNED kiss_twiddle_cpx; + +#define MAXFACTORS 8 +/* e.g. an fft of length 128 has 4 factors + as far as kissfft is concerned + 4*4*4*2 + */ + +typedef struct arch_fft_state{ + int is_supported; + void *priv; +} arch_fft_state; + +typedef struct kiss_fft_state{ + int nfft; + opus_val16 scale; +#ifdef FIXED_POINT + int scale_shift; +#endif + int shift; + opus_int16 factors[2*MAXFACTORS]; + const opus_int16 *bitrev; + const kiss_twiddle_cpx *twiddles; + arch_fft_state *arch_fft; +} kiss_fft_state; + +#if defined(HAVE_ARM_NE10) +#include "arm/fft_arm.h" +#endif + +/*typedef struct kiss_fft_state* kiss_fft_cfg;*/ + +/** + * opus_fft_alloc + * + * Initialize a FFT (or IFFT) algorithm's cfg/state buffer. + * + * typical usage: kiss_fft_cfg mycfg=opus_fft_alloc(1024,0,NULL,NULL); + * + * The return value from fft_alloc is a cfg buffer used internally + * by the fft routine or NULL. + * + * If lenmem is NULL, then opus_fft_alloc will allocate a cfg buffer using malloc. + * The returned value should be free()d when done to avoid memory leaks. + * + * The state can be placed in a user supplied buffer 'mem': + * If lenmem is not NULL and mem is not NULL and *lenmem is large enough, + * then the function places the cfg in mem and the size used in *lenmem + * and returns mem. + * + * If lenmem is not NULL and ( mem is NULL or *lenmem is not large enough), + * then the function returns NULL and places the minimum cfg + * buffer size in *lenmem. + * */ + +kiss_fft_state *opus_fft_alloc_twiddles(int nfft,void * mem,size_t * lenmem, const kiss_fft_state *base, int arch); + +kiss_fft_state *opus_fft_alloc(int nfft,void * mem,size_t * lenmem, int arch); + +/** + * opus_fft(cfg,in_out_buf) + * + * Perform an FFT on a complex input buffer. + * for a forward FFT, + * fin should be f[0] , f[1] , ... ,f[nfft-1] + * fout will be F[0] , F[1] , ... ,F[nfft-1] + * Note that each element is complex and can be accessed like + f[k].r and f[k].i + * */ +void opus_fft_c(const kiss_fft_state *cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout); +void opus_ifft_c(const kiss_fft_state *cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout); + +void opus_fft_impl(const kiss_fft_state *st,kiss_fft_cpx *fout); +void opus_ifft_impl(const kiss_fft_state *st,kiss_fft_cpx *fout); + +void opus_fft_free(const kiss_fft_state *cfg, int arch); + + +void opus_fft_free_arch_c(kiss_fft_state *st); +int opus_fft_alloc_arch_c(kiss_fft_state *st); + +#if !defined(OVERRIDE_OPUS_FFT) +/* Is run-time CPU detection enabled on this platform? */ +#if defined(OPUS_HAVE_RTCD) && (defined(HAVE_ARM_NE10)) + +extern int (*const OPUS_FFT_ALLOC_ARCH_IMPL[OPUS_ARCHMASK+1])( + kiss_fft_state *st); + +#define opus_fft_alloc_arch(_st, arch) \ + ((*OPUS_FFT_ALLOC_ARCH_IMPL[(arch)&OPUS_ARCHMASK])(_st)) + +extern void (*const OPUS_FFT_FREE_ARCH_IMPL[OPUS_ARCHMASK+1])( + kiss_fft_state *st); +#define opus_fft_free_arch(_st, arch) \ + ((*OPUS_FFT_FREE_ARCH_IMPL[(arch)&OPUS_ARCHMASK])(_st)) + +extern void (*const OPUS_FFT[OPUS_ARCHMASK+1])(const kiss_fft_state *cfg, + const kiss_fft_cpx *fin, kiss_fft_cpx *fout); +#define opus_fft(_cfg, _fin, _fout, arch) \ + ((*OPUS_FFT[(arch)&OPUS_ARCHMASK])(_cfg, _fin, _fout)) + +extern void (*const OPUS_IFFT[OPUS_ARCHMASK+1])(const kiss_fft_state *cfg, + const kiss_fft_cpx *fin, kiss_fft_cpx *fout); +#define opus_ifft(_cfg, _fin, _fout, arch) \ + ((*OPUS_IFFT[(arch)&OPUS_ARCHMASK])(_cfg, _fin, _fout)) + +#else /* else for if defined(OPUS_HAVE_RTCD) && (defined(HAVE_ARM_NE10)) */ + +#define opus_fft_alloc_arch(_st, arch) \ + ((void)(arch), opus_fft_alloc_arch_c(_st)) + +#define opus_fft_free_arch(_st, arch) \ + ((void)(arch), opus_fft_free_arch_c(_st)) + +#define opus_fft(_cfg, _fin, _fout, arch) \ + ((void)(arch), opus_fft_c(_cfg, _fin, _fout)) + +#define opus_ifft(_cfg, _fin, _fout, arch) \ + ((void)(arch), opus_ifft_c(_cfg, _fin, _fout)) + +#endif /* end if defined(OPUS_HAVE_RTCD) && (defined(HAVE_ARM_NE10)) */ +#endif /* end if !defined(OVERRIDE_OPUS_FFT) */ + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/port/third_party/opus/celt/laplace.c b/port/third_party/opus/celt/laplace.c new file mode 100644 index 00000000..21809666 --- /dev/null +++ b/port/third_party/opus/celt/laplace.c @@ -0,0 +1,235 @@ +/* Copyright (c) 2007 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "laplace.h" +#include "mathops.h" + +/* The minimum probability of an energy delta (out of 32768). */ +#define LAPLACE_LOG_MINP (0) +#define LAPLACE_MINP (1<>15; +} + +void ec_laplace_encode(ec_enc *enc, int *value, unsigned fs, int decay) +{ + unsigned fl; + int val = *value; + fl = 0; + if (val) + { + int s; + int i; + s = -(val<0); + val = (val+s)^s; + fl = fs; + fs = ec_laplace_get_freq1(fs, decay); + /* Search the decaying part of the PDF.*/ + for (i=1; fs > 0 && i < val; i++) + { + fs *= 2; + fl += fs+2*LAPLACE_MINP; + fs = (fs*(opus_int32)decay)>>15; + } + /* Everything beyond that has probability LAPLACE_MINP. */ + if (!fs) + { + int di; + int ndi_max; + ndi_max = (32768-fl+LAPLACE_MINP-1)>>LAPLACE_LOG_MINP; + ndi_max = (ndi_max-s)>>1; + di = IMIN(val - i, ndi_max - 1); + fl += (2*di+1+s)*LAPLACE_MINP; + fs = IMIN(LAPLACE_MINP, 32768-fl); + *value = (i+di+s)^s; + } + else + { + fs += LAPLACE_MINP; + fl += fs&~s; + } + celt_assert(fl+fs<=32768); + celt_assert(fs>0); + } + ec_encode_bin(enc, fl, fl+fs, 15); +} + +int ec_laplace_decode(ec_dec *dec, unsigned fs, int decay) +{ + int val=0; + unsigned fl; + unsigned fm; + fm = ec_decode_bin(dec, 15); + fl = 0; + if (fm >= fs) + { + val++; + fl = fs; + fs = ec_laplace_get_freq1(fs, decay)+LAPLACE_MINP; + /* Search the decaying part of the PDF.*/ + while(fs > LAPLACE_MINP && fm >= fl+2*fs) + { + fs *= 2; + fl += fs; + fs = ((fs-2*LAPLACE_MINP)*(opus_int32)decay)>>15; + fs += LAPLACE_MINP; + val++; + } + /* Everything beyond that has probability LAPLACE_MINP. */ + if (fs <= LAPLACE_MINP) + { + int di; + di = (fm-fl)>>(LAPLACE_LOG_MINP+1); + val += di; + fl += 2*di*LAPLACE_MINP; + } + if (fm < fl+fs) + val = -val; + else + fl += fs; + } + celt_assert(fl<32768); + celt_assert(fs>0); + celt_assert(fl<=fm); + celt_assert(fm 0 ? 1 : 2); + ec_enc_icdf16(enc, s, sign_icdf, 15); + value = abs(value); + if (value) + { + int i; + opus_uint16 icdf[8]; + icdf[0] = IMAX(7, decay); + for (i=1;i<7;i++) + { + icdf[i] = IMAX(7-i, (icdf[i-1] * (opus_int32)decay) >> 15); + } + icdf[7] = 0; + value--; + do { + ec_enc_icdf16(enc, IMIN(value, 7), icdf, 15); + value -= 7; + } while (value >= 0); + } +} + +int ec_laplace_decode_p0(ec_dec *dec, opus_uint16 p0, opus_uint16 decay) +{ + int s; + int value; + opus_uint16 sign_icdf[3]; + sign_icdf[0] = 32768-p0; + sign_icdf[1] = sign_icdf[0]/2; + sign_icdf[2] = 0; + s = ec_dec_icdf16(dec, sign_icdf, 15); + if (s==2) s = -1; + if (s != 0) + { + int i; + int v; + opus_uint16 icdf[8]; + icdf[0] = IMAX(7, decay); + for (i=1;i<7;i++) + { + icdf[i] = IMAX(7-i, (icdf[i-1] * (opus_int32)decay) >> 15); + } + icdf[7] = 0; + value = 1; + do { + v = ec_dec_icdf16(dec, icdf, 15); + value += v; + } while (v == 7); + return s*value; + } else return 0; +} + +#if 0 + +#include +#define NB_VALS 10 +#define DATA_SIZE 10000 +int main() { + ec_enc enc; + ec_dec dec; + unsigned char *ptr; + int i; + int decay, p0; + int val[NB_VALS] = {6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + /*for (i=0;i>1; + b=1U<>=1; + bshift--; + } + while(bshift>=0); + return g; +} + +#ifdef FIXED_POINT + +opus_val32 frac_div32(opus_val32 a, opus_val32 b) +{ + opus_val16 rcp; + opus_val32 result, rem; + int shift = celt_ilog2(b)-29; + a = VSHR32(a,shift); + b = VSHR32(b,shift); + /* 16-bit reciprocal */ + rcp = ROUND16(celt_rcp(ROUND16(b,16)),3); + result = MULT16_32_Q15(rcp, a); + rem = PSHR32(a,2)-MULT32_32_Q31(result, b); + result = ADD32(result, SHL32(MULT16_32_Q15(rcp, rem),2)); + if (result >= 536870912) /* 2^29 */ + return 2147483647; /* 2^31 - 1 */ + else if (result <= -536870912) /* -2^29 */ + return -2147483647; /* -2^31 */ + else + return SHL32(result, 2); +} + +/** Reciprocal sqrt approximation in the range [0.25,1) (Q16 in, Q14 out) */ +opus_val16 celt_rsqrt_norm(opus_val32 x) +{ + opus_val16 n; + opus_val16 r; + opus_val16 r2; + opus_val16 y; + /* Range of n is [-16384,32767] ([-0.5,1) in Q15). */ + n = x-32768; + /* Get a rough initial guess for the root. + The optimal minimax quadratic approximation (using relative error) is + r = 1.437799046117536+n*(-0.823394375837328+n*0.4096419668459485). + Coefficients here, and the final result r, are Q14.*/ + r = ADD16(23557, MULT16_16_Q15(n, ADD16(-13490, MULT16_16_Q15(n, 6713)))); + /* We want y = x*r*r-1 in Q15, but x is 32-bit Q16 and r is Q14. + We can compute the result from n and r using Q15 multiplies with some + adjustment, carefully done to avoid overflow. + Range of y is [-1564,1594]. */ + r2 = MULT16_16_Q15(r, r); + y = SHL16(SUB16(ADD16(MULT16_16_Q15(r2, n), r2), 16384), 1); + /* Apply a 2nd-order Householder iteration: r += r*y*(y*0.375-0.5). + This yields the Q14 reciprocal square root of the Q16 x, with a maximum + relative error of 1.04956E-4, a (relative) RMSE of 2.80979E-5, and a + peak absolute error of 2.26591/16384. */ + return ADD16(r, MULT16_16_Q15(r, MULT16_16_Q15(y, + SUB16(MULT16_16_Q15(y, 12288), 16384)))); +} + +/** Sqrt approximation (QX input, QX/2 output) */ +opus_val32 celt_sqrt(opus_val32 x) +{ + int k; + opus_val16 n; + opus_val32 rt; + static const opus_val16 C[5] = {23175, 11561, -3011, 1699, -664}; + if (x==0) + return 0; + else if (x>=1073741824) + return 32767; + k = (celt_ilog2(x)>>1)-7; + x = VSHR32(x, 2*k); + n = x-32768; + rt = ADD16(C[0], MULT16_16_Q15(n, ADD16(C[1], MULT16_16_Q15(n, ADD16(C[2], + MULT16_16_Q15(n, ADD16(C[3], MULT16_16_Q15(n, (C[4]))))))))); + rt = VSHR32(rt,7-k); + return rt; +} + +#define L1 32767 +#define L2 -7651 +#define L3 8277 +#define L4 -626 + +static OPUS_INLINE opus_val16 _celt_cos_pi_2(opus_val16 x) +{ + opus_val16 x2; + + x2 = MULT16_16_P15(x,x); + return ADD16(1,MIN16(32766,ADD32(SUB16(L1,x2), MULT16_16_P15(x2, ADD32(L2, MULT16_16_P15(x2, ADD32(L3, MULT16_16_P15(L4, x2 + )))))))); +} + +#undef L1 +#undef L2 +#undef L3 +#undef L4 + +opus_val16 celt_cos_norm(opus_val32 x) +{ + x = x&0x0001ffff; + if (x>SHL32(EXTEND32(1), 16)) + x = SUB32(SHL32(EXTEND32(1), 17),x); + if (x&0x00007fff) + { + if (x0); + i = celt_ilog2(x); + /* n is Q15 with range [0,1). */ + n = VSHR32(x,i-15)-32768; + /* Start with a linear approximation: + r = 1.8823529411764706-0.9411764705882353*n. + The coefficients and the result are Q14 in the range [15420,30840].*/ + r = ADD16(30840, MULT16_16_Q15(-15420, n)); + /* Perform two Newton iterations: + r -= r*((r*n)-1.Q15) + = r*((r*n)+(r-1.Q15)). */ + r = SUB16(r, MULT16_16_Q15(r, + ADD16(MULT16_16_Q15(r, n), ADD16(r, -32768)))); + /* We subtract an extra 1 in the second iteration to avoid overflow; it also + neatly compensates for truncation error in the rest of the process. */ + r = SUB16(r, ADD16(1, MULT16_16_Q15(r, + ADD16(MULT16_16_Q15(r, n), ADD16(r, -32768))))); + /* r is now the Q15 solution to 2/(n+1), with a maximum relative error + of 7.05346E-5, a (relative) RMSE of 2.14418E-5, and a peak absolute + error of 1.24665/32768. */ + return VSHR32(EXTEND32(r),i-16); +} + +#endif diff --git a/port/third_party/opus/celt/mathops.h b/port/third_party/opus/celt/mathops.h new file mode 100644 index 00000000..e2eece29 --- /dev/null +++ b/port/third_party/opus/celt/mathops.h @@ -0,0 +1,296 @@ +/* Copyright (c) 2002-2008 Jean-Marc Valin + Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/** + @file mathops.h + @brief Various math functions +*/ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef MATHOPS_H +#define MATHOPS_H + +#include "arch.h" +#include "entcode.h" +#include "os_support.h" + +#define PI 3.141592653f + +/* Multiplies two 16-bit fractional values. Bit-exactness of this macro is important */ +#define FRAC_MUL16(a,b) ((16384+((opus_int32)(opus_int16)(a)*(opus_int16)(b)))>>15) + +unsigned isqrt32(opus_uint32 _val); + +/* CELT doesn't need it for fixed-point, by analysis.c does. */ +#if !defined(FIXED_POINT) || defined(ANALYSIS_C) +#define cA 0.43157974f +#define cB 0.67848403f +#define cC 0.08595542f +#define cE ((float)PI/2) +static OPUS_INLINE float fast_atan2f(float y, float x) { + float x2, y2; + x2 = x*x; + y2 = y*y; + /* For very small values, we don't care about the answer, so + we can just return 0. */ + if (x2 + y2 < 1e-18f) + { + return 0; + } + if(x2>23)-127; + in.i -= (opus_uint32)integer<<23; + frac = in.f - 1.5f; + frac = -0.41445418f + frac*(0.95909232f + + frac*(-0.33951290f + frac*0.16541097f)); + return 1+integer+frac; +} + +/** Base-2 exponential approximation (2^x). */ +static OPUS_INLINE float celt_exp2(float x) +{ + int integer; + float frac; + union { + float f; + opus_uint32 i; + } res; + integer = (int)floor(x); + if (integer < -50) + return 0; + frac = x-integer; + /* K0 = 1, K1 = log(2), K2 = 3-4*log(2), K3 = 3*log(2) - 2 */ + res.f = 0.99992522f + frac * (0.69583354f + + frac * (0.22606716f + 0.078024523f*frac)); + res.i = (res.i + ((opus_uint32)integer<<23)) & 0x7fffffff; + return res.f; +} + +#else +#define celt_log2(x) ((float)(1.442695040888963387*log(x))) +#define celt_exp2(x) ((float)exp(0.6931471805599453094*(x))) +#endif + +#endif + +#ifdef FIXED_POINT + +#include "os_support.h" + +#ifndef OVERRIDE_CELT_ILOG2 +/** Integer log in base2. Undefined for zero and negative numbers */ +static OPUS_INLINE opus_int16 celt_ilog2(opus_int32 x) +{ + celt_sig_assert(x>0); + return EC_ILOG(x)-1; +} +#endif + + +/** Integer log in base2. Defined for zero, but not for negative numbers */ +static OPUS_INLINE opus_int16 celt_zlog2(opus_val32 x) +{ + return x <= 0 ? 0 : celt_ilog2(x); +} + +opus_val16 celt_rsqrt_norm(opus_val32 x); + +opus_val32 celt_sqrt(opus_val32 x); + +opus_val16 celt_cos_norm(opus_val32 x); + +/** Base-2 logarithm approximation (log2(x)). (Q14 input, Q10 output) */ +static OPUS_INLINE opus_val16 celt_log2(opus_val32 x) +{ + int i; + opus_val16 n, frac; + /* -0.41509302963303146, 0.9609890551383969, -0.31836011537636605, + 0.15530808010959576, -0.08556153059057618 */ + static const opus_val16 C[5] = {-6801+(1<<(13-DB_SHIFT)), 15746, -5217, 2545, -1401}; + if (x==0) + return -32767; + i = celt_ilog2(x); + n = VSHR32(x,i-15)-32768-16384; + frac = ADD16(C[0], MULT16_16_Q15(n, ADD16(C[1], MULT16_16_Q15(n, ADD16(C[2], MULT16_16_Q15(n, ADD16(C[3], MULT16_16_Q15(n, C[4])))))))); + return SHL16(i-13,DB_SHIFT)+SHR16(frac,14-DB_SHIFT); +} + +/* + K0 = 1 + K1 = log(2) + K2 = 3-4*log(2) + K3 = 3*log(2) - 2 +*/ +#define D0 16383 +#define D1 22804 +#define D2 14819 +#define D3 10204 + +static OPUS_INLINE opus_val32 celt_exp2_frac(opus_val16 x) +{ + opus_val16 frac; + frac = SHL16(x, 4); + return ADD16(D0, MULT16_16_Q15(frac, ADD16(D1, MULT16_16_Q15(frac, ADD16(D2 , MULT16_16_Q15(D3,frac)))))); +} + +#undef D0 +#undef D1 +#undef D2 +#undef D3 + +/** Base-2 exponential approximation (2^x). (Q10 input, Q16 output) */ +static OPUS_INLINE opus_val32 celt_exp2(opus_val16 x) +{ + int integer; + opus_val16 frac; + integer = SHR16(x,10); + if (integer>14) + return 0x7f000000; + else if (integer < -15) + return 0; + frac = celt_exp2_frac(x-SHL16(integer,10)); + return VSHR32(EXTEND32(frac), -integer-2); +} + +opus_val32 celt_rcp(opus_val32 x); + +#define celt_div(a,b) MULT32_32_Q31((opus_val32)(a),celt_rcp(b)) + +opus_val32 frac_div32(opus_val32 a, opus_val32 b); + +#define M1 32767 +#define M2 -21 +#define M3 -11943 +#define M4 4936 + +/* Atan approximation using a 4th order polynomial. Input is in Q15 format + and normalized by pi/4. Output is in Q15 format */ +static OPUS_INLINE opus_val16 celt_atan01(opus_val16 x) +{ + return MULT16_16_P15(x, ADD32(M1, MULT16_16_P15(x, ADD32(M2, MULT16_16_P15(x, ADD32(M3, MULT16_16_P15(M4, x))))))); +} + +#undef M1 +#undef M2 +#undef M3 +#undef M4 + +/* atan2() approximation valid for positive input values */ +static OPUS_INLINE opus_val16 celt_atan2p(opus_val16 y, opus_val16 x) +{ + if (y < x) + { + opus_val32 arg; + arg = celt_div(SHL32(EXTEND32(y),15),x); + if (arg >= 32767) + arg = 32767; + return SHR16(celt_atan01(EXTRACT16(arg)),1); + } else { + opus_val32 arg; + arg = celt_div(SHL32(EXTEND32(x),15),y); + if (arg >= 32767) + arg = 32767; + return 25736-SHR16(celt_atan01(EXTRACT16(arg)),1); + } +} + +#endif /* FIXED_POINT */ +#endif /* MATHOPS_H */ diff --git a/port/third_party/opus/celt/mdct.c b/port/third_party/opus/celt/mdct.c new file mode 100644 index 00000000..5c6dab5b --- /dev/null +++ b/port/third_party/opus/celt/mdct.c @@ -0,0 +1,343 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2008 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* This is a simple MDCT implementation that uses a N/4 complex FFT + to do most of the work. It should be relatively straightforward to + plug in pretty much and FFT here. + + This replaces the Vorbis FFT (and uses the exact same API), which + was a bit too messy and that was ending up duplicating code + (might as well use the same FFT everywhere). + + The algorithm is similar to (and inspired from) Fabrice Bellard's + MDCT implementation in FFMPEG, but has differences in signs, ordering + and scaling in many places. +*/ + +#ifndef SKIP_CONFIG_H +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#endif + +#include "mdct.h" +#include "kiss_fft.h" +#include "_kiss_fft_guts.h" +#include +#include "os_support.h" +#include "mathops.h" +#include "stack_alloc.h" + +#if defined(MIPSr1_ASM) +#include "mips/mdct_mipsr1.h" +#endif + + +#ifdef CUSTOM_MODES + +int clt_mdct_init(mdct_lookup *l,int N, int maxshift, int arch) +{ + int i; + kiss_twiddle_scalar *trig; + int shift; + int N2=N>>1; + l->n = N; + l->maxshift = maxshift; + for (i=0;i<=maxshift;i++) + { + if (i==0) + l->kfft[i] = opus_fft_alloc(N>>2>>i, 0, 0, arch); + else + l->kfft[i] = opus_fft_alloc_twiddles(N>>2>>i, 0, 0, l->kfft[0], arch); +#ifndef ENABLE_TI_DSPLIB55 + if (l->kfft[i]==NULL) + return 0; +#endif + } + l->trig = trig = (kiss_twiddle_scalar*)opus_alloc((N-(N2>>maxshift))*sizeof(kiss_twiddle_scalar)); + if (l->trig==NULL) + return 0; + for (shift=0;shift<=maxshift;shift++) + { + /* We have enough points that sine isn't necessary */ +#if defined(FIXED_POINT) +#if 1 + for (i=0;i>= 1; + N >>= 1; + } + return 1; +} + +void clt_mdct_clear(mdct_lookup *l, int arch) +{ + int i; + for (i=0;i<=l->maxshift;i++) + opus_fft_free(l->kfft[i], arch); + opus_free((kiss_twiddle_scalar*)l->trig); +} + +#endif /* CUSTOM_MODES */ + +/* Forward MDCT trashes the input array */ +#ifndef OVERRIDE_clt_mdct_forward +void clt_mdct_forward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_scalar * OPUS_RESTRICT out, + const opus_val16 *window, int overlap, int shift, int stride, int arch) +{ + int i; + int N, N2, N4; + VARDECL(kiss_fft_scalar, f); + VARDECL(kiss_fft_cpx, f2); + const kiss_fft_state *st = l->kfft[shift]; + const kiss_twiddle_scalar *trig; + opus_val16 scale; +#ifdef FIXED_POINT + /* Allows us to scale with MULT16_32_Q16(), which is faster than + MULT16_32_Q15() on ARM. */ + int scale_shift = st->scale_shift-1; +#endif + SAVE_STACK; + (void)arch; + scale = st->scale; + + N = l->n; + trig = l->trig; + for (i=0;i>= 1; + trig += N; + } + N2 = N>>1; + N4 = N>>2; + + ALLOC(f, N2, kiss_fft_scalar); + ALLOC(f2, N4, kiss_fft_cpx); + + /* Consider the input to be composed of four blocks: [a, b, c, d] */ + /* Window, shuffle, fold */ + { + /* Temp pointers to make it really clear to the compiler what we're doing */ + const kiss_fft_scalar * OPUS_RESTRICT xp1 = in+(overlap>>1); + const kiss_fft_scalar * OPUS_RESTRICT xp2 = in+N2-1+(overlap>>1); + kiss_fft_scalar * OPUS_RESTRICT yp = f; + const opus_val16 * OPUS_RESTRICT wp1 = window+(overlap>>1); + const opus_val16 * OPUS_RESTRICT wp2 = window+(overlap>>1)-1; + for(i=0;i<((overlap+3)>>2);i++) + { + /* Real part arranged as -d-cR, Imag part arranged as -b+aR*/ + *yp++ = MULT16_32_Q15(*wp2, xp1[N2]) + MULT16_32_Q15(*wp1,*xp2); + *yp++ = MULT16_32_Q15(*wp1, *xp1) - MULT16_32_Q15(*wp2, xp2[-N2]); + xp1+=2; + xp2-=2; + wp1+=2; + wp2-=2; + } + wp1 = window; + wp2 = window+overlap-1; + for(;i>2);i++) + { + /* Real part arranged as a-bR, Imag part arranged as -c-dR */ + *yp++ = *xp2; + *yp++ = *xp1; + xp1+=2; + xp2-=2; + } + for(;ibitrev[i]] = yc; + } + } + + /* N/4 complex FFT, does not downscale anymore */ + opus_fft_impl(st, f2); + + /* Post-rotate */ + { + /* Temp pointers to make it really clear to the compiler what we're doing */ + const kiss_fft_cpx * OPUS_RESTRICT fp = f2; + kiss_fft_scalar * OPUS_RESTRICT yp1 = out; + kiss_fft_scalar * OPUS_RESTRICT yp2 = out+stride*(N2-1); + const kiss_twiddle_scalar *t = &trig[0]; + /* Temp pointers to make it really clear to the compiler what we're doing */ + for(i=0;ii,t[N4+i]) - S_MUL(fp->r,t[i]); + yi = S_MUL(fp->r,t[N4+i]) + S_MUL(fp->i,t[i]); + *yp1 = yr; + *yp2 = yi; + fp++; + yp1 += 2*stride; + yp2 -= 2*stride; + } + } + RESTORE_STACK; +} +#endif /* OVERRIDE_clt_mdct_forward */ + +#ifndef OVERRIDE_clt_mdct_backward +void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, kiss_fft_scalar * OPUS_RESTRICT out, + const opus_val16 * OPUS_RESTRICT window, int overlap, int shift, int stride, int arch) +{ + int i; + int N, N2, N4; + const kiss_twiddle_scalar *trig; + (void) arch; + + N = l->n; + trig = l->trig; + for (i=0;i>= 1; + trig += N; + } + N2 = N>>1; + N4 = N>>2; + + /* Pre-rotate */ + { + /* Temp pointers to make it really clear to the compiler what we're doing */ + const kiss_fft_scalar * OPUS_RESTRICT xp1 = in; + const kiss_fft_scalar * OPUS_RESTRICT xp2 = in+stride*(N2-1); + kiss_fft_scalar * OPUS_RESTRICT yp = out+(overlap>>1); + const kiss_twiddle_scalar * OPUS_RESTRICT t = &trig[0]; + const opus_int16 * OPUS_RESTRICT bitrev = l->kfft[shift]->bitrev; + for(i=0;ikfft[shift], (kiss_fft_cpx*)(out+(overlap>>1))); + + /* Post-rotate and de-shuffle from both ends of the buffer at once to make + it in-place. */ + { + kiss_fft_scalar * yp0 = out+(overlap>>1); + kiss_fft_scalar * yp1 = out+(overlap>>1)+N2-2; + const kiss_twiddle_scalar *t = &trig[0]; + /* Loop to (N4+1)>>1 to handle odd N4. When N4 is odd, the + middle pair will be computed twice. */ + for(i=0;i<(N4+1)>>1;i++) + { + kiss_fft_scalar re, im, yr, yi; + kiss_twiddle_scalar t0, t1; + /* We swap real and imag because we're using an FFT instead of an IFFT. */ + re = yp0[1]; + im = yp0[0]; + t0 = t[i]; + t1 = t[N4+i]; + /* We'd scale up by 2 here, but instead it's done when mixing the windows */ + yr = ADD32_ovflw(S_MUL(re,t0), S_MUL(im,t1)); + yi = SUB32_ovflw(S_MUL(re,t1), S_MUL(im,t0)); + /* We swap real and imag because we're using an FFT instead of an IFFT. */ + re = yp1[1]; + im = yp1[0]; + yp0[0] = yr; + yp1[1] = yi; + + t0 = t[(N4-i-1)]; + t1 = t[(N2-i-1)]; + /* We'd scale up by 2 here, but instead it's done when mixing the windows */ + yr = ADD32_ovflw(S_MUL(re,t0), S_MUL(im,t1)); + yi = SUB32_ovflw(S_MUL(re,t1), S_MUL(im,t0)); + yp1[0] = yr; + yp0[1] = yi; + yp0 += 2; + yp1 -= 2; + } + } + + /* Mirror on both sides for TDAC */ + { + kiss_fft_scalar * OPUS_RESTRICT xp1 = out+overlap-1; + kiss_fft_scalar * OPUS_RESTRICT yp1 = out; + const opus_val16 * OPUS_RESTRICT wp1 = window; + const opus_val16 * OPUS_RESTRICT wp2 = window+overlap-1; + + for(i = 0; i < overlap/2; i++) + { + kiss_fft_scalar x1, x2; + x1 = *xp1; + x2 = *yp1; + *yp1++ = SUB32_ovflw(MULT16_32_Q15(*wp2, x2), MULT16_32_Q15(*wp1, x1)); + *xp1-- = ADD32_ovflw(MULT16_32_Q15(*wp1, x2), MULT16_32_Q15(*wp2, x1)); + wp1++; + wp2--; + } + } +} +#endif /* OVERRIDE_clt_mdct_backward */ diff --git a/port/third_party/opus/celt/mdct.h b/port/third_party/opus/celt/mdct.h new file mode 100644 index 00000000..160ae4e0 --- /dev/null +++ b/port/third_party/opus/celt/mdct.h @@ -0,0 +1,112 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2008 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* This is a simple MDCT implementation that uses a N/4 complex FFT + to do most of the work. It should be relatively straightforward to + plug in pretty much and FFT here. + + This replaces the Vorbis FFT (and uses the exact same API), which + was a bit too messy and that was ending up duplicating code + (might as well use the same FFT everywhere). + + The algorithm is similar to (and inspired from) Fabrice Bellard's + MDCT implementation in FFMPEG, but has differences in signs, ordering + and scaling in many places. +*/ + +#ifndef MDCT_H +#define MDCT_H + +#include "opus_defines.h" +#include "kiss_fft.h" +#include "arch.h" + +typedef struct { + int n; + int maxshift; + const kiss_fft_state *kfft[4]; + const kiss_twiddle_scalar * OPUS_RESTRICT trig; +} mdct_lookup; + +#if defined(HAVE_ARM_NE10) +#include "arm/mdct_arm.h" +#endif + + +int clt_mdct_init(mdct_lookup *l,int N, int maxshift, int arch); +void clt_mdct_clear(mdct_lookup *l, int arch); + +/** Compute a forward MDCT and scale by 4/N, trashes the input array */ +void clt_mdct_forward_c(const mdct_lookup *l, kiss_fft_scalar *in, + kiss_fft_scalar * OPUS_RESTRICT out, + const opus_val16 *window, int overlap, + int shift, int stride, int arch); + +/** Compute a backward MDCT (no scaling) and performs weighted overlap-add + (scales implicitly by 1/2) */ +void clt_mdct_backward_c(const mdct_lookup *l, kiss_fft_scalar *in, + kiss_fft_scalar * OPUS_RESTRICT out, + const opus_val16 * OPUS_RESTRICT window, + int overlap, int shift, int stride, int arch); + +#if !defined(OVERRIDE_OPUS_MDCT) +/* Is run-time CPU detection enabled on this platform? */ +#if defined(OPUS_HAVE_RTCD) && defined(HAVE_ARM_NE10) + +extern void (*const CLT_MDCT_FORWARD_IMPL[OPUS_ARCHMASK+1])( + const mdct_lookup *l, kiss_fft_scalar *in, + kiss_fft_scalar * OPUS_RESTRICT out, const opus_val16 *window, + int overlap, int shift, int stride, int arch); + +#define clt_mdct_forward(_l, _in, _out, _window, _overlap, _shift, _stride, _arch) \ + ((*CLT_MDCT_FORWARD_IMPL[(arch)&OPUS_ARCHMASK])(_l, _in, _out, \ + _window, _overlap, _shift, \ + _stride, _arch)) + +extern void (*const CLT_MDCT_BACKWARD_IMPL[OPUS_ARCHMASK+1])( + const mdct_lookup *l, kiss_fft_scalar *in, + kiss_fft_scalar * OPUS_RESTRICT out, const opus_val16 *window, + int overlap, int shift, int stride, int arch); + +#define clt_mdct_backward(_l, _in, _out, _window, _overlap, _shift, _stride, _arch) \ + (*CLT_MDCT_BACKWARD_IMPL[(arch)&OPUS_ARCHMASK])(_l, _in, _out, \ + _window, _overlap, _shift, \ + _stride, _arch) + +#else /* if defined(OPUS_HAVE_RTCD) && defined(HAVE_ARM_NE10) */ + +#define clt_mdct_forward(_l, _in, _out, _window, _overlap, _shift, _stride, _arch) \ + clt_mdct_forward_c(_l, _in, _out, _window, _overlap, _shift, _stride, _arch) + +#define clt_mdct_backward(_l, _in, _out, _window, _overlap, _shift, _stride, _arch) \ + clt_mdct_backward_c(_l, _in, _out, _window, _overlap, _shift, _stride, _arch) + +#endif /* end if defined(OPUS_HAVE_RTCD) && defined(HAVE_ARM_NE10) && !defined(FIXED_POINT) */ +#endif /* end if !defined(OVERRIDE_OPUS_MDCT) */ + +#endif diff --git a/port/third_party/opus/celt/mfrngcod.h b/port/third_party/opus/celt/mfrngcod.h new file mode 100644 index 00000000..809152a5 --- /dev/null +++ b/port/third_party/opus/celt/mfrngcod.h @@ -0,0 +1,48 @@ +/* Copyright (c) 2001-2008 Timothy B. Terriberry + Copyright (c) 2008-2009 Xiph.Org Foundation */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#if !defined(_mfrngcode_H) +# define _mfrngcode_H (1) +# include "entcode.h" + +/*Constants used by the entropy encoder/decoder.*/ + +/*The number of bits to output at a time.*/ +# define EC_SYM_BITS (8) +/*The total number of bits in each of the state registers.*/ +# define EC_CODE_BITS (32) +/*The maximum symbol value.*/ +# define EC_SYM_MAX ((1U<>EC_SYM_BITS) +/*The number of bits available for the last, partial symbol in the code field.*/ +# define EC_CODE_EXTRA ((EC_CODE_BITS-2)%EC_SYM_BITS+1) +#endif diff --git a/port/third_party/opus/celt/modes.c b/port/third_party/opus/celt/modes.c new file mode 100644 index 00000000..23f7cde6 --- /dev/null +++ b/port/third_party/opus/celt/modes.c @@ -0,0 +1,445 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Copyright (c) 2008 Gregory Maxwell + Written by Jean-Marc Valin and Gregory Maxwell */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "celt.h" +#include "modes.h" +#include "rate.h" +#include "os_support.h" +#include "stack_alloc.h" +#include "quant_bands.h" +#include "cpu_support.h" + +static const opus_int16 eband5ms[] = { +/*0 200 400 600 800 1k 1.2 1.4 1.6 2k 2.4 2.8 3.2 4k 4.8 5.6 6.8 8k 9.6 12k 15.6 */ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 34, 40, 48, 60, 78, 100 +}; + +/* Alternate tuning (partially derived from Vorbis) */ +#define BITALLOC_SIZE 11 +/* Bit allocation table in units of 1/32 bit/sample (0.1875 dB SNR) */ +static const unsigned char band_allocation[] = { +/*0 200 400 600 800 1k 1.2 1.4 1.6 2k 2.4 2.8 3.2 4k 4.8 5.6 6.8 8k 9.6 12k 15.6 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 90, 80, 75, 69, 63, 56, 49, 40, 34, 29, 20, 18, 10, 0, 0, 0, 0, 0, 0, 0, 0, +110,100, 90, 84, 78, 71, 65, 58, 51, 45, 39, 32, 26, 20, 12, 0, 0, 0, 0, 0, 0, +118,110,103, 93, 86, 80, 75, 70, 65, 59, 53, 47, 40, 31, 23, 15, 4, 0, 0, 0, 0, +126,119,112,104, 95, 89, 83, 78, 72, 66, 60, 54, 47, 39, 32, 25, 17, 12, 1, 0, 0, +134,127,120,114,103, 97, 91, 85, 78, 72, 66, 60, 54, 47, 41, 35, 29, 23, 16, 10, 1, +144,137,130,124,113,107,101, 95, 88, 82, 76, 70, 64, 57, 51, 45, 39, 33, 26, 15, 1, +152,145,138,132,123,117,111,105, 98, 92, 86, 80, 74, 67, 61, 55, 49, 43, 36, 20, 1, +162,155,148,142,133,127,121,115,108,102, 96, 90, 84, 77, 71, 65, 59, 53, 46, 30, 1, +172,165,158,152,143,137,131,125,118,112,106,100, 94, 87, 81, 75, 69, 63, 56, 45, 20, +200,200,200,200,200,200,200,200,198,193,188,183,178,173,168,163,158,153,148,129,104, +}; + +#ifndef CUSTOM_MODES_ONLY + #ifdef FIXED_POINT + #include "static_modes_fixed.h" + #else + #include "static_modes_float.h" + #endif +#endif /* CUSTOM_MODES_ONLY */ + +#ifndef M_PI +#define M_PI 3.141592653 +#endif + +#ifdef CUSTOM_MODES + +/* Defining 25 critical bands for the full 0-20 kHz audio bandwidth + Taken from http://ccrma.stanford.edu/~jos/bbt/Bark_Frequency_Scale.html */ +#define BARK_BANDS 25 +static const opus_int16 bark_freq[BARK_BANDS+1] = { + 0, 100, 200, 300, 400, + 510, 630, 770, 920, 1080, + 1270, 1480, 1720, 2000, 2320, + 2700, 3150, 3700, 4400, 5300, + 6400, 7700, 9500, 12000, 15500, + 20000}; + +static opus_int16 *compute_ebands(opus_int32 Fs, int frame_size, int res, int *nbEBands) +{ + opus_int16 *eBands; + int i, j, lin, low, high, nBark, offset=0; + + /* All modes that have 2.5 ms short blocks use the same definition */ + if (Fs == 400*(opus_int32)frame_size) + { + *nbEBands = sizeof(eband5ms)/sizeof(eband5ms[0])-1; + eBands = opus_alloc(sizeof(opus_int16)*(*nbEBands+1)); + for (i=0;i<*nbEBands+1;i++) + eBands[i] = eband5ms[i]; + return eBands; + } + /* Find the number of critical bands supported by our sampling rate */ + for (nBark=1;nBark= Fs) + break; + + /* Find where the linear part ends (i.e. where the spacing is more than min_width */ + for (lin=0;lin= res) + break; + + low = (bark_freq[lin]+res/2)/res; + high = nBark-lin; + *nbEBands = low+high; + eBands = opus_alloc(sizeof(opus_int16)*(*nbEBands+2)); + + if (eBands==NULL) + return NULL; + + /* Linear spacing (min_width) */ + for (i=0;i0) + offset = eBands[low-1]*res - bark_freq[lin-1]; + /* Spacing follows critical bands */ + for (i=0;i frame_size) + eBands[*nbEBands] = frame_size; + for (i=1;i<*nbEBands-1;i++) + { + if (eBands[i+1]-eBands[i] < eBands[i]-eBands[i-1]) + { + eBands[i] -= (2*eBands[i]-eBands[i-1]-eBands[i+1])/2; + } + } + /* Remove any empty bands. */ + for (i=j=0;i<*nbEBands;i++) + if(eBands[i+1]>eBands[j]) + eBands[++j]=eBands[i+1]; + *nbEBands=j; + + for (i=1;i<*nbEBands;i++) + { + /* Every band must be smaller than the last band. */ + celt_assert(eBands[i]-eBands[i-1]<=eBands[*nbEBands]-eBands[*nbEBands-1]); + /* Each band must be no larger than twice the size of the previous one. */ + celt_assert(eBands[i+1]-eBands[i]<=2*(eBands[i]-eBands[i-1])); + } + + return eBands; +} + +static void compute_allocation_table(CELTMode *mode) +{ + int i, j; + unsigned char *allocVectors; + int maxBands = sizeof(eband5ms)/sizeof(eband5ms[0])-1; + + mode->nbAllocVectors = BITALLOC_SIZE; + allocVectors = opus_alloc(sizeof(unsigned char)*(BITALLOC_SIZE*mode->nbEBands)); + if (allocVectors==NULL) + { + mode->allocVectors = NULL; + return; + } + + /* Check for standard mode */ + if (mode->Fs == 400*(opus_int32)mode->shortMdctSize) + { + for (i=0;inbEBands;i++) + allocVectors[i] = band_allocation[i]; + mode->allocVectors = allocVectors; + return; + } + /* If not the standard mode, interpolate */ + /* Compute per-codec-band allocation from per-critical-band matrix */ + for (i=0;inbEBands;j++) + { + int k; + for (k=0;k mode->eBands[j]*(opus_int32)mode->Fs/mode->shortMdctSize) + break; + } + if (k>maxBands-1) + allocVectors[i*mode->nbEBands+j] = band_allocation[i*maxBands + maxBands-1]; + else { + opus_int32 a0, a1; + a1 = mode->eBands[j]*(opus_int32)mode->Fs/mode->shortMdctSize - 400*(opus_int32)eband5ms[k-1]; + a0 = 400*(opus_int32)eband5ms[k] - mode->eBands[j]*(opus_int32)mode->Fs/mode->shortMdctSize; + allocVectors[i*mode->nbEBands+j] = (a0*band_allocation[i*maxBands+k-1] + + a1*band_allocation[i*maxBands+k])/(a0+a1); + } + } + } + + /*printf ("\n"); + for (i=0;inbEBands;j++) + printf ("%d ", allocVectors[i*mode->nbEBands+j]); + printf ("\n"); + } + exit(0);*/ + + mode->allocVectors = allocVectors; +} + +#endif /* CUSTOM_MODES */ + +CELTMode *opus_custom_mode_create(opus_int32 Fs, int frame_size, int *error) +{ + int i; +#ifdef CUSTOM_MODES + CELTMode *mode=NULL; + int res; + opus_val16 *window; + opus_int16 *logN; + int LM; + int arch = opus_select_arch(); + ALLOC_STACK; +#if !defined(VAR_ARRAYS) && !defined(USE_ALLOCA) + if (global_stack==NULL) + goto failure; +#endif +#endif + +#ifndef CUSTOM_MODES_ONLY + for (i=0;iFs && + (frame_size<shortMdctSize*static_mode_list[i]->nbShortMdcts) + { + if (error) + *error = OPUS_OK; + return (CELTMode*)static_mode_list[i]; + } + } + } +#endif /* CUSTOM_MODES_ONLY */ + +#ifndef CUSTOM_MODES + if (error) + *error = OPUS_BAD_ARG; + return NULL; +#else + + /* The good thing here is that permutation of the arguments will automatically be invalid */ + + if (Fs < 8000 || Fs > 96000) + { + if (error) + *error = OPUS_BAD_ARG; + return NULL; + } + if (frame_size < 40 || frame_size > 1024 || frame_size%2!=0) + { + if (error) + *error = OPUS_BAD_ARG; + return NULL; + } + /* Frames of less than 1ms are not supported. */ + if ((opus_int32)frame_size*1000 < Fs) + { + if (error) + *error = OPUS_BAD_ARG; + return NULL; + } + + if ((opus_int32)frame_size*75 >= Fs && (frame_size%16)==0) + { + LM = 3; + } else if ((opus_int32)frame_size*150 >= Fs && (frame_size%8)==0) + { + LM = 2; + } else if ((opus_int32)frame_size*300 >= Fs && (frame_size%4)==0) + { + LM = 1; + } else + { + LM = 0; + } + + /* Shorts longer than 3.3ms are not supported. */ + if ((opus_int32)(frame_size>>LM)*300 > Fs) + { + if (error) + *error = OPUS_BAD_ARG; + return NULL; + } + + mode = opus_alloc(sizeof(CELTMode)); + if (mode==NULL) + goto failure; + mode->Fs = Fs; + + /* Pre/de-emphasis depends on sampling rate. The "standard" pre-emphasis + is defined as A(z) = 1 - 0.85*z^-1 at 48 kHz. Other rates should + approximate that. */ + if(Fs < 12000) /* 8 kHz */ + { + mode->preemph[0] = QCONST16(0.3500061035f, 15); + mode->preemph[1] = -QCONST16(0.1799926758f, 15); + mode->preemph[2] = QCONST16(0.2719968125f, SIG_SHIFT); /* exact 1/preemph[3] */ + mode->preemph[3] = QCONST16(3.6765136719f, 13); + } else if(Fs < 24000) /* 16 kHz */ + { + mode->preemph[0] = QCONST16(0.6000061035f, 15); + mode->preemph[1] = -QCONST16(0.1799926758f, 15); + mode->preemph[2] = QCONST16(0.4424998650f, SIG_SHIFT); /* exact 1/preemph[3] */ + mode->preemph[3] = QCONST16(2.2598876953f, 13); + } else if(Fs < 40000) /* 32 kHz */ + { + mode->preemph[0] = QCONST16(0.7799987793f, 15); + mode->preemph[1] = -QCONST16(0.1000061035f, 15); + mode->preemph[2] = QCONST16(0.7499771125f, SIG_SHIFT); /* exact 1/preemph[3] */ + mode->preemph[3] = QCONST16(1.3333740234f, 13); + } else /* 48 kHz */ + { + mode->preemph[0] = QCONST16(0.8500061035f, 15); + mode->preemph[1] = QCONST16(0.0f, 15); + mode->preemph[2] = QCONST16(1.f, SIG_SHIFT); + mode->preemph[3] = QCONST16(1.f, 13); + } + + mode->maxLM = LM; + mode->nbShortMdcts = 1<shortMdctSize = frame_size/mode->nbShortMdcts; + res = (mode->Fs+mode->shortMdctSize)/(2*mode->shortMdctSize); + + mode->eBands = compute_ebands(Fs, mode->shortMdctSize, res, &mode->nbEBands); + if (mode->eBands==NULL) + goto failure; +#if !defined(SMALL_FOOTPRINT) + /* Make sure we don't allocate a band larger than our PVQ table. + 208 should be enough, but let's be paranoid. */ + if ((mode->eBands[mode->nbEBands] - mode->eBands[mode->nbEBands-1])< + 208) { + goto failure; + } +#endif + + mode->effEBands = mode->nbEBands; + while (mode->eBands[mode->effEBands] > mode->shortMdctSize) + mode->effEBands--; + + /* Overlap must be divisible by 4 */ + mode->overlap = ((mode->shortMdctSize>>2)<<2); + + compute_allocation_table(mode); + if (mode->allocVectors==NULL) + goto failure; + + window = (opus_val16*)opus_alloc(mode->overlap*sizeof(opus_val16)); + if (window==NULL) + goto failure; + +#ifndef FIXED_POINT + for (i=0;ioverlap;i++) + window[i] = Q15ONE*sin(.5*M_PI* sin(.5*M_PI*(i+.5)/mode->overlap) * sin(.5*M_PI*(i+.5)/mode->overlap)); +#else + for (i=0;ioverlap;i++) + window[i] = MIN32(32767,floor(.5+32768.*sin(.5*M_PI* sin(.5*M_PI*(i+.5)/mode->overlap) * sin(.5*M_PI*(i+.5)/mode->overlap)))); +#endif + mode->window = window; + + logN = (opus_int16*)opus_alloc(mode->nbEBands*sizeof(opus_int16)); + if (logN==NULL) + goto failure; + + for (i=0;inbEBands;i++) + logN[i] = log2_frac(mode->eBands[i+1]-mode->eBands[i], BITRES); + mode->logN = logN; + + compute_pulse_cache(mode, mode->maxLM); + + if (clt_mdct_init(&mode->mdct, 2*mode->shortMdctSize*mode->nbShortMdcts, + mode->maxLM, arch) == 0) + goto failure; + + if (error) + *error = OPUS_OK; + + return mode; +failure: + if (error) + *error = OPUS_ALLOC_FAIL; + if (mode!=NULL) + opus_custom_mode_destroy(mode); + return NULL; +#endif /* !CUSTOM_MODES */ +} + +#ifdef CUSTOM_MODES +void opus_custom_mode_destroy(CELTMode *mode) +{ + int arch = opus_select_arch(); + + if (mode == NULL) + return; +#ifndef CUSTOM_MODES_ONLY + { + int i; + for (i=0;ieBands); + opus_free((unsigned char*)mode->allocVectors); + + opus_free((opus_val16*)mode->window); + opus_free((opus_int16*)mode->logN); + + opus_free((opus_int16*)mode->cache.index); + opus_free((unsigned char*)mode->cache.bits); + opus_free((unsigned char*)mode->cache.caps); + clt_mdct_clear(&mode->mdct, arch); + + opus_free((CELTMode *)mode); +} +#endif diff --git a/port/third_party/opus/celt/modes.h b/port/third_party/opus/celt/modes.h new file mode 100644 index 00000000..be813ccc --- /dev/null +++ b/port/third_party/opus/celt/modes.h @@ -0,0 +1,75 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Copyright (c) 2008 Gregory Maxwell + Written by Jean-Marc Valin and Gregory Maxwell */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef MODES_H +#define MODES_H + +#include "opus_types.h" +#include "celt.h" +#include "arch.h" +#include "mdct.h" +#include "entenc.h" +#include "entdec.h" + +#define MAX_PERIOD 1024 + +typedef struct { + int size; + const opus_int16 *index; + const unsigned char *bits; + const unsigned char *caps; +} PulseCache; + +/** Mode definition (opaque) + @brief Mode definition + */ +struct OpusCustomMode { + opus_int32 Fs; + int overlap; + + int nbEBands; + int effEBands; + opus_val16 preemph[4]; + const opus_int16 *eBands; /**< Definition for each "pseudo-critical band" */ + + int maxLM; + int nbShortMdcts; + int shortMdctSize; + + int nbAllocVectors; /**< Number of lines in the matrix below */ + const unsigned char *allocVectors; /**< Number of bits in each band for several rates */ + const opus_int16 *logN; + + const opus_val16 *window; + mdct_lookup mdct; + PulseCache cache; +}; + + +#endif diff --git a/port/third_party/opus/celt/os_support.h b/port/third_party/opus/celt/os_support.h new file mode 100644 index 00000000..7d2d3781 --- /dev/null +++ b/port/third_party/opus/celt/os_support.h @@ -0,0 +1,99 @@ +/* Copyright (C) 2007 Jean-Marc Valin + + File: os_support.h + This is the (tiny) OS abstraction layer. Aside from math.h, this is the + only place where system headers are allowed. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef OS_SUPPORT_H +#define OS_SUPPORT_H + +#ifdef CUSTOM_SUPPORT +# include "custom_support.h" +#endif + +#include "opus_types.h" +#include "opus_defines.h" + +#include +#include + +/** Opus wrapper for malloc(). To do your own dynamic allocation replace this function, opus_realloc, and opus_free */ +#ifndef OVERRIDE_OPUS_ALLOC +static OPUS_INLINE void *opus_alloc (size_t size) +{ + return malloc(size); +} +#endif + +#ifndef OVERRIDE_OPUS_REALLOC +static OPUS_INLINE void *opus_realloc (void *ptr, size_t size) +{ + return realloc(ptr, size); +} +#endif + +/** Used only for non-threadsafe pseudostack. + If desired, this can always return the same area of memory rather than allocating a new one every time. */ +#ifndef OVERRIDE_OPUS_ALLOC_SCRATCH +static OPUS_INLINE void *opus_alloc_scratch (size_t size) +{ + /* Scratch space doesn't need to be cleared */ + return opus_alloc(size); +} +#endif + +/** Opus wrapper for free(). To do your own dynamic allocation replace this function, opus_realloc, and opus_free */ +#ifndef OVERRIDE_OPUS_FREE +static OPUS_INLINE void opus_free (void *ptr) +{ + free(ptr); +} +#endif + +/** Copy n elements from src to dst. The 0* term provides compile-time type checking */ +#ifndef OVERRIDE_OPUS_COPY +#define OPUS_COPY(dst, src, n) (memcpy((dst), (src), (n)*sizeof(*(dst)) + 0*((dst)-(src)) )) +#endif + +/** Copy n elements from src to dst, allowing overlapping regions. The 0* term + provides compile-time type checking */ +#ifndef OVERRIDE_OPUS_MOVE +#define OPUS_MOVE(dst, src, n) (memmove((dst), (src), (n)*sizeof(*(dst)) + 0*((dst)-(src)) )) +#endif + +/** Set n elements of dst to zero */ +#ifndef OVERRIDE_OPUS_CLEAR +#define OPUS_CLEAR(dst, n) (memset((dst), 0, (n)*sizeof(*(dst)))) +#endif + +/*#ifdef __GNUC__ +#pragma GCC poison printf sprintf +#pragma GCC poison malloc free realloc calloc +#endif*/ + +#endif /* OS_SUPPORT_H */ + diff --git a/port/third_party/opus/celt/pitch.c b/port/third_party/opus/celt/pitch.c new file mode 100644 index 00000000..e33c60a3 --- /dev/null +++ b/port/third_party/opus/celt/pitch.c @@ -0,0 +1,555 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/** + @file pitch.c + @brief Pitch analysis + */ + +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "pitch.h" +#include "os_support.h" +#include "modes.h" +#include "stack_alloc.h" +#include "mathops.h" +#include "celt_lpc.h" + +static void find_best_pitch(opus_val32 *xcorr, opus_val16 *y, int len, + int max_pitch, int *best_pitch +#ifdef FIXED_POINT + , int yshift, opus_val32 maxcorr +#endif + ) +{ + int i, j; + opus_val32 Syy=1; + opus_val16 best_num[2]; + opus_val32 best_den[2]; +#ifdef FIXED_POINT + int xshift; + + xshift = celt_ilog2(maxcorr)-14; +#endif + + best_num[0] = -1; + best_num[1] = -1; + best_den[0] = 0; + best_den[1] = 0; + best_pitch[0] = 0; + best_pitch[1] = 1; + for (j=0;j0) + { + opus_val16 num; + opus_val32 xcorr16; + xcorr16 = EXTRACT16(VSHR32(xcorr[i], xshift)); +#ifndef FIXED_POINT + /* Considering the range of xcorr16, this should avoid both underflows + and overflows (inf) when squaring xcorr16 */ + xcorr16 *= 1e-12f; +#endif + num = MULT16_16_Q15(xcorr16,xcorr16); + if (MULT16_32_Q15(num,best_den[1]) > MULT16_32_Q15(best_num[1],Syy)) + { + if (MULT16_32_Q15(num,best_den[0]) > MULT16_32_Q15(best_num[0],Syy)) + { + best_num[1] = best_num[0]; + best_den[1] = best_den[0]; + best_pitch[1] = best_pitch[0]; + best_num[0] = num; + best_den[0] = Syy; + best_pitch[0] = i; + } else { + best_num[1] = num; + best_den[1] = Syy; + best_pitch[1] = i; + } + } + } + Syy += SHR32(MULT16_16(y[i+len],y[i+len]),yshift) - SHR32(MULT16_16(y[i],y[i]),yshift); + Syy = MAX32(1, Syy); + } +} + +static void celt_fir5(opus_val16 *x, + const opus_val16 *num, + int N) +{ + int i; + opus_val16 num0, num1, num2, num3, num4; + opus_val32 mem0, mem1, mem2, mem3, mem4; + num0=num[0]; + num1=num[1]; + num2=num[2]; + num3=num[3]; + num4=num[4]; + mem0=0; + mem1=0; + mem2=0; + mem3=0; + mem4=0; + for (i=0;i>1;i++) + x_lp[i] = SHR32(x[0][(2*i-1)], shift+2) + SHR32(x[0][(2*i+1)], shift+2) + SHR32(x[0][2*i], shift+1); + x_lp[0] = SHR32(x[0][1], shift+2) + SHR32(x[0][0], shift+1); + if (C==2) + { + for (i=1;i>1;i++) + x_lp[i] += SHR32(x[1][(2*i-1)], shift+2) + SHR32(x[1][(2*i+1)], shift+2) + SHR32(x[1][2*i], shift+1); + x_lp[0] += SHR32(x[1][1], shift+2) + SHR32(x[1][0], shift+1); + } +#else + for (i=1;i>1;i++) + x_lp[i] = .25f*x[0][(2*i-1)] + .25f*x[0][(2*i+1)] + .5f*x[0][2*i]; + x_lp[0] = .25f*x[0][1] + .5f*x[0][0]; + if (C==2) + { + for (i=1;i>1;i++) + x_lp[i] += .25f*x[1][(2*i-1)] + .25f*x[1][(2*i+1)] + .5f*x[1][2*i]; + x_lp[0] += .25f*x[1][1] + .5f*x[1][0]; + } +#endif + _celt_autocorr(x_lp, ac, NULL, 0, + 4, len>>1, arch); + + /* Noise floor -40 dB */ +#ifdef FIXED_POINT + ac[0] += SHR32(ac[0],13); +#else + ac[0] *= 1.0001f; +#endif + /* Lag windowing */ + for (i=1;i<=4;i++) + { + /*ac[i] *= exp(-.5*(2*M_PI*.002*i)*(2*M_PI*.002*i));*/ +#ifdef FIXED_POINT + ac[i] -= MULT16_32_Q15(2*i*i, ac[i]); +#else + ac[i] -= ac[i]*(.008f*i)*(.008f*i); +#endif + } + + _celt_lpc(lpc, ac, 4); + for (i=0;i<4;i++) + { + tmp = MULT16_16_Q15(QCONST16(.9f,15), tmp); + lpc[i] = MULT16_16_Q15(lpc[i], tmp); + } + /* Add a zero */ + lpc2[0] = lpc[0] + QCONST16(.8f,SIG_SHIFT); + lpc2[1] = lpc[1] + MULT16_16_Q15(c1,lpc[0]); + lpc2[2] = lpc[2] + MULT16_16_Q15(c1,lpc[1]); + lpc2[3] = lpc[3] + MULT16_16_Q15(c1,lpc[2]); + lpc2[4] = MULT16_16_Q15(c1,lpc[3]); + celt_fir5(x_lp, lpc2, len>>1); +} + +/* Pure C implementation. */ +#ifdef FIXED_POINT +opus_val32 +#else +void +#endif +celt_pitch_xcorr_c(const opus_val16 *_x, const opus_val16 *_y, + opus_val32 *xcorr, int len, int max_pitch, int arch) +{ + +#if 0 /* This is a simple version of the pitch correlation that should work + well on DSPs like Blackfin and TI C5x/C6x */ + int i, j; +#ifdef FIXED_POINT + opus_val32 maxcorr=1; +#endif +#if !defined(OVERRIDE_PITCH_XCORR) + (void)arch; +#endif + for (i=0;i0); + celt_sig_assert(((size_t)_x&3)==0); + for (i=0;i0); + celt_assert(max_pitch>0); + lag = len+max_pitch; + + ALLOC(x_lp4, len>>2, opus_val16); + ALLOC(y_lp4, lag>>2, opus_val16); + ALLOC(xcorr, max_pitch>>1, opus_val32); + + /* Downsample by 2 again */ + for (j=0;j>2;j++) + x_lp4[j] = x_lp[2*j]; + for (j=0;j>2;j++) + y_lp4[j] = y[2*j]; + +#ifdef FIXED_POINT + xmax = celt_maxabs16(x_lp4, len>>2); + ymax = celt_maxabs16(y_lp4, lag>>2); + shift = celt_ilog2(MAX32(1, MAX32(xmax, ymax)))-11; + if (shift>0) + { + for (j=0;j>2;j++) + x_lp4[j] = SHR16(x_lp4[j], shift); + for (j=0;j>2;j++) + y_lp4[j] = SHR16(y_lp4[j], shift); + /* Use double the shift for a MAC */ + shift *= 2; + } else { + shift = 0; + } +#endif + + /* Coarse search with 4x decimation */ + +#ifdef FIXED_POINT + maxcorr = +#endif + celt_pitch_xcorr(x_lp4, y_lp4, xcorr, len>>2, max_pitch>>2, arch); + + find_best_pitch(xcorr, y_lp4, len>>2, max_pitch>>2, best_pitch +#ifdef FIXED_POINT + , 0, maxcorr +#endif + ); + + /* Finer search with 2x decimation */ +#ifdef FIXED_POINT + maxcorr=1; +#endif + for (i=0;i>1;i++) + { + opus_val32 sum; + xcorr[i] = 0; + if (abs(i-2*best_pitch[0])>2 && abs(i-2*best_pitch[1])>2) + continue; +#ifdef FIXED_POINT + sum = 0; + for (j=0;j>1;j++) + sum += SHR32(MULT16_16(x_lp[j],y[i+j]), shift); +#else + sum = celt_inner_prod(x_lp, y+i, len>>1, arch); +#endif + xcorr[i] = MAX32(-1, sum); +#ifdef FIXED_POINT + maxcorr = MAX32(maxcorr, sum); +#endif + } + find_best_pitch(xcorr, y, len>>1, max_pitch>>1, best_pitch +#ifdef FIXED_POINT + , shift+1, maxcorr +#endif + ); + + /* Refine by pseudo-interpolation */ + if (best_pitch[0]>0 && best_pitch[0]<(max_pitch>>1)-1) + { + opus_val32 a, b, c; + a = xcorr[best_pitch[0]-1]; + b = xcorr[best_pitch[0]]; + c = xcorr[best_pitch[0]+1]; + if ((c-a) > MULT16_32_Q15(QCONST16(.7f,15),b-a)) + offset = 1; + else if ((a-c) > MULT16_32_Q15(QCONST16(.7f,15),b-c)) + offset = -1; + else + offset = 0; + } else { + offset = 0; + } + *pitch = 2*best_pitch[0]-offset; + + RESTORE_STACK; +} + +#ifdef FIXED_POINT +static opus_val16 compute_pitch_gain(opus_val32 xy, opus_val32 xx, opus_val32 yy) +{ + opus_val32 x2y2; + int sx, sy, shift; + opus_val32 g; + opus_val16 den; + if (xy == 0 || xx == 0 || yy == 0) + return 0; + sx = celt_ilog2(xx)-14; + sy = celt_ilog2(yy)-14; + shift = sx + sy; + x2y2 = SHR32(MULT16_16(VSHR32(xx, sx), VSHR32(yy, sy)), 14); + if (shift & 1) { + if (x2y2 < 32768) + { + x2y2 <<= 1; + shift--; + } else { + x2y2 >>= 1; + shift++; + } + } + den = celt_rsqrt_norm(x2y2); + g = MULT16_32_Q15(den, xy); + g = VSHR32(g, (shift>>1)-1); + return EXTRACT16(MIN32(g, Q15ONE)); +} +#else +static opus_val16 compute_pitch_gain(opus_val32 xy, opus_val32 xx, opus_val32 yy) +{ + return xy/celt_sqrt(1+xx*yy); +} +#endif + +static const int second_check[16] = {0, 0, 3, 2, 3, 2, 5, 2, 3, 2, 3, 2, 5, 2, 3, 2}; +opus_val16 remove_doubling(opus_val16 *x, int maxperiod, int minperiod, + int N, int *T0_, int prev_period, opus_val16 prev_gain, int arch) +{ + int k, i, T, T0; + opus_val16 g, g0; + opus_val16 pg; + opus_val32 xy,xx,yy,xy2; + opus_val32 xcorr[3]; + opus_val32 best_xy, best_yy; + int offset; + int minperiod0; + VARDECL(opus_val32, yy_lookup); + SAVE_STACK; + + minperiod0 = minperiod; + maxperiod /= 2; + minperiod /= 2; + *T0_ /= 2; + prev_period /= 2; + N /= 2; + x += maxperiod; + if (*T0_>=maxperiod) + *T0_=maxperiod-1; + + T = T0 = *T0_; + ALLOC(yy_lookup, maxperiod+1, opus_val32); + dual_inner_prod(x, x, x-T0, N, &xx, &xy, arch); + yy_lookup[0] = xx; + yy=xx; + for (i=1;i<=maxperiod;i++) + { + yy = yy+MULT16_16(x[-i],x[-i])-MULT16_16(x[N-i],x[N-i]); + yy_lookup[i] = MAX32(0, yy); + } + yy = yy_lookup[T0]; + best_xy = xy; + best_yy = yy; + g = g0 = compute_pitch_gain(xy, xx, yy); + /* Look for any pitch at T/k */ + for (k=2;k<=15;k++) + { + int T1, T1b; + opus_val16 g1; + opus_val16 cont=0; + opus_val16 thresh; + T1 = celt_udiv(2*T0+k, 2*k); + if (T1 < minperiod) + break; + /* Look for another strong correlation at T1b */ + if (k==2) + { + if (T1+T0>maxperiod) + T1b = T0; + else + T1b = T0+T1; + } else + { + T1b = celt_udiv(2*second_check[k]*T0+k, 2*k); + } + dual_inner_prod(x, &x[-T1], &x[-T1b], N, &xy, &xy2, arch); + xy = HALF32(xy + xy2); + yy = HALF32(yy_lookup[T1] + yy_lookup[T1b]); + g1 = compute_pitch_gain(xy, xx, yy); + if (abs(T1-prev_period)<=1) + cont = prev_gain; + else if (abs(T1-prev_period)<=2 && 5*k*k < T0) + cont = HALF16(prev_gain); + else + cont = 0; + thresh = MAX16(QCONST16(.3f,15), MULT16_16_Q15(QCONST16(.7f,15),g0)-cont); + /* Bias against very high pitch (very short period) to avoid false-positives + due to short-term correlation */ + if (T1<3*minperiod) + thresh = MAX16(QCONST16(.4f,15), MULT16_16_Q15(QCONST16(.85f,15),g0)-cont); + else if (T1<2*minperiod) + thresh = MAX16(QCONST16(.5f,15), MULT16_16_Q15(QCONST16(.9f,15),g0)-cont); + if (g1 > thresh) + { + best_xy = xy; + best_yy = yy; + T = T1; + g = g1; + } + } + best_xy = MAX32(0, best_xy); + if (best_yy <= best_xy) + pg = Q15ONE; + else + pg = SHR32(frac_div32(best_xy,best_yy+1),16); + + for (k=0;k<3;k++) + xcorr[k] = celt_inner_prod(x, x-(T+k-1), N, arch); + if ((xcorr[2]-xcorr[0]) > MULT16_32_Q15(QCONST16(.7f,15),xcorr[1]-xcorr[0])) + offset = 1; + else if ((xcorr[0]-xcorr[2]) > MULT16_32_Q15(QCONST16(.7f,15),xcorr[1]-xcorr[2])) + offset = -1; + else + offset = 0; + if (pg > g) + pg = g; + *T0_ = 2*T+offset; + + if (*T0_=3); + y_3=0; /* gcc doesn't realize that y_3 can't be used uninitialized */ + y_0=*y++; + y_1=*y++; + y_2=*y++; + for (j=0;j +#include "os_support.h" +#include "arch.h" +#include "mathops.h" +#include "stack_alloc.h" +#include "rate.h" + +#ifdef FIXED_POINT +/* Mean energy in each band quantized in Q4 */ +const signed char eMeans[25] = { + 103,100, 92, 85, 81, + 77, 72, 70, 78, 75, + 73, 71, 78, 74, 69, + 72, 70, 74, 76, 71, + 60, 60, 60, 60, 60 +}; +#else +/* Mean energy in each band quantized in Q4 and converted back to float */ +const opus_val16 eMeans[25] = { + 6.437500f, 6.250000f, 5.750000f, 5.312500f, 5.062500f, + 4.812500f, 4.500000f, 4.375000f, 4.875000f, 4.687500f, + 4.562500f, 4.437500f, 4.875000f, 4.625000f, 4.312500f, + 4.500000f, 4.375000f, 4.625000f, 4.750000f, 4.437500f, + 3.750000f, 3.750000f, 3.750000f, 3.750000f, 3.750000f +}; +#endif +/* prediction coefficients: 0.9, 0.8, 0.65, 0.5 */ +#ifdef FIXED_POINT +static const opus_val16 pred_coef[4] = {29440, 26112, 21248, 16384}; +static const opus_val16 beta_coef[4] = {30147, 22282, 12124, 6554}; +static const opus_val16 beta_intra = 4915; +#else +static const opus_val16 pred_coef[4] = {29440/32768., 26112/32768., 21248/32768., 16384/32768.}; +static const opus_val16 beta_coef[4] = {30147/32768., 22282/32768., 12124/32768., 6554/32768.}; +static const opus_val16 beta_intra = 4915/32768.; +#endif + +/*Parameters of the Laplace-like probability models used for the coarse energy. + There is one pair of parameters for each frame size, prediction type + (inter/intra), and band number. + The first number of each pair is the probability of 0, and the second is the + decay rate, both in Q8 precision.*/ +static const unsigned char e_prob_model[4][2][42] = { + /*120 sample frames.*/ + { + /*Inter*/ + { + 72, 127, 65, 129, 66, 128, 65, 128, 64, 128, 62, 128, 64, 128, + 64, 128, 92, 78, 92, 79, 92, 78, 90, 79, 116, 41, 115, 40, + 114, 40, 132, 26, 132, 26, 145, 17, 161, 12, 176, 10, 177, 11 + }, + /*Intra*/ + { + 24, 179, 48, 138, 54, 135, 54, 132, 53, 134, 56, 133, 55, 132, + 55, 132, 61, 114, 70, 96, 74, 88, 75, 88, 87, 74, 89, 66, + 91, 67, 100, 59, 108, 50, 120, 40, 122, 37, 97, 43, 78, 50 + } + }, + /*240 sample frames.*/ + { + /*Inter*/ + { + 83, 78, 84, 81, 88, 75, 86, 74, 87, 71, 90, 73, 93, 74, + 93, 74, 109, 40, 114, 36, 117, 34, 117, 34, 143, 17, 145, 18, + 146, 19, 162, 12, 165, 10, 178, 7, 189, 6, 190, 8, 177, 9 + }, + /*Intra*/ + { + 23, 178, 54, 115, 63, 102, 66, 98, 69, 99, 74, 89, 71, 91, + 73, 91, 78, 89, 86, 80, 92, 66, 93, 64, 102, 59, 103, 60, + 104, 60, 117, 52, 123, 44, 138, 35, 133, 31, 97, 38, 77, 45 + } + }, + /*480 sample frames.*/ + { + /*Inter*/ + { + 61, 90, 93, 60, 105, 42, 107, 41, 110, 45, 116, 38, 113, 38, + 112, 38, 124, 26, 132, 27, 136, 19, 140, 20, 155, 14, 159, 16, + 158, 18, 170, 13, 177, 10, 187, 8, 192, 6, 175, 9, 159, 10 + }, + /*Intra*/ + { + 21, 178, 59, 110, 71, 86, 75, 85, 84, 83, 91, 66, 88, 73, + 87, 72, 92, 75, 98, 72, 105, 58, 107, 54, 115, 52, 114, 55, + 112, 56, 129, 51, 132, 40, 150, 33, 140, 29, 98, 35, 77, 42 + } + }, + /*960 sample frames.*/ + { + /*Inter*/ + { + 42, 121, 96, 66, 108, 43, 111, 40, 117, 44, 123, 32, 120, 36, + 119, 33, 127, 33, 134, 34, 139, 21, 147, 23, 152, 20, 158, 25, + 154, 26, 166, 21, 173, 16, 184, 13, 184, 10, 150, 13, 139, 15 + }, + /*Intra*/ + { + 22, 178, 63, 114, 74, 82, 84, 83, 92, 82, 103, 62, 96, 72, + 96, 67, 101, 73, 107, 72, 113, 55, 118, 52, 125, 52, 118, 52, + 117, 55, 135, 49, 137, 39, 157, 32, 145, 29, 97, 33, 77, 40 + } + } +}; + +static const unsigned char small_energy_icdf[3]={2,1,0}; + +static opus_val32 loss_distortion(const opus_val16 *eBands, opus_val16 *oldEBands, int start, int end, int len, int C) +{ + int c, i; + opus_val32 dist = 0; + c=0; do { + for (i=start;inbEBands]; + oldE = MAX16(-QCONST16(9.f,DB_SHIFT), oldEBands[i+c*m->nbEBands]); +#ifdef FIXED_POINT + f = SHL32(EXTEND32(x),7) - PSHR32(MULT16_16(coef,oldE), 8) - prev[c]; + /* Rounding to nearest integer here is really important! */ + qi = (f+QCONST32(.5f,DB_SHIFT+7))>>(DB_SHIFT+7); + decay_bound = EXTRACT16(MAX32(-QCONST16(28.f,DB_SHIFT), + SUB32((opus_val32)oldEBands[i+c*m->nbEBands],max_decay))); +#else + f = x-coef*oldE-prev[c]; + /* Rounding to nearest integer here is really important! */ + qi = (int)floor(.5f+f); + decay_bound = MAX16(-QCONST16(28.f,DB_SHIFT), oldEBands[i+c*m->nbEBands]) - max_decay; +#endif + /* Prevent the energy from going down too quickly (e.g. for bands + that have just one bin) */ + if (qi < 0 && x < decay_bound) + { + qi += (int)SHR16(SUB16(decay_bound,x), DB_SHIFT); + if (qi > 0) + qi = 0; + } + qi0 = qi; + /* If we don't have enough bits to encode all the energy, just assume + something safe. */ + tell = ec_tell(enc); + bits_left = budget-tell-3*C*(end-i); + if (i!=start && bits_left < 30) + { + if (bits_left < 24) + qi = IMIN(1, qi); + if (bits_left < 16) + qi = IMAX(-1, qi); + } + if (lfe && i>=2) + qi = IMIN(qi, 0); + if (budget-tell >= 15) + { + int pi; + pi = 2*IMIN(i,20); + ec_laplace_encode(enc, &qi, + prob_model[pi]<<7, prob_model[pi+1]<<6); + } + else if(budget-tell >= 2) + { + qi = IMAX(-1, IMIN(qi, 1)); + ec_enc_icdf(enc, 2*qi^-(qi<0), small_energy_icdf, 2); + } + else if(budget-tell >= 1) + { + qi = IMIN(0, qi); + ec_enc_bit_logp(enc, -qi, 1); + } + else + qi = -1; + error[i+c*m->nbEBands] = PSHR32(f,7) - SHL16(qi,DB_SHIFT); + badness += abs(qi0-qi); + q = (opus_val32)SHL32(EXTEND32(qi),DB_SHIFT); + + tmp = PSHR32(MULT16_16(coef,oldE),8) + prev[c] + SHL32(q,7); +#ifdef FIXED_POINT + tmp = MAX32(-QCONST32(28.f, DB_SHIFT+7), tmp); +#endif + oldEBands[i+c*m->nbEBands] = PSHR32(tmp, 7); + prev[c] = prev[c] + SHL32(q,7) - MULT16_16(beta,PSHR32(q,8)); + } while (++c < C); + } + return lfe ? 0 : badness; +} + +void quant_coarse_energy(const CELTMode *m, int start, int end, int effEnd, + const opus_val16 *eBands, opus_val16 *oldEBands, opus_uint32 budget, + opus_val16 *error, ec_enc *enc, int C, int LM, int nbAvailableBytes, + int force_intra, opus_val32 *delayedIntra, int two_pass, int loss_rate, int lfe) +{ + int intra; + opus_val16 max_decay; + VARDECL(opus_val16, oldEBands_intra); + VARDECL(opus_val16, error_intra); + ec_enc enc_start_state; + opus_uint32 tell; + int badness1=0; + opus_int32 intra_bias; + opus_val32 new_distortion; + SAVE_STACK; + + intra = force_intra || (!two_pass && *delayedIntra>2*C*(end-start) && nbAvailableBytes > (end-start)*C); + intra_bias = (opus_int32)((budget**delayedIntra*loss_rate)/(C*512)); + new_distortion = loss_distortion(eBands, oldEBands, start, effEnd, m->nbEBands, C); + + tell = ec_tell(enc); + if (tell+3 > budget) + two_pass = intra = 0; + + max_decay = QCONST16(16.f,DB_SHIFT); + if (end-start>10) + { +#ifdef FIXED_POINT + max_decay = MIN32(max_decay, SHL32(EXTEND32(nbAvailableBytes),DB_SHIFT-3)); +#else + max_decay = MIN32(max_decay, .125f*nbAvailableBytes); +#endif + } + if (lfe) + max_decay = QCONST16(3.f,DB_SHIFT); + enc_start_state = *enc; + + ALLOC(oldEBands_intra, C*m->nbEBands, opus_val16); + ALLOC(error_intra, C*m->nbEBands, opus_val16); + OPUS_COPY(oldEBands_intra, oldEBands, C*m->nbEBands); + + if (two_pass || intra) + { + badness1 = quant_coarse_energy_impl(m, start, end, eBands, oldEBands_intra, budget, + tell, e_prob_model[LM][1], error_intra, enc, C, LM, 1, max_decay, lfe); + } + + if (!intra) + { + unsigned char *intra_buf; + ec_enc enc_intra_state; + opus_int32 tell_intra; + opus_uint32 nstart_bytes; + opus_uint32 nintra_bytes; + opus_uint32 save_bytes; + int badness2; + VARDECL(unsigned char, intra_bits); + + tell_intra = ec_tell_frac(enc); + + enc_intra_state = *enc; + + nstart_bytes = ec_range_bytes(&enc_start_state); + nintra_bytes = ec_range_bytes(&enc_intra_state); + intra_buf = ec_get_buffer(&enc_intra_state) + nstart_bytes; + save_bytes = nintra_bytes-nstart_bytes; + if (save_bytes == 0) + save_bytes = ALLOC_NONE; + ALLOC(intra_bits, save_bytes, unsigned char); + /* Copy bits from intra bit-stream */ + OPUS_COPY(intra_bits, intra_buf, nintra_bytes - nstart_bytes); + + *enc = enc_start_state; + + badness2 = quant_coarse_energy_impl(m, start, end, eBands, oldEBands, budget, + tell, e_prob_model[LM][intra], error, enc, C, LM, 0, max_decay, lfe); + + if (two_pass && (badness1 < badness2 || (badness1 == badness2 && ((opus_int32)ec_tell_frac(enc))+intra_bias > tell_intra))) + { + *enc = enc_intra_state; + /* Copy intra bits to bit-stream */ + OPUS_COPY(intra_buf, intra_bits, nintra_bytes - nstart_bytes); + OPUS_COPY(oldEBands, oldEBands_intra, C*m->nbEBands); + OPUS_COPY(error, error_intra, C*m->nbEBands); + intra = 1; + } + } else { + OPUS_COPY(oldEBands, oldEBands_intra, C*m->nbEBands); + OPUS_COPY(error, error_intra, C*m->nbEBands); + } + + if (intra) + *delayedIntra = new_distortion; + else + *delayedIntra = ADD32(MULT16_32_Q15(MULT16_16_Q15(pred_coef[LM], pred_coef[LM]),*delayedIntra), + new_distortion); + + RESTORE_STACK; +} + +void quant_fine_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, opus_val16 *error, int *fine_quant, ec_enc *enc, int C) +{ + int i, c; + + /* Encode finer resolution */ + for (i=start;inbEBands]+QCONST16(.5f,DB_SHIFT))>>(DB_SHIFT-fine_quant[i]); +#else + q2 = (int)floor((error[i+c*m->nbEBands]+.5f)*frac); +#endif + if (q2 > frac-1) + q2 = frac-1; + if (q2<0) + q2 = 0; + ec_enc_bits(enc, q2, fine_quant[i]); +#ifdef FIXED_POINT + offset = SUB16(SHR32(SHL32(EXTEND32(q2),DB_SHIFT)+QCONST16(.5f,DB_SHIFT),fine_quant[i]),QCONST16(.5f,DB_SHIFT)); +#else + offset = (q2+.5f)*(1<<(14-fine_quant[i]))*(1.f/16384) - .5f; +#endif + oldEBands[i+c*m->nbEBands] += offset; + error[i+c*m->nbEBands] -= offset; + /*printf ("%f ", error[i] - offset);*/ + } while (++c < C); + } +} + +void quant_energy_finalise(const CELTMode *m, int start, int end, opus_val16 *oldEBands, opus_val16 *error, int *fine_quant, int *fine_priority, int bits_left, ec_enc *enc, int C) +{ + int i, prio, c; + + /* Use up the remaining bits */ + for (prio=0;prio<2;prio++) + { + for (i=start;i=C ;i++) + { + if (fine_quant[i] >= MAX_FINE_BITS || fine_priority[i]!=prio) + continue; + c=0; + do { + int q2; + opus_val16 offset; + q2 = error[i+c*m->nbEBands]<0 ? 0 : 1; + ec_enc_bits(enc, q2, 1); +#ifdef FIXED_POINT + offset = SHR16(SHL16(q2,DB_SHIFT)-QCONST16(.5f,DB_SHIFT),fine_quant[i]+1); +#else + offset = (q2-.5f)*(1<<(14-fine_quant[i]-1))*(1.f/16384); +#endif + oldEBands[i+c*m->nbEBands] += offset; + error[i+c*m->nbEBands] -= offset; + bits_left--; + } while (++c < C); + } + } +} + +void unquant_coarse_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int intra, ec_dec *dec, int C, int LM) +{ + const unsigned char *prob_model = e_prob_model[LM][intra]; + int i, c; + opus_val32 prev[2] = {0, 0}; + opus_val16 coef; + opus_val16 beta; + opus_int32 budget; + opus_int32 tell; + + if (intra) + { + coef = 0; + beta = beta_intra; + } else { + beta = beta_coef[LM]; + coef = pred_coef[LM]; + } + + budget = dec->storage*8; + + /* Decode at a fixed coarse resolution */ + for (i=start;i=15) + { + int pi; + pi = 2*IMIN(i,20); + qi = ec_laplace_decode(dec, + prob_model[pi]<<7, prob_model[pi+1]<<6); + } + else if(budget-tell>=2) + { + qi = ec_dec_icdf(dec, small_energy_icdf, 2); + qi = (qi>>1)^-(qi&1); + } + else if(budget-tell>=1) + { + qi = -ec_dec_bit_logp(dec, 1); + } + else + qi = -1; + q = (opus_val32)SHL32(EXTEND32(qi),DB_SHIFT); + + oldEBands[i+c*m->nbEBands] = MAX16(-QCONST16(9.f,DB_SHIFT), oldEBands[i+c*m->nbEBands]); + tmp = PSHR32(MULT16_16(coef,oldEBands[i+c*m->nbEBands]),8) + prev[c] + SHL32(q,7); +#ifdef FIXED_POINT + tmp = MAX32(-QCONST32(28.f, DB_SHIFT+7), tmp); +#endif + oldEBands[i+c*m->nbEBands] = PSHR32(tmp, 7); + prev[c] = prev[c] + SHL32(q,7) - MULT16_16(beta,PSHR32(q,8)); + } while (++c < C); + } +} + +void unquant_fine_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int *fine_quant, ec_dec *dec, int C) +{ + int i, c; + /* Decode finer resolution */ + for (i=start;inbEBands] += offset; + } while (++c < C); + } +} + +void unquant_energy_finalise(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int *fine_quant, int *fine_priority, int bits_left, ec_dec *dec, int C) +{ + int i, prio, c; + + /* Use up the remaining bits */ + for (prio=0;prio<2;prio++) + { + for (i=start;i=C ;i++) + { + if (fine_quant[i] >= MAX_FINE_BITS || fine_priority[i]!=prio) + continue; + c=0; + do { + int q2; + opus_val16 offset; + q2 = ec_dec_bits(dec, 1); +#ifdef FIXED_POINT + offset = SHR16(SHL16(q2,DB_SHIFT)-QCONST16(.5f,DB_SHIFT),fine_quant[i]+1); +#else + offset = (q2-.5f)*(1<<(14-fine_quant[i]-1))*(1.f/16384); +#endif + oldEBands[i+c*m->nbEBands] += offset; + bits_left--; + } while (++c < C); + } + } +} + +void amp2Log2(const CELTMode *m, int effEnd, int end, + celt_ener *bandE, opus_val16 *bandLogE, int C) +{ + int c, i; + c=0; + do { + for (i=0;inbEBands] = + celt_log2(bandE[i+c*m->nbEBands]) + - SHL16((opus_val16)eMeans[i],6); +#ifdef FIXED_POINT + /* Compensate for bandE[] being Q12 but celt_log2() taking a Q14 input. */ + bandLogE[i+c*m->nbEBands] += QCONST16(2.f, DB_SHIFT); +#endif + } + for (i=effEnd;inbEBands+i] = -QCONST16(14.f,DB_SHIFT); + } while (++c < C); +} diff --git a/port/third_party/opus/celt/quant_bands.h b/port/third_party/opus/celt/quant_bands.h new file mode 100644 index 00000000..0490bca4 --- /dev/null +++ b/port/third_party/opus/celt/quant_bands.h @@ -0,0 +1,66 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef QUANT_BANDS +#define QUANT_BANDS + +#include "arch.h" +#include "modes.h" +#include "entenc.h" +#include "entdec.h" +#include "mathops.h" + +#ifdef FIXED_POINT +extern const signed char eMeans[25]; +#else +extern const opus_val16 eMeans[25]; +#endif + +void amp2Log2(const CELTMode *m, int effEnd, int end, + celt_ener *bandE, opus_val16 *bandLogE, int C); + +void log2Amp(const CELTMode *m, int start, int end, + celt_ener *eBands, const opus_val16 *oldEBands, int C); + +void quant_coarse_energy(const CELTMode *m, int start, int end, int effEnd, + const opus_val16 *eBands, opus_val16 *oldEBands, opus_uint32 budget, + opus_val16 *error, ec_enc *enc, int C, int LM, + int nbAvailableBytes, int force_intra, opus_val32 *delayedIntra, + int two_pass, int loss_rate, int lfe); + +void quant_fine_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, opus_val16 *error, int *fine_quant, ec_enc *enc, int C); + +void quant_energy_finalise(const CELTMode *m, int start, int end, opus_val16 *oldEBands, opus_val16 *error, int *fine_quant, int *fine_priority, int bits_left, ec_enc *enc, int C); + +void unquant_coarse_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int intra, ec_dec *dec, int C, int LM); + +void unquant_fine_energy(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int *fine_quant, ec_dec *dec, int C); + +void unquant_energy_finalise(const CELTMode *m, int start, int end, opus_val16 *oldEBands, int *fine_quant, int *fine_priority, int bits_left, ec_dec *dec, int C); + +#endif /* QUANT_BANDS */ diff --git a/port/third_party/opus/celt/rate.c b/port/third_party/opus/celt/rate.c new file mode 100644 index 00000000..7f7ad3fa --- /dev/null +++ b/port/third_party/opus/celt/rate.c @@ -0,0 +1,646 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "modes.h" +#include "cwrs.h" +#include "arch.h" +#include "os_support.h" + +#include "entcode.h" +#include "rate.h" + +static const unsigned char LOG2_FRAC_TABLE[24]={ + 0, + 8,13, + 16,19,21,23, + 24,26,27,28,29,30,31,32, + 32,33,34,34,35,36,36,37,37 +}; + +#ifdef CUSTOM_MODES + +/*Determines if V(N,K) fits in a 32-bit unsigned integer. + N and K are themselves limited to 15 bits.*/ +static int fits_in32(int _n, int _k) +{ + static const opus_int16 maxN[15] = { + 32767, 32767, 32767, 1476, 283, 109, 60, 40, + 29, 24, 20, 18, 16, 14, 13}; + static const opus_int16 maxK[15] = { + 32767, 32767, 32767, 32767, 1172, 238, 95, 53, + 36, 27, 22, 18, 16, 15, 13}; + if (_n>=14) + { + if (_k>=14) + return 0; + else + return _n <= maxN[_k]; + } else { + return _k <= maxK[_n]; + } +} + +void compute_pulse_cache(CELTMode *m, int LM) +{ + int C; + int i; + int j; + int curr=0; + int nbEntries=0; + int entryN[100], entryK[100], entryI[100]; + const opus_int16 *eBands = m->eBands; + PulseCache *cache = &m->cache; + opus_int16 *cindex; + unsigned char *bits; + unsigned char *cap; + + cindex = (opus_int16 *)opus_alloc(sizeof(cache->index[0])*m->nbEBands*(LM+2)); + cache->index = cindex; + + /* Scan for all unique band sizes */ + for (i=0;i<=LM+1;i++) + { + for (j=0;jnbEBands;j++) + { + int k; + int N = (eBands[j+1]-eBands[j])<>1; + cindex[i*m->nbEBands+j] = -1; + /* Find other bands that have the same size */ + for (k=0;k<=i;k++) + { + int n; + for (n=0;nnbEBands && (k!=i || n>1) + { + cindex[i*m->nbEBands+j] = cindex[k*m->nbEBands+n]; + break; + } + } + } + if (cache->index[i*m->nbEBands+j] == -1 && N!=0) + { + int K; + entryN[nbEntries] = N; + K = 0; + while (fits_in32(N,get_pulses(K+1)) && KnbEBands+j] = curr; + entryI[nbEntries] = curr; + + curr += K+1; + nbEntries++; + } + } + } + bits = (unsigned char *)opus_alloc(sizeof(unsigned char)*curr); + cache->bits = bits; + cache->size = curr; + /* Compute the cache for all unique sizes */ + for (i=0;icaps = cap = (unsigned char *)opus_alloc(sizeof(cache->caps[0])*(LM+1)*2*m->nbEBands); + for (i=0;i<=LM;i++) + { + for (C=1;C<=2;C++) + { + for (j=0;jnbEBands;j++) + { + int N0; + int max_bits; + N0 = m->eBands[j+1]-m->eBands[j]; + /* N=1 bands only have a sign bit and fine bits. */ + if (N0<1 are even, including custom modes.*/ + if (N0 > 2) + { + N0>>=1; + LM0--; + } + /* N0=1 bands can't be split down to N<2. */ + else if (N0 <= 1) + { + LM0=IMIN(i,1); + N0<<=LM0; + } + /* Compute the cost for the lowest-level PVQ of a fully split + band. */ + pcache = bits + cindex[(LM0+1)*m->nbEBands+j]; + max_bits = pcache[pcache[0]]+1; + /* Add in the cost of coding regular splits. */ + N = N0; + for(k=0;klogN[j]+((LM0+k)<>1)-QTHETA_OFFSET; + /* The number of qtheta bits we'll allocate if the remainder + is to be max_bits. + The average measured cost for theta is 0.89701 times qb, + approximated here as 459/512. */ + num=459*(opus_int32)((2*N-1)*offset+max_bits); + den=((opus_int32)(2*N-1)<<9)-459; + qb = IMIN((num+(den>>1))/den, 57); + celt_assert(qb >= 0); + max_bits += qb; + N <<= 1; + } + /* Add in the cost of a stereo split, if necessary. */ + if (C==2) + { + max_bits <<= 1; + offset = ((m->logN[j]+(i<>1)-(N==2?QTHETA_OFFSET_TWOPHASE:QTHETA_OFFSET); + ndof = 2*N-1-(N==2); + /* The average measured cost for theta with the step PDF is + 0.95164 times qb, approximated here as 487/512. */ + num = (N==2?512:487)*(opus_int32)(max_bits+ndof*offset); + den = ((opus_int32)ndof<<9)-(N==2?512:487); + qb = IMIN((num+(den>>1))/den, (N==2?64:61)); + celt_assert(qb >= 0); + max_bits += qb; + } + /* Add the fine bits we'll use. */ + /* Compensate for the extra DoF in stereo */ + ndof = C*N + ((C==2 && N>2) ? 1 : 0); + /* Offset the number of fine bits by log2(N)/2 + FINE_OFFSET + compared to their "fair share" of total/N */ + offset = ((m->logN[j] + (i<>1)-FINE_OFFSET; + /* N=2 is the only point that doesn't match the curve */ + if (N==2) + offset += 1<>2; + /* The number of fine bits we'll allocate if the remainder is + to be max_bits. */ + num = max_bits+ndof*offset; + den = (ndof-1)<>1))/den, MAX_FINE_BITS); + celt_assert(qb >= 0); + max_bits += C*qb<eBands[j+1]-m->eBands[j])<= 0); + celt_assert(max_bits < 256); + *cap++ = (unsigned char)max_bits; + } + } + } +} + +#endif /* CUSTOM_MODES */ + +#define ALLOC_STEPS 6 + +static OPUS_INLINE int interp_bits2pulses(const CELTMode *m, int start, int end, int skip_start, + const int *bits1, const int *bits2, const int *thresh, const int *cap, opus_int32 total, opus_int32 *_balance, + int skip_rsv, int *intensity, int intensity_rsv, int *dual_stereo, int dual_stereo_rsv, int *bits, + int *ebits, int *fine_priority, int C, int LM, ec_ctx *ec, int encode, int prev, int signalBandwidth) +{ + opus_int32 psum; + int lo, hi; + int i, j; + int logM; + int stereo; + int codedBands=-1; + int alloc_floor; + opus_int32 left, percoeff; + int done; + opus_int32 balance; + SAVE_STACK; + + alloc_floor = C<1; + + logM = LM<>1; + psum = 0; + done = 0; + for (j=end;j-->start;) + { + int tmp = bits1[j] + (mid*(opus_int32)bits2[j]>>ALLOC_STEPS); + if (tmp >= thresh[j] || done) + { + done = 1; + /* Don't allocate more than we can actually use */ + psum += IMIN(tmp, cap[j]); + } else { + if (tmp >= alloc_floor) + psum += alloc_floor; + } + } + if (psum > total) + hi = mid; + else + lo = mid; + } + psum = 0; + /*printf ("interp bisection gave %d\n", lo);*/ + done = 0; + for (j=end;j-->start;) + { + int tmp = bits1[j] + ((opus_int32)lo*bits2[j]>>ALLOC_STEPS); + if (tmp < thresh[j] && !done) + { + if (tmp >= alloc_floor) + tmp = alloc_floor; + else + tmp = 0; + } else + done = 1; + /* Don't allocate more than we can actually use */ + tmp = IMIN(tmp, cap[j]); + bits[j] = tmp; + psum += tmp; + } + + /* Decide which bands to skip, working backwards from the end. */ + for (codedBands=end;;codedBands--) + { + int band_width; + int band_bits; + int rem; + j = codedBands-1; + /* Never skip the first band, nor a band that has been boosted by + dynalloc. + In the first case, we'd be coding a bit to signal we're going to waste + all the other bits. + In the second case, we'd be coding a bit to redistribute all the bits + we just signaled should be cocentrated in this band. */ + if (j<=skip_start) + { + /* Give the bit we reserved to end skipping back. */ + total += skip_rsv; + break; + } + /*Figure out how many left-over bits we would be adding to this band. + This can include bits we've stolen back from higher, skipped bands.*/ + left = total-psum; + percoeff = celt_udiv(left, m->eBands[codedBands]-m->eBands[start]); + left -= (m->eBands[codedBands]-m->eBands[start])*percoeff; + rem = IMAX(left-(m->eBands[j]-m->eBands[start]),0); + band_width = m->eBands[codedBands]-m->eBands[j]; + band_bits = (int)(bits[j] + percoeff*band_width + rem); + /*Only code a skip decision if we're above the threshold for this band. + Otherwise it is force-skipped. + This ensures that we have enough bits to code the skip flag.*/ + if (band_bits >= IMAX(thresh[j], alloc_floor+(1< 17) + depth_threshold = j (depth_threshold*band_width<>4 && j<=signalBandwidth)) +#endif + { + ec_enc_bit_logp(ec, 1, 1); + break; + } + ec_enc_bit_logp(ec, 0, 1); + } else if (ec_dec_bit_logp(ec, 1)) { + break; + } + /*We used a bit to skip this band.*/ + psum += 1< 0) + intensity_rsv = LOG2_FRAC_TABLE[j-start]; + psum += intensity_rsv; + if (band_bits >= alloc_floor) + { + /*If we have enough for a fine energy bit per channel, use it.*/ + psum += alloc_floor; + bits[j] = alloc_floor; + } else { + /*Otherwise this band gets nothing at all.*/ + bits[j] = 0; + } + } + + celt_assert(codedBands > start); + /* Code the intensity and dual stereo parameters. */ + if (intensity_rsv > 0) + { + if (encode) + { + *intensity = IMIN(*intensity, codedBands); + ec_enc_uint(ec, *intensity-start, codedBands+1-start); + } + else + *intensity = start+ec_dec_uint(ec, codedBands+1-start); + } + else + *intensity = 0; + if (*intensity <= start) + { + total += dual_stereo_rsv; + dual_stereo_rsv = 0; + } + if (dual_stereo_rsv > 0) + { + if (encode) + ec_enc_bit_logp(ec, *dual_stereo, 1); + else + *dual_stereo = ec_dec_bit_logp(ec, 1); + } + else + *dual_stereo = 0; + + /* Allocate the remaining bits */ + left = total-psum; + percoeff = celt_udiv(left, m->eBands[codedBands]-m->eBands[start]); + left -= (m->eBands[codedBands]-m->eBands[start])*percoeff; + for (j=start;jeBands[j+1]-m->eBands[j])); + for (j=start;jeBands[j+1]-m->eBands[j]); + bits[j] += tmp; + left -= tmp; + } + /*for (j=0;j= 0); + N0 = m->eBands[j+1]-m->eBands[j]; + N=N0<1) + { + excess = MAX32(bit-cap[j],0); + bits[j] = bit-excess; + + /* Compensate for the extra DoF in stereo */ + den=(C*N+ ((C==2 && N>2 && !*dual_stereo && j<*intensity) ? 1 : 0)); + + NClogN = den*(m->logN[j] + logM); + + /* Offset for the number of fine bits by log2(N)/2 + FINE_OFFSET + compared to their "fair share" of total/N */ + offset = (NClogN>>1)-den*FINE_OFFSET; + + /* N=2 is the only point that doesn't match the curve */ + if (N==2) + offset += den<>2; + + /* Changing the offset for allocating the second and third + fine energy bit */ + if (bits[j] + offset < den*2<>2; + else if (bits[j] + offset < den*3<>3; + + /* Divide with rounding */ + ebits[j] = IMAX(0, (bits[j] + offset + (den<<(BITRES-1)))); + ebits[j] = celt_udiv(ebits[j], den)>>BITRES; + + /* Make sure not to bust */ + if (C*ebits[j] > (bits[j]>>BITRES)) + ebits[j] = bits[j] >> stereo >> BITRES; + + /* More than that is useless because that's about as far as PVQ can go */ + ebits[j] = IMIN(ebits[j], MAX_FINE_BITS); + + /* If we rounded down or capped this band, make it a candidate for the + final fine energy pass */ + fine_priority[j] = ebits[j]*(den<= bits[j]+offset; + + /* Remove the allocated fine bits; the rest are assigned to PVQ */ + bits[j] -= C*ebits[j]< 0) + { + int extra_fine; + int extra_bits; + extra_fine = IMIN(excess>>(stereo+BITRES),MAX_FINE_BITS-ebits[j]); + ebits[j] += extra_fine; + extra_bits = extra_fine*C<= excess-balance; + excess -= extra_bits; + } + balance = excess; + + celt_assert(bits[j] >= 0); + celt_assert(ebits[j] >= 0); + } + /* Save any remaining bits over the cap for the rebalancing in + quant_all_bands(). */ + *_balance = balance; + + /* The skipped bands use all their bits for fine energy. */ + for (;j> stereo >> BITRES; + celt_assert(C*ebits[j]<nbEBands; + skip_start = start; + /* Reserve a bit to signal the end of manually skipped bands. */ + skip_rsv = total >= 1<total) + intensity_rsv = 0; + else + { + total -= intensity_rsv; + dual_stereo_rsv = total>=1<eBands[j+1]-m->eBands[j])<>4); + /* Tilt of the allocation curve */ + trim_offset[j] = C*(m->eBands[j+1]-m->eBands[j])*(alloc_trim-5-LM)*(end-j-1) + *(1<<(LM+BITRES))>>6; + /* Giving less resolution to single-coefficient bands because they get + more benefit from having one coarse value per coefficient*/ + if ((m->eBands[j+1]-m->eBands[j])<nbAllocVectors - 1; + do + { + int done = 0; + int psum = 0; + int mid = (lo+hi) >> 1; + for (j=end;j-->start;) + { + int bitsj; + int N = m->eBands[j+1]-m->eBands[j]; + bitsj = C*N*m->allocVectors[mid*len+j]<>2; + if (bitsj > 0) + bitsj = IMAX(0, bitsj + trim_offset[j]); + bitsj += offsets[j]; + if (bitsj >= thresh[j] || done) + { + done = 1; + /* Don't allocate more than we can actually use */ + psum += IMIN(bitsj, cap[j]); + } else { + if (bitsj >= C< total) + hi = mid - 1; + else + lo = mid + 1; + /*printf ("lo = %d, hi = %d\n", lo, hi);*/ + } + while (lo <= hi); + hi = lo--; + /*printf ("interp between %d and %d\n", lo, hi);*/ + for (j=start;jeBands[j+1]-m->eBands[j]; + bits1j = C*N*m->allocVectors[lo*len+j]<>2; + bits2j = hi>=m->nbAllocVectors ? + cap[j] : C*N*m->allocVectors[hi*len+j]<>2; + if (bits1j > 0) + bits1j = IMAX(0, bits1j + trim_offset[j]); + if (bits2j > 0) + bits2j = IMAX(0, bits2j + trim_offset[j]); + if (lo > 0) + bits1j += offsets[j]; + bits2j += offsets[j]; + if (offsets[j]>0) + skip_start = j; + bits2j = IMAX(0,bits2j-bits1j); + bits1[j] = bits1j; + bits2[j] = bits2j; + } + codedBands = interp_bits2pulses(m, start, end, skip_start, bits1, bits2, thresh, cap, + total, balance, skip_rsv, intensity, intensity_rsv, dual_stereo, dual_stereo_rsv, + pulses, ebits, fine_priority, C, LM, ec, encode, prev, signalBandwidth); + RESTORE_STACK; + return codedBands; +} + diff --git a/port/third_party/opus/celt/rate.h b/port/third_party/opus/celt/rate.h new file mode 100644 index 00000000..fad5e412 --- /dev/null +++ b/port/third_party/opus/celt/rate.h @@ -0,0 +1,101 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef RATE_H +#define RATE_H + +#define MAX_PSEUDO 40 +#define LOG_MAX_PSEUDO 6 + +#define CELT_MAX_PULSES 128 + +#define MAX_FINE_BITS 8 + +#define FINE_OFFSET 21 +#define QTHETA_OFFSET 4 +#define QTHETA_OFFSET_TWOPHASE 16 + +#include "cwrs.h" +#include "modes.h" + +void compute_pulse_cache(CELTMode *m, int LM); + +static OPUS_INLINE int get_pulses(int i) +{ + return i<8 ? i : (8 + (i&7)) << ((i>>3)-1); +} + +static OPUS_INLINE int bits2pulses(const CELTMode *m, int band, int LM, int bits) +{ + int i; + int lo, hi; + const unsigned char *cache; + + LM++; + cache = m->cache.bits + m->cache.index[LM*m->nbEBands+band]; + + lo = 0; + hi = cache[0]; + bits--; + for (i=0;i>1; + /* OPT: Make sure this is implemented with a conditional move */ + if ((int)cache[mid] >= bits) + hi = mid; + else + lo = mid; + } + if (bits- (lo == 0 ? -1 : (int)cache[lo]) <= (int)cache[hi]-bits) + return lo; + else + return hi; +} + +static OPUS_INLINE int pulses2bits(const CELTMode *m, int band, int LM, int pulses) +{ + const unsigned char *cache; + + LM++; + cache = m->cache.bits + m->cache.index[LM*m->nbEBands+band]; + return pulses == 0 ? 0 : cache[pulses]+1; +} + +/** Compute the pulse allocation, i.e. how many pulses will go in each + * band. + @param m mode + @param offsets Requested increase or decrease in the number of bits for + each band + @param total Number of bands + @param pulses Number of pulses per band (returned) + @return Total number of bits allocated +*/ +int clt_compute_allocation(const CELTMode *m, int start, int end, const int *offsets, const int *cap, int alloc_trim, int *intensity, int *dual_stereo, + opus_int32 total, opus_int32 *balance, int *pulses, int *ebits, int *fine_priority, int C, int LM, ec_ctx *ec, int encode, int prev, int signalBandwidth); + +#endif diff --git a/port/third_party/opus/celt/stack_alloc.h b/port/third_party/opus/celt/stack_alloc.h new file mode 100644 index 00000000..e2739bdf --- /dev/null +++ b/port/third_party/opus/celt/stack_alloc.h @@ -0,0 +1,184 @@ +/* Copyright (C) 2002-2003 Jean-Marc Valin + Copyright (C) 2007-2009 Xiph.Org Foundation */ +/** + @file stack_alloc.h + @brief Temporary memory allocation on stack +*/ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef STACK_ALLOC_H +#define STACK_ALLOC_H + +#include "opus_types.h" +#include "opus_defines.h" + +#if (!defined (VAR_ARRAYS) && !defined (USE_ALLOCA) && !defined (NONTHREADSAFE_PSEUDOSTACK)) +#error "Opus requires one of VAR_ARRAYS, USE_ALLOCA, or NONTHREADSAFE_PSEUDOSTACK be defined to select the temporary allocation mode." +#endif + +#ifdef USE_ALLOCA +# ifdef _WIN32 +# include +# else +# ifdef HAVE_ALLOCA_H +# include +# else +# include +# endif +# endif +#endif + +/** + * @def ALIGN(stack, size) + * + * Aligns the stack to a 'size' boundary + * + * @param stack Stack + * @param size New size boundary + */ + +/** + * @def PUSH(stack, size, type) + * + * Allocates 'size' elements of type 'type' on the stack + * + * @param stack Stack + * @param size Number of elements + * @param type Type of element + */ + +/** + * @def VARDECL(var) + * + * Declare variable on stack + * + * @param var Variable to declare + */ + +/** + * @def ALLOC(var, size, type) + * + * Allocate 'size' elements of 'type' on stack + * + * @param var Name of variable to allocate + * @param size Number of elements + * @param type Type of element + */ + +#if defined(VAR_ARRAYS) + +#define VARDECL(type, var) +#define ALLOC(var, size, type) type var[size] +#define SAVE_STACK +#define RESTORE_STACK +#define ALLOC_STACK +/* C99 does not allow VLAs of size zero */ +#define ALLOC_NONE 1 + +#elif defined(USE_ALLOCA) + +#define VARDECL(type, var) type *var + +# ifdef _WIN32 +# define ALLOC(var, size, type) var = ((type*)_alloca(sizeof(type)*(size))) +# else +# define ALLOC(var, size, type) var = ((type*)alloca(sizeof(type)*(size))) +# endif + +#define SAVE_STACK +#define RESTORE_STACK +#define ALLOC_STACK +#define ALLOC_NONE 0 + +#else + +#ifdef CELT_C +char *scratch_ptr=0; +char *global_stack=0; +#else +extern char *global_stack; +extern char *scratch_ptr; +#endif /* CELT_C */ + +#ifdef ENABLE_VALGRIND + +#include + +#ifdef CELT_C +char *global_stack_top=0; +#else +extern char *global_stack_top; +#endif /* CELT_C */ + +#define ALIGN(stack, size) ((stack) += ((size) - (long)(stack)) & ((size) - 1)) +#define PUSH(stack, size, type) (VALGRIND_MAKE_MEM_NOACCESS(stack, global_stack_top-stack),ALIGN((stack),sizeof(type)/sizeof(char)),VALGRIND_MAKE_MEM_UNDEFINED(stack, ((size)*sizeof(type)/sizeof(char))),(stack)+=(2*(size)*sizeof(type)/sizeof(char)),(type*)((stack)-(2*(size)*sizeof(type)/sizeof(char)))) +#define RESTORE_STACK ((global_stack = _saved_stack),VALGRIND_MAKE_MEM_NOACCESS(global_stack, global_stack_top-global_stack)) +#define ALLOC_STACK char *_saved_stack; ((global_stack = (global_stack==0) ? ((global_stack_top=opus_alloc_scratch(GLOBAL_STACK_SIZE*2)+(GLOBAL_STACK_SIZE*2))-(GLOBAL_STACK_SIZE*2)) : global_stack),VALGRIND_MAKE_MEM_NOACCESS(global_stack, global_stack_top-global_stack)); _saved_stack = global_stack; + +#else + +#define ALIGN(stack, size) ((stack) += ((size) - (long)(stack)) & ((size) - 1)) +#define PUSH(stack, size, type) (ALIGN((stack),sizeof(type)/(sizeof(char))),(stack)+=(size)*(sizeof(type)/(sizeof(char))),(type*)((stack)-(size)*(sizeof(type)/(sizeof(char))))) +#if 0 /* Set this to 1 to instrument pseudostack usage */ +#define RESTORE_STACK (printf("%ld %s:%d\n", global_stack-scratch_ptr, __FILE__, __LINE__),global_stack = _saved_stack) +#else +#define RESTORE_STACK (global_stack = _saved_stack) +#endif +#define ALLOC_STACK char *_saved_stack; (global_stack = (global_stack==0) ? (scratch_ptr=opus_alloc_scratch(GLOBAL_STACK_SIZE)) : global_stack); _saved_stack = global_stack; + +#endif /* ENABLE_VALGRIND */ + +#include "os_support.h" +#define VARDECL(type, var) type *var +#define ALLOC(var, size, type) var = PUSH(global_stack, size, type) +#define SAVE_STACK char *_saved_stack = global_stack; +#define ALLOC_NONE 0 + +#endif /* VAR_ARRAYS */ + + +#ifdef ENABLE_VALGRIND + +#include +#define OPUS_CHECK_ARRAY(ptr, len) VALGRIND_CHECK_MEM_IS_DEFINED(ptr, len*sizeof(*ptr)) +#define OPUS_CHECK_VALUE(value) VALGRIND_CHECK_VALUE_IS_DEFINED(value) +#define OPUS_CHECK_ARRAY_COND(ptr, len) VALGRIND_CHECK_MEM_IS_DEFINED(ptr, len*sizeof(*ptr)) +#define OPUS_CHECK_VALUE_COND(value) VALGRIND_CHECK_VALUE_IS_DEFINED(value) +#define OPUS_PRINT_INT(value) do {fprintf(stderr, #value " = %d at %s:%d\n", value, __FILE__, __LINE__);}while(0) +#define OPUS_FPRINTF fprintf + +#else + +static OPUS_INLINE int _opus_false(void) {return 0;} +#define OPUS_CHECK_ARRAY(ptr, len) _opus_false() +#define OPUS_CHECK_VALUE(value) _opus_false() +#define OPUS_PRINT_INT(value) do{}while(0) +#define OPUS_FPRINTF (void) + +#endif + + +#endif /* STACK_ALLOC_H */ diff --git a/port/third_party/opus/celt/static_modes_fixed.h b/port/third_party/opus/celt/static_modes_fixed.h new file mode 100644 index 00000000..8717d626 --- /dev/null +++ b/port/third_party/opus/celt/static_modes_fixed.h @@ -0,0 +1,892 @@ +/* The contents of this file was automatically generated by dump_modes.c + with arguments: 48000 960 + It contains static definitions for some pre-defined modes. */ +#include "modes.h" +#include "rate.h" + +#ifdef HAVE_ARM_NE10 +#define OVERRIDE_FFT 1 +#include "static_modes_fixed_arm_ne10.h" +#endif + +#ifndef DEF_WINDOW120 +#define DEF_WINDOW120 +static const opus_val16 window120[120] = { +2, 20, 55, 108, 178, +266, 372, 494, 635, 792, +966, 1157, 1365, 1590, 1831, +2089, 2362, 2651, 2956, 3276, +3611, 3961, 4325, 4703, 5094, +5499, 5916, 6346, 6788, 7241, +7705, 8179, 8663, 9156, 9657, +10167, 10684, 11207, 11736, 12271, +12810, 13353, 13899, 14447, 14997, +15547, 16098, 16648, 17197, 17744, +18287, 18827, 19363, 19893, 20418, +20936, 21447, 21950, 22445, 22931, +23407, 23874, 24330, 24774, 25208, +25629, 26039, 26435, 26819, 27190, +27548, 27893, 28224, 28541, 28845, +29135, 29411, 29674, 29924, 30160, +30384, 30594, 30792, 30977, 31151, +31313, 31463, 31602, 31731, 31849, +31958, 32057, 32148, 32229, 32303, +32370, 32429, 32481, 32528, 32568, +32604, 32634, 32661, 32683, 32701, +32717, 32729, 32740, 32748, 32754, +32758, 32762, 32764, 32766, 32767, +32767, 32767, 32767, 32767, 32767, +}; +#endif + +#ifndef DEF_LOGN400 +#define DEF_LOGN400 +static const opus_int16 logN400[21] = { +0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 16, 16, 16, 21, 21, 24, 29, 34, 36, }; +#endif + +#ifndef DEF_PULSE_CACHE50 +#define DEF_PULSE_CACHE50 +static const opus_int16 cache_index50[105] = { +-1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 41, 41, 41, +82, 82, 123, 164, 200, 222, 0, 0, 0, 0, 0, 0, 0, 0, 41, +41, 41, 41, 123, 123, 123, 164, 164, 240, 266, 283, 295, 41, 41, 41, +41, 41, 41, 41, 41, 123, 123, 123, 123, 240, 240, 240, 266, 266, 305, +318, 328, 336, 123, 123, 123, 123, 123, 123, 123, 123, 240, 240, 240, 240, +305, 305, 305, 318, 318, 343, 351, 358, 364, 240, 240, 240, 240, 240, 240, +240, 240, 305, 305, 305, 305, 343, 343, 343, 351, 351, 370, 376, 382, 387, +}; +static const unsigned char cache_bits50[392] = { +40, 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, 40, 15, 23, 28, +31, 34, 36, 38, 39, 41, 42, 43, 44, 45, 46, 47, 47, 49, 50, +51, 52, 53, 54, 55, 55, 57, 58, 59, 60, 61, 62, 63, 63, 65, +66, 67, 68, 69, 70, 71, 71, 40, 20, 33, 41, 48, 53, 57, 61, +64, 66, 69, 71, 73, 75, 76, 78, 80, 82, 85, 87, 89, 91, 92, +94, 96, 98, 101, 103, 105, 107, 108, 110, 112, 114, 117, 119, 121, 123, +124, 126, 128, 40, 23, 39, 51, 60, 67, 73, 79, 83, 87, 91, 94, +97, 100, 102, 105, 107, 111, 115, 118, 121, 124, 126, 129, 131, 135, 139, +142, 145, 148, 150, 153, 155, 159, 163, 166, 169, 172, 174, 177, 179, 35, +28, 49, 65, 78, 89, 99, 107, 114, 120, 126, 132, 136, 141, 145, 149, +153, 159, 165, 171, 176, 180, 185, 189, 192, 199, 205, 211, 216, 220, 225, +229, 232, 239, 245, 251, 21, 33, 58, 79, 97, 112, 125, 137, 148, 157, +166, 174, 182, 189, 195, 201, 207, 217, 227, 235, 243, 251, 17, 35, 63, +86, 106, 123, 139, 152, 165, 177, 187, 197, 206, 214, 222, 230, 237, 250, +25, 31, 55, 75, 91, 105, 117, 128, 138, 146, 154, 161, 168, 174, 180, +185, 190, 200, 208, 215, 222, 229, 235, 240, 245, 255, 16, 36, 65, 89, +110, 128, 144, 159, 173, 185, 196, 207, 217, 226, 234, 242, 250, 11, 41, +74, 103, 128, 151, 172, 191, 209, 225, 241, 255, 9, 43, 79, 110, 138, +163, 186, 207, 227, 246, 12, 39, 71, 99, 123, 144, 164, 182, 198, 214, +228, 241, 253, 9, 44, 81, 113, 142, 168, 192, 214, 235, 255, 7, 49, +90, 127, 160, 191, 220, 247, 6, 51, 95, 134, 170, 203, 234, 7, 47, +87, 123, 155, 184, 212, 237, 6, 52, 97, 137, 174, 208, 240, 5, 57, +106, 151, 192, 231, 5, 59, 111, 158, 202, 243, 5, 55, 103, 147, 187, +224, 5, 60, 113, 161, 206, 248, 4, 65, 122, 175, 224, 4, 67, 127, +182, 234, }; +static const unsigned char cache_caps50[168] = { +224, 224, 224, 224, 224, 224, 224, 224, 160, 160, 160, 160, 185, 185, 185, +178, 178, 168, 134, 61, 37, 224, 224, 224, 224, 224, 224, 224, 224, 240, +240, 240, 240, 207, 207, 207, 198, 198, 183, 144, 66, 40, 160, 160, 160, +160, 160, 160, 160, 160, 185, 185, 185, 185, 193, 193, 193, 183, 183, 172, +138, 64, 38, 240, 240, 240, 240, 240, 240, 240, 240, 207, 207, 207, 207, +204, 204, 204, 193, 193, 180, 143, 66, 40, 185, 185, 185, 185, 185, 185, +185, 185, 193, 193, 193, 193, 193, 193, 193, 183, 183, 172, 138, 65, 39, +207, 207, 207, 207, 207, 207, 207, 207, 204, 204, 204, 204, 201, 201, 201, +188, 188, 176, 141, 66, 40, 193, 193, 193, 193, 193, 193, 193, 193, 193, +193, 193, 193, 194, 194, 194, 184, 184, 173, 139, 65, 39, 204, 204, 204, +204, 204, 204, 204, 204, 201, 201, 201, 201, 198, 198, 198, 187, 187, 175, +140, 66, 40, }; +#endif + +#ifndef FFT_TWIDDLES48000_960 +#define FFT_TWIDDLES48000_960 +static const kiss_twiddle_cpx fft_twiddles48000_960[480] = { +{32767, 0}, {32766, -429}, +{32757, -858}, {32743, -1287}, +{32724, -1715}, {32698, -2143}, +{32667, -2570}, {32631, -2998}, +{32588, -3425}, {32541, -3851}, +{32488, -4277}, {32429, -4701}, +{32364, -5125}, {32295, -5548}, +{32219, -5971}, {32138, -6393}, +{32051, -6813}, {31960, -7231}, +{31863, -7650}, {31760, -8067}, +{31652, -8481}, {31539, -8895}, +{31419, -9306}, {31294, -9716}, +{31165, -10126}, {31030, -10532}, +{30889, -10937}, {30743, -11340}, +{30592, -11741}, {30436, -12141}, +{30274, -12540}, {30107, -12935}, +{29936, -13328}, {29758, -13718}, +{29577, -14107}, {29390, -14493}, +{29197, -14875}, {29000, -15257}, +{28797, -15635}, {28590, -16010}, +{28379, -16384}, {28162, -16753}, +{27940, -17119}, {27714, -17484}, +{27482, -17845}, {27246, -18205}, +{27006, -18560}, {26760, -18911}, +{26510, -19260}, {26257, -19606}, +{25997, -19947}, {25734, -20286}, +{25466, -20621}, {25194, -20952}, +{24918, -21281}, {24637, -21605}, +{24353, -21926}, {24063, -22242}, +{23770, -22555}, {23473, -22865}, +{23171, -23171}, {22866, -23472}, +{22557, -23769}, {22244, -24063}, +{21927, -24352}, {21606, -24636}, +{21282, -24917}, {20954, -25194}, +{20622, -25465}, {20288, -25733}, +{19949, -25997}, {19607, -26255}, +{19261, -26509}, {18914, -26760}, +{18561, -27004}, {18205, -27246}, +{17846, -27481}, {17485, -27713}, +{17122, -27940}, {16755, -28162}, +{16385, -28378}, {16012, -28590}, +{15636, -28797}, {15258, -28999}, +{14878, -29197}, {14494, -29389}, +{14108, -29576}, {13720, -29757}, +{13329, -29934}, {12937, -30107}, +{12540, -30274}, {12142, -30435}, +{11744, -30592}, {11342, -30743}, +{10939, -30889}, {10534, -31030}, +{10127, -31164}, {9718, -31294}, +{9307, -31418}, {8895, -31537}, +{8482, -31652}, {8067, -31759}, +{7650, -31862}, {7233, -31960}, +{6815, -32051}, {6393, -32138}, +{5973, -32219}, {5549, -32294}, +{5127, -32364}, {4703, -32429}, +{4278, -32487}, {3852, -32541}, +{3426, -32588}, {2999, -32630}, +{2572, -32667}, {2144, -32698}, +{1716, -32724}, {1287, -32742}, +{860, -32757}, {430, -32766}, +{0, -32767}, {-429, -32766}, +{-858, -32757}, {-1287, -32743}, +{-1715, -32724}, {-2143, -32698}, +{-2570, -32667}, {-2998, -32631}, +{-3425, -32588}, {-3851, -32541}, +{-4277, -32488}, {-4701, -32429}, +{-5125, -32364}, {-5548, -32295}, +{-5971, -32219}, {-6393, -32138}, +{-6813, -32051}, {-7231, -31960}, +{-7650, -31863}, {-8067, -31760}, +{-8481, -31652}, {-8895, -31539}, +{-9306, -31419}, {-9716, -31294}, +{-10126, -31165}, {-10532, -31030}, +{-10937, -30889}, {-11340, -30743}, +{-11741, -30592}, {-12141, -30436}, +{-12540, -30274}, {-12935, -30107}, +{-13328, -29936}, {-13718, -29758}, +{-14107, -29577}, {-14493, -29390}, +{-14875, -29197}, {-15257, -29000}, +{-15635, -28797}, {-16010, -28590}, +{-16384, -28379}, {-16753, -28162}, +{-17119, -27940}, {-17484, -27714}, +{-17845, -27482}, {-18205, -27246}, +{-18560, -27006}, {-18911, -26760}, +{-19260, -26510}, {-19606, -26257}, +{-19947, -25997}, {-20286, -25734}, +{-20621, -25466}, {-20952, -25194}, +{-21281, -24918}, {-21605, -24637}, +{-21926, -24353}, {-22242, -24063}, +{-22555, -23770}, {-22865, -23473}, +{-23171, -23171}, {-23472, -22866}, +{-23769, -22557}, {-24063, -22244}, +{-24352, -21927}, {-24636, -21606}, +{-24917, -21282}, {-25194, -20954}, +{-25465, -20622}, {-25733, -20288}, +{-25997, -19949}, {-26255, -19607}, +{-26509, -19261}, {-26760, -18914}, +{-27004, -18561}, {-27246, -18205}, +{-27481, -17846}, {-27713, -17485}, +{-27940, -17122}, {-28162, -16755}, +{-28378, -16385}, {-28590, -16012}, +{-28797, -15636}, {-28999, -15258}, +{-29197, -14878}, {-29389, -14494}, +{-29576, -14108}, {-29757, -13720}, +{-29934, -13329}, {-30107, -12937}, +{-30274, -12540}, {-30435, -12142}, +{-30592, -11744}, {-30743, -11342}, +{-30889, -10939}, {-31030, -10534}, +{-31164, -10127}, {-31294, -9718}, +{-31418, -9307}, {-31537, -8895}, +{-31652, -8482}, {-31759, -8067}, +{-31862, -7650}, {-31960, -7233}, +{-32051, -6815}, {-32138, -6393}, +{-32219, -5973}, {-32294, -5549}, +{-32364, -5127}, {-32429, -4703}, +{-32487, -4278}, {-32541, -3852}, +{-32588, -3426}, {-32630, -2999}, +{-32667, -2572}, {-32698, -2144}, +{-32724, -1716}, {-32742, -1287}, +{-32757, -860}, {-32766, -430}, +{-32767, 0}, {-32766, 429}, +{-32757, 858}, {-32743, 1287}, +{-32724, 1715}, {-32698, 2143}, +{-32667, 2570}, {-32631, 2998}, +{-32588, 3425}, {-32541, 3851}, +{-32488, 4277}, {-32429, 4701}, +{-32364, 5125}, {-32295, 5548}, +{-32219, 5971}, {-32138, 6393}, +{-32051, 6813}, {-31960, 7231}, +{-31863, 7650}, {-31760, 8067}, +{-31652, 8481}, {-31539, 8895}, +{-31419, 9306}, {-31294, 9716}, +{-31165, 10126}, {-31030, 10532}, +{-30889, 10937}, {-30743, 11340}, +{-30592, 11741}, {-30436, 12141}, +{-30274, 12540}, {-30107, 12935}, +{-29936, 13328}, {-29758, 13718}, +{-29577, 14107}, {-29390, 14493}, +{-29197, 14875}, {-29000, 15257}, +{-28797, 15635}, {-28590, 16010}, +{-28379, 16384}, {-28162, 16753}, +{-27940, 17119}, {-27714, 17484}, +{-27482, 17845}, {-27246, 18205}, +{-27006, 18560}, {-26760, 18911}, +{-26510, 19260}, {-26257, 19606}, +{-25997, 19947}, {-25734, 20286}, +{-25466, 20621}, {-25194, 20952}, +{-24918, 21281}, {-24637, 21605}, +{-24353, 21926}, {-24063, 22242}, +{-23770, 22555}, {-23473, 22865}, +{-23171, 23171}, {-22866, 23472}, +{-22557, 23769}, {-22244, 24063}, +{-21927, 24352}, {-21606, 24636}, +{-21282, 24917}, {-20954, 25194}, +{-20622, 25465}, {-20288, 25733}, +{-19949, 25997}, {-19607, 26255}, +{-19261, 26509}, {-18914, 26760}, +{-18561, 27004}, {-18205, 27246}, +{-17846, 27481}, {-17485, 27713}, +{-17122, 27940}, {-16755, 28162}, +{-16385, 28378}, {-16012, 28590}, +{-15636, 28797}, {-15258, 28999}, +{-14878, 29197}, {-14494, 29389}, +{-14108, 29576}, {-13720, 29757}, +{-13329, 29934}, {-12937, 30107}, +{-12540, 30274}, {-12142, 30435}, +{-11744, 30592}, {-11342, 30743}, +{-10939, 30889}, {-10534, 31030}, +{-10127, 31164}, {-9718, 31294}, +{-9307, 31418}, {-8895, 31537}, +{-8482, 31652}, {-8067, 31759}, +{-7650, 31862}, {-7233, 31960}, +{-6815, 32051}, {-6393, 32138}, +{-5973, 32219}, {-5549, 32294}, +{-5127, 32364}, {-4703, 32429}, +{-4278, 32487}, {-3852, 32541}, +{-3426, 32588}, {-2999, 32630}, +{-2572, 32667}, {-2144, 32698}, +{-1716, 32724}, {-1287, 32742}, +{-860, 32757}, {-430, 32766}, +{0, 32767}, {429, 32766}, +{858, 32757}, {1287, 32743}, +{1715, 32724}, {2143, 32698}, +{2570, 32667}, {2998, 32631}, +{3425, 32588}, {3851, 32541}, +{4277, 32488}, {4701, 32429}, +{5125, 32364}, {5548, 32295}, +{5971, 32219}, {6393, 32138}, +{6813, 32051}, {7231, 31960}, +{7650, 31863}, {8067, 31760}, +{8481, 31652}, {8895, 31539}, +{9306, 31419}, {9716, 31294}, +{10126, 31165}, {10532, 31030}, +{10937, 30889}, {11340, 30743}, +{11741, 30592}, {12141, 30436}, +{12540, 30274}, {12935, 30107}, +{13328, 29936}, {13718, 29758}, +{14107, 29577}, {14493, 29390}, +{14875, 29197}, {15257, 29000}, +{15635, 28797}, {16010, 28590}, +{16384, 28379}, {16753, 28162}, +{17119, 27940}, {17484, 27714}, +{17845, 27482}, {18205, 27246}, +{18560, 27006}, {18911, 26760}, +{19260, 26510}, {19606, 26257}, +{19947, 25997}, {20286, 25734}, +{20621, 25466}, {20952, 25194}, +{21281, 24918}, {21605, 24637}, +{21926, 24353}, {22242, 24063}, +{22555, 23770}, {22865, 23473}, +{23171, 23171}, {23472, 22866}, +{23769, 22557}, {24063, 22244}, +{24352, 21927}, {24636, 21606}, +{24917, 21282}, {25194, 20954}, +{25465, 20622}, {25733, 20288}, +{25997, 19949}, {26255, 19607}, +{26509, 19261}, {26760, 18914}, +{27004, 18561}, {27246, 18205}, +{27481, 17846}, {27713, 17485}, +{27940, 17122}, {28162, 16755}, +{28378, 16385}, {28590, 16012}, +{28797, 15636}, {28999, 15258}, +{29197, 14878}, {29389, 14494}, +{29576, 14108}, {29757, 13720}, +{29934, 13329}, {30107, 12937}, +{30274, 12540}, {30435, 12142}, +{30592, 11744}, {30743, 11342}, +{30889, 10939}, {31030, 10534}, +{31164, 10127}, {31294, 9718}, +{31418, 9307}, {31537, 8895}, +{31652, 8482}, {31759, 8067}, +{31862, 7650}, {31960, 7233}, +{32051, 6815}, {32138, 6393}, +{32219, 5973}, {32294, 5549}, +{32364, 5127}, {32429, 4703}, +{32487, 4278}, {32541, 3852}, +{32588, 3426}, {32630, 2999}, +{32667, 2572}, {32698, 2144}, +{32724, 1716}, {32742, 1287}, +{32757, 860}, {32766, 430}, +}; +#ifndef FFT_BITREV480 +#define FFT_BITREV480 +static const opus_int16 fft_bitrev480[480] = { +0, 96, 192, 288, 384, 32, 128, 224, 320, 416, 64, 160, 256, 352, 448, +8, 104, 200, 296, 392, 40, 136, 232, 328, 424, 72, 168, 264, 360, 456, +16, 112, 208, 304, 400, 48, 144, 240, 336, 432, 80, 176, 272, 368, 464, +24, 120, 216, 312, 408, 56, 152, 248, 344, 440, 88, 184, 280, 376, 472, +4, 100, 196, 292, 388, 36, 132, 228, 324, 420, 68, 164, 260, 356, 452, +12, 108, 204, 300, 396, 44, 140, 236, 332, 428, 76, 172, 268, 364, 460, +20, 116, 212, 308, 404, 52, 148, 244, 340, 436, 84, 180, 276, 372, 468, +28, 124, 220, 316, 412, 60, 156, 252, 348, 444, 92, 188, 284, 380, 476, +1, 97, 193, 289, 385, 33, 129, 225, 321, 417, 65, 161, 257, 353, 449, +9, 105, 201, 297, 393, 41, 137, 233, 329, 425, 73, 169, 265, 361, 457, +17, 113, 209, 305, 401, 49, 145, 241, 337, 433, 81, 177, 273, 369, 465, +25, 121, 217, 313, 409, 57, 153, 249, 345, 441, 89, 185, 281, 377, 473, +5, 101, 197, 293, 389, 37, 133, 229, 325, 421, 69, 165, 261, 357, 453, +13, 109, 205, 301, 397, 45, 141, 237, 333, 429, 77, 173, 269, 365, 461, +21, 117, 213, 309, 405, 53, 149, 245, 341, 437, 85, 181, 277, 373, 469, +29, 125, 221, 317, 413, 61, 157, 253, 349, 445, 93, 189, 285, 381, 477, +2, 98, 194, 290, 386, 34, 130, 226, 322, 418, 66, 162, 258, 354, 450, +10, 106, 202, 298, 394, 42, 138, 234, 330, 426, 74, 170, 266, 362, 458, +18, 114, 210, 306, 402, 50, 146, 242, 338, 434, 82, 178, 274, 370, 466, +26, 122, 218, 314, 410, 58, 154, 250, 346, 442, 90, 186, 282, 378, 474, +6, 102, 198, 294, 390, 38, 134, 230, 326, 422, 70, 166, 262, 358, 454, +14, 110, 206, 302, 398, 46, 142, 238, 334, 430, 78, 174, 270, 366, 462, +22, 118, 214, 310, 406, 54, 150, 246, 342, 438, 86, 182, 278, 374, 470, +30, 126, 222, 318, 414, 62, 158, 254, 350, 446, 94, 190, 286, 382, 478, +3, 99, 195, 291, 387, 35, 131, 227, 323, 419, 67, 163, 259, 355, 451, +11, 107, 203, 299, 395, 43, 139, 235, 331, 427, 75, 171, 267, 363, 459, +19, 115, 211, 307, 403, 51, 147, 243, 339, 435, 83, 179, 275, 371, 467, +27, 123, 219, 315, 411, 59, 155, 251, 347, 443, 91, 187, 283, 379, 475, +7, 103, 199, 295, 391, 39, 135, 231, 327, 423, 71, 167, 263, 359, 455, +15, 111, 207, 303, 399, 47, 143, 239, 335, 431, 79, 175, 271, 367, 463, +23, 119, 215, 311, 407, 55, 151, 247, 343, 439, 87, 183, 279, 375, 471, +31, 127, 223, 319, 415, 63, 159, 255, 351, 447, 95, 191, 287, 383, 479, +}; +#endif + +#ifndef FFT_BITREV240 +#define FFT_BITREV240 +static const opus_int16 fft_bitrev240[240] = { +0, 48, 96, 144, 192, 16, 64, 112, 160, 208, 32, 80, 128, 176, 224, +4, 52, 100, 148, 196, 20, 68, 116, 164, 212, 36, 84, 132, 180, 228, +8, 56, 104, 152, 200, 24, 72, 120, 168, 216, 40, 88, 136, 184, 232, +12, 60, 108, 156, 204, 28, 76, 124, 172, 220, 44, 92, 140, 188, 236, +1, 49, 97, 145, 193, 17, 65, 113, 161, 209, 33, 81, 129, 177, 225, +5, 53, 101, 149, 197, 21, 69, 117, 165, 213, 37, 85, 133, 181, 229, +9, 57, 105, 153, 201, 25, 73, 121, 169, 217, 41, 89, 137, 185, 233, +13, 61, 109, 157, 205, 29, 77, 125, 173, 221, 45, 93, 141, 189, 237, +2, 50, 98, 146, 194, 18, 66, 114, 162, 210, 34, 82, 130, 178, 226, +6, 54, 102, 150, 198, 22, 70, 118, 166, 214, 38, 86, 134, 182, 230, +10, 58, 106, 154, 202, 26, 74, 122, 170, 218, 42, 90, 138, 186, 234, +14, 62, 110, 158, 206, 30, 78, 126, 174, 222, 46, 94, 142, 190, 238, +3, 51, 99, 147, 195, 19, 67, 115, 163, 211, 35, 83, 131, 179, 227, +7, 55, 103, 151, 199, 23, 71, 119, 167, 215, 39, 87, 135, 183, 231, +11, 59, 107, 155, 203, 27, 75, 123, 171, 219, 43, 91, 139, 187, 235, +15, 63, 111, 159, 207, 31, 79, 127, 175, 223, 47, 95, 143, 191, 239, +}; +#endif + +#ifndef FFT_BITREV120 +#define FFT_BITREV120 +static const opus_int16 fft_bitrev120[120] = { +0, 24, 48, 72, 96, 8, 32, 56, 80, 104, 16, 40, 64, 88, 112, +4, 28, 52, 76, 100, 12, 36, 60, 84, 108, 20, 44, 68, 92, 116, +1, 25, 49, 73, 97, 9, 33, 57, 81, 105, 17, 41, 65, 89, 113, +5, 29, 53, 77, 101, 13, 37, 61, 85, 109, 21, 45, 69, 93, 117, +2, 26, 50, 74, 98, 10, 34, 58, 82, 106, 18, 42, 66, 90, 114, +6, 30, 54, 78, 102, 14, 38, 62, 86, 110, 22, 46, 70, 94, 118, +3, 27, 51, 75, 99, 11, 35, 59, 83, 107, 19, 43, 67, 91, 115, +7, 31, 55, 79, 103, 15, 39, 63, 87, 111, 23, 47, 71, 95, 119, +}; +#endif + +#ifndef FFT_BITREV60 +#define FFT_BITREV60 +static const opus_int16 fft_bitrev60[60] = { +0, 12, 24, 36, 48, 4, 16, 28, 40, 52, 8, 20, 32, 44, 56, +1, 13, 25, 37, 49, 5, 17, 29, 41, 53, 9, 21, 33, 45, 57, +2, 14, 26, 38, 50, 6, 18, 30, 42, 54, 10, 22, 34, 46, 58, +3, 15, 27, 39, 51, 7, 19, 31, 43, 55, 11, 23, 35, 47, 59, +}; +#endif + +#ifndef FFT_STATE48000_960_0 +#define FFT_STATE48000_960_0 +static const kiss_fft_state fft_state48000_960_0 = { +480, /* nfft */ +17476, /* scale */ +8, /* scale_shift */ +-1, /* shift */ +{5, 96, 3, 32, 4, 8, 2, 4, 4, 1, 0, 0, 0, 0, 0, 0, }, /* factors */ +fft_bitrev480, /* bitrev */ +fft_twiddles48000_960, /* bitrev */ +#ifdef OVERRIDE_FFT +(arch_fft_state *)&cfg_arch_480, +#else +NULL, +#endif +}; +#endif + +#ifndef FFT_STATE48000_960_1 +#define FFT_STATE48000_960_1 +static const kiss_fft_state fft_state48000_960_1 = { +240, /* nfft */ +17476, /* scale */ +7, /* scale_shift */ +1, /* shift */ +{5, 48, 3, 16, 4, 4, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, }, /* factors */ +fft_bitrev240, /* bitrev */ +fft_twiddles48000_960, /* bitrev */ +#ifdef OVERRIDE_FFT +(arch_fft_state *)&cfg_arch_240, +#else +NULL, +#endif +}; +#endif + +#ifndef FFT_STATE48000_960_2 +#define FFT_STATE48000_960_2 +static const kiss_fft_state fft_state48000_960_2 = { +120, /* nfft */ +17476, /* scale */ +6, /* scale_shift */ +2, /* shift */ +{5, 24, 3, 8, 2, 4, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, }, /* factors */ +fft_bitrev120, /* bitrev */ +fft_twiddles48000_960, /* bitrev */ +#ifdef OVERRIDE_FFT +(arch_fft_state *)&cfg_arch_120, +#else +NULL, +#endif +}; +#endif + +#ifndef FFT_STATE48000_960_3 +#define FFT_STATE48000_960_3 +static const kiss_fft_state fft_state48000_960_3 = { +60, /* nfft */ +17476, /* scale */ +5, /* scale_shift */ +3, /* shift */ +{5, 12, 3, 4, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, /* factors */ +fft_bitrev60, /* bitrev */ +fft_twiddles48000_960, /* bitrev */ +#ifdef OVERRIDE_FFT +(arch_fft_state *)&cfg_arch_60, +#else +NULL, +#endif +}; +#endif + +#endif + +#ifndef MDCT_TWIDDLES960 +#define MDCT_TWIDDLES960 +static const opus_val16 mdct_twiddles960[1800] = { +32767, 32767, 32767, 32766, 32765, +32763, 32761, 32759, 32756, 32753, +32750, 32746, 32742, 32738, 32733, +32728, 32722, 32717, 32710, 32704, +32697, 32690, 32682, 32674, 32666, +32657, 32648, 32639, 32629, 32619, +32609, 32598, 32587, 32576, 32564, +32552, 32539, 32526, 32513, 32500, +32486, 32472, 32457, 32442, 32427, +32411, 32395, 32379, 32362, 32345, +32328, 32310, 32292, 32274, 32255, +32236, 32217, 32197, 32177, 32157, +32136, 32115, 32093, 32071, 32049, +32027, 32004, 31981, 31957, 31933, +31909, 31884, 31859, 31834, 31809, +31783, 31756, 31730, 31703, 31676, +31648, 31620, 31592, 31563, 31534, +31505, 31475, 31445, 31415, 31384, +31353, 31322, 31290, 31258, 31226, +31193, 31160, 31127, 31093, 31059, +31025, 30990, 30955, 30920, 30884, +30848, 30812, 30775, 30738, 30701, +30663, 30625, 30587, 30548, 30509, +30470, 30430, 30390, 30350, 30309, +30269, 30227, 30186, 30144, 30102, +30059, 30016, 29973, 29930, 29886, +29842, 29797, 29752, 29707, 29662, +29616, 29570, 29524, 29477, 29430, +29383, 29335, 29287, 29239, 29190, +29142, 29092, 29043, 28993, 28943, +28892, 28842, 28791, 28739, 28688, +28636, 28583, 28531, 28478, 28425, +28371, 28317, 28263, 28209, 28154, +28099, 28044, 27988, 27932, 27876, +27820, 27763, 27706, 27648, 27591, +27533, 27474, 27416, 27357, 27298, +27238, 27178, 27118, 27058, 26997, +26936, 26875, 26814, 26752, 26690, +26628, 26565, 26502, 26439, 26375, +26312, 26247, 26183, 26119, 26054, +25988, 25923, 25857, 25791, 25725, +25658, 25592, 25524, 25457, 25389, +25322, 25253, 25185, 25116, 25047, +24978, 24908, 24838, 24768, 24698, +24627, 24557, 24485, 24414, 24342, +24270, 24198, 24126, 24053, 23980, +23907, 23834, 23760, 23686, 23612, +23537, 23462, 23387, 23312, 23237, +23161, 23085, 23009, 22932, 22856, +22779, 22701, 22624, 22546, 22468, +22390, 22312, 22233, 22154, 22075, +21996, 21916, 21836, 21756, 21676, +21595, 21515, 21434, 21352, 21271, +21189, 21107, 21025, 20943, 20860, +20777, 20694, 20611, 20528, 20444, +20360, 20276, 20192, 20107, 20022, +19937, 19852, 19767, 19681, 19595, +19509, 19423, 19336, 19250, 19163, +19076, 18988, 18901, 18813, 18725, +18637, 18549, 18460, 18372, 18283, +18194, 18104, 18015, 17925, 17835, +17745, 17655, 17565, 17474, 17383, +17292, 17201, 17110, 17018, 16927, +16835, 16743, 16650, 16558, 16465, +16372, 16279, 16186, 16093, 15999, +15906, 15812, 15718, 15624, 15529, +15435, 15340, 15245, 15150, 15055, +14960, 14864, 14769, 14673, 14577, +14481, 14385, 14288, 14192, 14095, +13998, 13901, 13804, 13706, 13609, +13511, 13414, 13316, 13218, 13119, +13021, 12923, 12824, 12725, 12626, +12527, 12428, 12329, 12230, 12130, +12030, 11930, 11831, 11730, 11630, +11530, 11430, 11329, 11228, 11128, +11027, 10926, 10824, 10723, 10622, +10520, 10419, 10317, 10215, 10113, +10011, 9909, 9807, 9704, 9602, +9499, 9397, 9294, 9191, 9088, +8985, 8882, 8778, 8675, 8572, +8468, 8364, 8261, 8157, 8053, +7949, 7845, 7741, 7637, 7532, +7428, 7323, 7219, 7114, 7009, +6905, 6800, 6695, 6590, 6485, +6380, 6274, 6169, 6064, 5958, +5853, 5747, 5642, 5536, 5430, +5325, 5219, 5113, 5007, 4901, +4795, 4689, 4583, 4476, 4370, +4264, 4157, 4051, 3945, 3838, +3732, 3625, 3518, 3412, 3305, +3198, 3092, 2985, 2878, 2771, +2664, 2558, 2451, 2344, 2237, +2130, 2023, 1916, 1809, 1702, +1594, 1487, 1380, 1273, 1166, +1059, 952, 844, 737, 630, +523, 416, 308, 201, 94, +-13, -121, -228, -335, -442, +-550, -657, -764, -871, -978, +-1086, -1193, -1300, -1407, -1514, +-1621, -1728, -1835, -1942, -2049, +-2157, -2263, -2370, -2477, -2584, +-2691, -2798, -2905, -3012, -3118, +-3225, -3332, -3439, -3545, -3652, +-3758, -3865, -3971, -4078, -4184, +-4290, -4397, -4503, -4609, -4715, +-4821, -4927, -5033, -5139, -5245, +-5351, -5457, -5562, -5668, -5774, +-5879, -5985, -6090, -6195, -6301, +-6406, -6511, -6616, -6721, -6826, +-6931, -7036, -7140, -7245, -7349, +-7454, -7558, -7663, -7767, -7871, +-7975, -8079, -8183, -8287, -8390, +-8494, -8597, -8701, -8804, -8907, +-9011, -9114, -9217, -9319, -9422, +-9525, -9627, -9730, -9832, -9934, +-10037, -10139, -10241, -10342, -10444, +-10546, -10647, -10748, -10850, -10951, +-11052, -11153, -11253, -11354, -11455, +-11555, -11655, -11756, -11856, -11955, +-12055, -12155, -12254, -12354, -12453, +-12552, -12651, -12750, -12849, -12947, +-13046, -13144, -13242, -13340, -13438, +-13536, -13633, -13731, -13828, -13925, +-14022, -14119, -14216, -14312, -14409, +-14505, -14601, -14697, -14793, -14888, +-14984, -15079, -15174, -15269, -15364, +-15459, -15553, -15647, -15741, -15835, +-15929, -16023, -16116, -16210, -16303, +-16396, -16488, -16581, -16673, -16766, +-16858, -16949, -17041, -17133, -17224, +-17315, -17406, -17497, -17587, -17678, +-17768, -17858, -17948, -18037, -18127, +-18216, -18305, -18394, -18483, -18571, +-18659, -18747, -18835, -18923, -19010, +-19098, -19185, -19271, -19358, -19444, +-19531, -19617, -19702, -19788, -19873, +-19959, -20043, -20128, -20213, -20297, +-20381, -20465, -20549, -20632, -20715, +-20798, -20881, -20963, -21046, -21128, +-21210, -21291, -21373, -21454, -21535, +-21616, -21696, -21776, -21856, -21936, +-22016, -22095, -22174, -22253, -22331, +-22410, -22488, -22566, -22643, -22721, +-22798, -22875, -22951, -23028, -23104, +-23180, -23256, -23331, -23406, -23481, +-23556, -23630, -23704, -23778, -23852, +-23925, -23998, -24071, -24144, -24216, +-24288, -24360, -24432, -24503, -24574, +-24645, -24716, -24786, -24856, -24926, +-24995, -25064, -25133, -25202, -25270, +-25339, -25406, -25474, -25541, -25608, +-25675, -25742, -25808, -25874, -25939, +-26005, -26070, -26135, -26199, -26264, +-26327, -26391, -26455, -26518, -26581, +-26643, -26705, -26767, -26829, -26891, +-26952, -27013, -27073, -27133, -27193, +-27253, -27312, -27372, -27430, -27489, +-27547, -27605, -27663, -27720, -27777, +-27834, -27890, -27946, -28002, -28058, +-28113, -28168, -28223, -28277, -28331, +-28385, -28438, -28491, -28544, -28596, +-28649, -28701, -28752, -28803, -28854, +-28905, -28955, -29006, -29055, -29105, +-29154, -29203, -29251, -29299, -29347, +-29395, -29442, -29489, -29535, -29582, +-29628, -29673, -29719, -29764, -29808, +-29853, -29897, -29941, -29984, -30027, +-30070, -30112, -30154, -30196, -30238, +-30279, -30320, -30360, -30400, -30440, +-30480, -30519, -30558, -30596, -30635, +-30672, -30710, -30747, -30784, -30821, +-30857, -30893, -30929, -30964, -30999, +-31033, -31068, -31102, -31135, -31168, +-31201, -31234, -31266, -31298, -31330, +-31361, -31392, -31422, -31453, -31483, +-31512, -31541, -31570, -31599, -31627, +-31655, -31682, -31710, -31737, -31763, +-31789, -31815, -31841, -31866, -31891, +-31915, -31939, -31963, -31986, -32010, +-32032, -32055, -32077, -32099, -32120, +-32141, -32162, -32182, -32202, -32222, +-32241, -32260, -32279, -32297, -32315, +-32333, -32350, -32367, -32383, -32399, +-32415, -32431, -32446, -32461, -32475, +-32489, -32503, -32517, -32530, -32542, +-32555, -32567, -32579, -32590, -32601, +-32612, -32622, -32632, -32641, -32651, +-32659, -32668, -32676, -32684, -32692, +-32699, -32706, -32712, -32718, -32724, +-32729, -32734, -32739, -32743, -32747, +-32751, -32754, -32757, -32760, -32762, +-32764, -32765, -32767, -32767, -32767, +32767, 32767, 32765, 32761, 32756, +32750, 32742, 32732, 32722, 32710, +32696, 32681, 32665, 32647, 32628, +32608, 32586, 32562, 32538, 32512, +32484, 32455, 32425, 32393, 32360, +32326, 32290, 32253, 32214, 32174, +32133, 32090, 32046, 32001, 31954, +31906, 31856, 31805, 31753, 31700, +31645, 31588, 31530, 31471, 31411, +31349, 31286, 31222, 31156, 31089, +31020, 30951, 30880, 30807, 30733, +30658, 30582, 30504, 30425, 30345, +30263, 30181, 30096, 30011, 29924, +29836, 29747, 29656, 29564, 29471, +29377, 29281, 29184, 29086, 28987, +28886, 28784, 28681, 28577, 28471, +28365, 28257, 28147, 28037, 27925, +27812, 27698, 27583, 27467, 27349, +27231, 27111, 26990, 26868, 26744, +26620, 26494, 26367, 26239, 26110, +25980, 25849, 25717, 25583, 25449, +25313, 25176, 25038, 24900, 24760, +24619, 24477, 24333, 24189, 24044, +23898, 23751, 23602, 23453, 23303, +23152, 22999, 22846, 22692, 22537, +22380, 22223, 22065, 21906, 21746, +21585, 21423, 21261, 21097, 20933, +20767, 20601, 20434, 20265, 20096, +19927, 19756, 19584, 19412, 19239, +19065, 18890, 18714, 18538, 18361, +18183, 18004, 17824, 17644, 17463, +17281, 17098, 16915, 16731, 16546, +16361, 16175, 15988, 15800, 15612, +15423, 15234, 15043, 14852, 14661, +14469, 14276, 14083, 13889, 13694, +13499, 13303, 13107, 12910, 12713, +12515, 12317, 12118, 11918, 11718, +11517, 11316, 11115, 10913, 10710, +10508, 10304, 10100, 9896, 9691, +9486, 9281, 9075, 8869, 8662, +8455, 8248, 8040, 7832, 7623, +7415, 7206, 6996, 6787, 6577, +6366, 6156, 5945, 5734, 5523, +5311, 5100, 4888, 4675, 4463, +4251, 4038, 3825, 3612, 3399, +3185, 2972, 2758, 2544, 2330, +2116, 1902, 1688, 1474, 1260, +1045, 831, 617, 402, 188, +-27, -241, -456, -670, -885, +-1099, -1313, -1528, -1742, -1956, +-2170, -2384, -2598, -2811, -3025, +-3239, -3452, -3665, -3878, -4091, +-4304, -4516, -4728, -4941, -5153, +-5364, -5576, -5787, -5998, -6209, +-6419, -6629, -6839, -7049, -7258, +-7467, -7676, -7884, -8092, -8300, +-8507, -8714, -8920, -9127, -9332, +-9538, -9743, -9947, -10151, -10355, +-10558, -10761, -10963, -11165, -11367, +-11568, -11768, -11968, -12167, -12366, +-12565, -12762, -12960, -13156, -13352, +-13548, -13743, -13937, -14131, -14324, +-14517, -14709, -14900, -15091, -15281, +-15470, -15659, -15847, -16035, -16221, +-16407, -16593, -16777, -16961, -17144, +-17326, -17508, -17689, -17869, -18049, +-18227, -18405, -18582, -18758, -18934, +-19108, -19282, -19455, -19627, -19799, +-19969, -20139, -20308, -20475, -20642, +-20809, -20974, -21138, -21301, -21464, +-21626, -21786, -21946, -22105, -22263, +-22420, -22575, -22730, -22884, -23037, +-23189, -23340, -23490, -23640, -23788, +-23935, -24080, -24225, -24369, -24512, +-24654, -24795, -24934, -25073, -25211, +-25347, -25482, -25617, -25750, -25882, +-26013, -26143, -26272, -26399, -26526, +-26651, -26775, -26898, -27020, -27141, +-27260, -27379, -27496, -27612, -27727, +-27841, -27953, -28065, -28175, -28284, +-28391, -28498, -28603, -28707, -28810, +-28911, -29012, -29111, -29209, -29305, +-29401, -29495, -29587, -29679, -29769, +-29858, -29946, -30032, -30118, -30201, +-30284, -30365, -30445, -30524, -30601, +-30677, -30752, -30825, -30897, -30968, +-31038, -31106, -31172, -31238, -31302, +-31365, -31426, -31486, -31545, -31602, +-31658, -31713, -31766, -31818, -31869, +-31918, -31966, -32012, -32058, -32101, +-32144, -32185, -32224, -32262, -32299, +-32335, -32369, -32401, -32433, -32463, +-32491, -32518, -32544, -32568, -32591, +-32613, -32633, -32652, -32669, -32685, +-32700, -32713, -32724, -32735, -32744, +-32751, -32757, -32762, -32766, -32767, +32767, 32764, 32755, 32741, 32720, +32694, 32663, 32626, 32583, 32535, +32481, 32421, 32356, 32286, 32209, +32128, 32041, 31948, 31850, 31747, +31638, 31523, 31403, 31278, 31148, +31012, 30871, 30724, 30572, 30415, +30253, 30086, 29913, 29736, 29553, +29365, 29172, 28974, 28771, 28564, +28351, 28134, 27911, 27684, 27452, +27216, 26975, 26729, 26478, 26223, +25964, 25700, 25432, 25159, 24882, +24601, 24315, 24026, 23732, 23434, +23133, 22827, 22517, 22204, 21886, +21565, 21240, 20912, 20580, 20244, +19905, 19563, 19217, 18868, 18516, +18160, 17802, 17440, 17075, 16708, +16338, 15964, 15588, 15210, 14829, +14445, 14059, 13670, 13279, 12886, +12490, 12093, 11693, 11291, 10888, +10482, 10075, 9666, 9255, 8843, +8429, 8014, 7597, 7180, 6760, +6340, 5919, 5496, 5073, 4649, +4224, 3798, 3372, 2945, 2517, +2090, 1661, 1233, 804, 375, +-54, -483, -911, -1340, -1768, +-2197, -2624, -3052, -3479, -3905, +-4330, -4755, -5179, -5602, -6024, +-6445, -6865, -7284, -7702, -8118, +-8533, -8946, -9358, -9768, -10177, +-10584, -10989, -11392, -11793, -12192, +-12589, -12984, -13377, -13767, -14155, +-14541, -14924, -15305, -15683, -16058, +-16430, -16800, -17167, -17531, -17892, +-18249, -18604, -18956, -19304, -19649, +-19990, -20329, -20663, -20994, -21322, +-21646, -21966, -22282, -22595, -22904, +-23208, -23509, -23806, -24099, -24387, +-24672, -24952, -25228, -25499, -25766, +-26029, -26288, -26541, -26791, -27035, +-27275, -27511, -27741, -27967, -28188, +-28405, -28616, -28823, -29024, -29221, +-29412, -29599, -29780, -29957, -30128, +-30294, -30455, -30611, -30761, -30906, +-31046, -31181, -31310, -31434, -31552, +-31665, -31773, -31875, -31972, -32063, +-32149, -32229, -32304, -32373, -32437, +-32495, -32547, -32594, -32635, -32671, +-32701, -32726, -32745, -32758, -32766, +32767, 32754, 32717, 32658, 32577, +32473, 32348, 32200, 32029, 31837, +31624, 31388, 31131, 30853, 30553, +30232, 29891, 29530, 29148, 28746, +28324, 27883, 27423, 26944, 26447, +25931, 25398, 24847, 24279, 23695, +23095, 22478, 21846, 21199, 20538, +19863, 19174, 18472, 17757, 17030, +16291, 15541, 14781, 14010, 13230, +12441, 11643, 10837, 10024, 9204, +8377, 7545, 6708, 5866, 5020, +4171, 3319, 2464, 1608, 751, +-107, -965, -1822, -2678, -3532, +-4383, -5232, -6077, -6918, -7754, +-8585, -9409, -10228, -11039, -11843, +-12639, -13426, -14204, -14972, -15730, +-16477, -17213, -17937, -18648, -19347, +-20033, -20705, -21363, -22006, -22634, +-23246, -23843, -24423, -24986, -25533, +-26062, -26573, -27066, -27540, -27995, +-28431, -28848, -29245, -29622, -29979, +-30315, -30630, -30924, -31197, -31449, +-31679, -31887, -32074, -32239, -32381, +-32501, -32600, -32675, -32729, -32759, +}; +#endif + +static const CELTMode mode48000_960_120 = { +48000, /* Fs */ +120, /* overlap */ +21, /* nbEBands */ +21, /* effEBands */ +{27853, 0, 4096, 8192, }, /* preemph */ +eband5ms, /* eBands */ +3, /* maxLM */ +8, /* nbShortMdcts */ +120, /* shortMdctSize */ +11, /* nbAllocVectors */ +band_allocation, /* allocVectors */ +logN400, /* logN */ +window120, /* window */ +{1920, 3, {&fft_state48000_960_0, &fft_state48000_960_1, &fft_state48000_960_2, &fft_state48000_960_3, }, mdct_twiddles960}, /* mdct */ +{392, cache_index50, cache_bits50, cache_caps50}, /* cache */ +}; + +/* List of all the available modes */ +#define TOTAL_MODES 1 +static const CELTMode * const static_mode_list[TOTAL_MODES] = { +&mode48000_960_120, +}; diff --git a/port/third_party/opus/celt/static_modes_fixed_arm_ne10.h b/port/third_party/opus/celt/static_modes_fixed_arm_ne10.h new file mode 100644 index 00000000..76230921 --- /dev/null +++ b/port/third_party/opus/celt/static_modes_fixed_arm_ne10.h @@ -0,0 +1,388 @@ +/* The contents of this file was automatically generated by + * dump_mode_arm_ne10.c with arguments: 48000 960 + * It contains static definitions for some pre-defined modes. */ +#include + +#ifndef NE10_FFT_PARAMS48000_960 +#define NE10_FFT_PARAMS48000_960 +static const ne10_int32_t ne10_factors_480[64] = { +4, 40, 4, 30, 2, 15, 5, 3, 3, 1, 1, 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, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, }; +static const ne10_int32_t ne10_factors_240[64] = { +3, 20, 4, 15, 5, 3, 3, 1, 1, 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, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, }; +static const ne10_int32_t ne10_factors_120[64] = { +3, 10, 2, 15, 5, 3, 3, 1, 1, 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, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, }; +static const ne10_int32_t ne10_factors_60[64] = { +2, 5, 5, 3, 3, 1, 1, 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, 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, }; +static const ne10_fft_cpx_int32_t ne10_twiddles_480[480] = { +{0,0}, {2147483647,0}, {2147483647,0}, +{2147483647,0}, {1961823921,-873460313}, {1436946998,-1595891394}, +{2147483647,0}, {1436946998,-1595891394}, {-224473265,-2135719496}, +{2147483647,0}, {663608871,-2042378339}, {-1737350854,-1262259096}, +{2147483647,0}, {-224473265,-2135719496}, {-2100555935,446487152}, +{2147483647,0}, {2100555974,-446486968}, {1961823921,-873460313}, +{1737350743,-1262259248}, {1436946998,-1595891394}, {1073741769,-1859775424}, +{663608871,-2042378339}, {224473078,-2135719516}, {-224473265,-2135719496}, +{-663609049,-2042378281}, {-1073741932,-1859775330}, {-1436947137,-1595891268}, +{-1737350854,-1262259096}, {-1961823997,-873460141}, {-2100556013,-446486785}, +{2147483647,0}, {2144540595,-112390613}, {2135719506,-224473172}, +{2121044558,-335940465}, {2100555974,-446486968}, {2074309912,-555809682}, +{2042378310,-663608960}, {2004848691,-769589332}, {1961823921,-873460313}, +{1913421927,-974937199}, {1859775377,-1073741851}, {1801031311,-1169603450}, +{1737350743,-1262259248}, {1668908218,-1351455280}, {1595891331,-1436947067}, +{1518500216,-1518500282}, {1436946998,-1595891394}, {1351455207,-1668908277}, +{1262259172,-1737350799}, {1169603371,-1801031362}, {1073741769,-1859775424}, +{974937230,-1913421912}, {873460227,-1961823959}, {769589125,-2004848771}, +{663608871,-2042378339}, {555809715,-2074309903}, {446486876,-2100555994}, +{335940246,-2121044593}, {224473078,-2135719516}, {112390647,-2144540593}, +{2147483647,0}, {2135719506,-224473172}, {2100555974,-446486968}, +{2042378310,-663608960}, {1961823921,-873460313}, {1859775377,-1073741851}, +{1737350743,-1262259248}, {1595891331,-1436947067}, {1436946998,-1595891394}, +{1262259172,-1737350799}, {1073741769,-1859775424}, {873460227,-1961823959}, +{663608871,-2042378339}, {446486876,-2100555994}, {224473078,-2135719516}, +{-94,-2147483647}, {-224473265,-2135719496}, {-446487060,-2100555955}, +{-663609049,-2042378281}, {-873460398,-1961823883}, {-1073741932,-1859775330}, +{-1262259116,-1737350839}, {-1436947137,-1595891268}, {-1595891628,-1436946738}, +{-1737350854,-1262259096}, {-1859775343,-1073741910}, {-1961823997,-873460141}, +{-2042378447,-663608538}, {-2100556013,-446486785}, {-2135719499,-224473240}, +{2147483647,0}, {2121044558,-335940465}, {2042378310,-663608960}, +{1913421927,-974937199}, {1737350743,-1262259248}, {1518500216,-1518500282}, +{1262259172,-1737350799}, {974937230,-1913421912}, {663608871,-2042378339}, +{335940246,-2121044593}, {-94,-2147483647}, {-335940431,-2121044564}, +{-663609049,-2042378281}, {-974937397,-1913421827}, {-1262259116,-1737350839}, +{-1518500258,-1518500240}, {-1737350854,-1262259096}, {-1913422071,-974936918}, +{-2042378447,-663608538}, {-2121044568,-335940406}, {-2147483647,188}, +{-2121044509,335940777}, {-2042378331,663608895}, {-1913421900,974937252}, +{-1737350633,1262259400}, {-1518499993,1518500506}, {-1262258813,1737351059}, +{-974936606,1913422229}, {-663609179,2042378239}, {-335940566,2121044542}, +{2147483647,0}, {2147299667,-28109693}, {2146747758,-56214570}, +{2145828015,-84309815}, {2144540595,-112390613}, {2142885719,-140452154}, +{2140863671,-168489630}, {2138474797,-196498235}, {2135719506,-224473172}, +{2132598271,-252409646}, {2129111626,-280302871}, {2125260168,-308148068}, +{2121044558,-335940465}, {2116465518,-363675300}, {2111523833,-391347822}, +{2106220349,-418953288}, {2100555974,-446486968}, {2094531681,-473944146}, +{2088148500,-501320115}, {2081407525,-528610186}, {2074309912,-555809682}, +{2066856885,-582913912}, {2059049696,-609918325}, {2050889698,-636818231}, +{2042378310,-663608960}, {2033516972,-690285983}, {2024307180,-716844791}, +{2014750533,-743280770}, {2004848691,-769589332}, {1994603329,-795766029}, +{1984016179,-821806435}, {1973089077,-847706028}, {1961823921,-873460313}, +{1950222618,-899064934}, {1938287127,-924515564}, {1926019520,-949807783}, +{1913421927,-974937199}, {1900496481,-999899565}, {1887245364,-1024690661}, +{1873670877,-1049306180}, {1859775377,-1073741851}, {1845561215,-1097993541}, +{1831030826,-1122057097}, {1816186632,-1145928502}, {1801031311,-1169603450}, +{1785567394,-1193077993}, {1769797456,-1216348214}, {1753724345,-1239409914}, +{1737350743,-1262259248}, {1720679456,-1284892300}, {1703713340,-1307305194}, +{1686455222,-1329494189}, {1668908218,-1351455280}, {1651075255,-1373184807}, +{1632959307,-1394679144}, {1614563642,-1415934412}, {1595891331,-1436947067}, +{1576945572,-1457713510}, {1557729613,-1478230181}, {1538246655,-1498493658}, +{1518500216,-1518500282}, {1498493590,-1538246721}, {1478230113,-1557729677}, +{1457713441,-1576945636}, {1436946998,-1595891394}, {1415934341,-1614563704}, +{1394679073,-1632959368}, {1373184735,-1651075315}, {1351455207,-1668908277}, +{1329494115,-1686455280}, {1307305120,-1703713397}, {1284892225,-1720679512}, +{1262259172,-1737350799}, {1239409837,-1753724400}, {1216348136,-1769797510}, +{1193077915,-1785567446}, {1169603371,-1801031362}, {1145928423,-1816186682}, +{1122057017,-1831030875}, {1097993571,-1845561197}, {1073741769,-1859775424}, +{1049305987,-1873670985}, {1024690635,-1887245378}, {999899482,-1900496524}, +{974937230,-1913421912}, {949807699,-1926019561}, {924515422,-1938287195}, +{899064965,-1950222603}, {873460227,-1961823959}, {847705824,-1973089164}, +{821806407,-1984016190}, {795765941,-1994603364}, {769589125,-2004848771}, +{743280682,-2014750566}, {716844642,-2024307233}, {690286016,-2033516961}, +{663608871,-2042378339}, {636818019,-2050889764}, {609918296,-2059049705}, +{582913822,-2066856911}, {555809715,-2074309903}, {528610126,-2081407540}, +{501319962,-2088148536}, {473944148,-2094531680}, {446486876,-2100555994}, +{418953102,-2106220386}, {391347792,-2111523838}, {363675176,-2116465540}, +{335940246,-2121044593}, {308148006,-2125260177}, {280302715,-2129111646}, +{252409648,-2132598271}, {224473078,-2135719516}, {196498046,-2138474814}, +{168489600,-2140863674}, {140452029,-2142885728}, {112390647,-2144540593}, +{84309753,-2145828017}, {56214412,-2146747762}, {28109695,-2147299667}, +{2147483647,0}, {2146747758,-56214570}, {2144540595,-112390613}, +{2140863671,-168489630}, {2135719506,-224473172}, {2129111626,-280302871}, +{2121044558,-335940465}, {2111523833,-391347822}, {2100555974,-446486968}, +{2088148500,-501320115}, {2074309912,-555809682}, {2059049696,-609918325}, +{2042378310,-663608960}, {2024307180,-716844791}, {2004848691,-769589332}, +{1984016179,-821806435}, {1961823921,-873460313}, {1938287127,-924515564}, +{1913421927,-974937199}, {1887245364,-1024690661}, {1859775377,-1073741851}, +{1831030826,-1122057097}, {1801031311,-1169603450}, {1769797456,-1216348214}, +{1737350743,-1262259248}, {1703713340,-1307305194}, {1668908218,-1351455280}, +{1632959307,-1394679144}, {1595891331,-1436947067}, {1557729613,-1478230181}, +{1518500216,-1518500282}, {1478230113,-1557729677}, {1436946998,-1595891394}, +{1394679073,-1632959368}, {1351455207,-1668908277}, {1307305120,-1703713397}, +{1262259172,-1737350799}, {1216348136,-1769797510}, {1169603371,-1801031362}, +{1122057017,-1831030875}, {1073741769,-1859775424}, {1024690635,-1887245378}, +{974937230,-1913421912}, {924515422,-1938287195}, {873460227,-1961823959}, +{821806407,-1984016190}, {769589125,-2004848771}, {716844642,-2024307233}, +{663608871,-2042378339}, {609918296,-2059049705}, {555809715,-2074309903}, +{501319962,-2088148536}, {446486876,-2100555994}, {391347792,-2111523838}, +{335940246,-2121044593}, {280302715,-2129111646}, {224473078,-2135719516}, +{168489600,-2140863674}, {112390647,-2144540593}, {56214412,-2146747762}, +{-94,-2147483647}, {-56214600,-2146747757}, {-112390835,-2144540584}, +{-168489787,-2140863659}, {-224473265,-2135719496}, {-280302901,-2129111622}, +{-335940431,-2121044564}, {-391347977,-2111523804}, {-446487060,-2100555955}, +{-501320144,-2088148493}, {-555809896,-2074309855}, {-609918476,-2059049651}, +{-663609049,-2042378281}, {-716844819,-2024307170}, {-769589300,-2004848703}, +{-821806581,-1984016118}, {-873460398,-1961823883}, {-924515591,-1938287114}, +{-974937397,-1913421827}, {-1024690575,-1887245411}, {-1073741932,-1859775330}, +{-1122057395,-1831030643}, {-1169603421,-1801031330}, {-1216348291,-1769797403}, +{-1262259116,-1737350839}, {-1307305268,-1703713283}, {-1351455453,-1668908078}, +{-1394679021,-1632959413}, {-1436947137,-1595891268}, {-1478230435,-1557729372}, +{-1518500258,-1518500240}, {-1557729742,-1478230045}, {-1595891628,-1436946738}, +{-1632959429,-1394679001}, {-1668908417,-1351455035}, {-1703713298,-1307305248}, +{-1737350854,-1262259096}, {-1769797708,-1216347848}, {-1801031344,-1169603400}, +{-1831030924,-1122056937}, {-1859775343,-1073741910}, {-1887245423,-1024690552}, +{-1913422071,-974936918}, {-1938287125,-924515568}, {-1961823997,-873460141}, +{-1984016324,-821806084}, {-2004848713,-769589276}, {-2024307264,-716844553}, +{-2042378447,-663608538}, {-2059049731,-609918206}, {-2074309994,-555809377}, +{-2088148499,-501320119}, {-2100556013,-446486785}, {-2111523902,-391347448}, +{-2121044568,-335940406}, {-2129111659,-280302621}, {-2135719499,-224473240}, +{-2140863681,-168489506}, {-2144540612,-112390298}, {-2146747758,-56214574}, +{2147483647,0}, {2145828015,-84309815}, {2140863671,-168489630}, +{2132598271,-252409646}, {2121044558,-335940465}, {2106220349,-418953288}, +{2088148500,-501320115}, {2066856885,-582913912}, {2042378310,-663608960}, +{2014750533,-743280770}, {1984016179,-821806435}, {1950222618,-899064934}, +{1913421927,-974937199}, {1873670877,-1049306180}, {1831030826,-1122057097}, +{1785567394,-1193077993}, {1737350743,-1262259248}, {1686455222,-1329494189}, +{1632959307,-1394679144}, {1576945572,-1457713510}, {1518500216,-1518500282}, +{1457713441,-1576945636}, {1394679073,-1632959368}, {1329494115,-1686455280}, +{1262259172,-1737350799}, {1193077915,-1785567446}, {1122057017,-1831030875}, +{1049305987,-1873670985}, {974937230,-1913421912}, {899064965,-1950222603}, +{821806407,-1984016190}, {743280682,-2014750566}, {663608871,-2042378339}, +{582913822,-2066856911}, {501319962,-2088148536}, {418953102,-2106220386}, +{335940246,-2121044593}, {252409648,-2132598271}, {168489600,-2140863674}, +{84309753,-2145828017}, {-94,-2147483647}, {-84309940,-2145828010}, +{-168489787,-2140863659}, {-252409834,-2132598249}, {-335940431,-2121044564}, +{-418953286,-2106220349}, {-501320144,-2088148493}, {-582914003,-2066856860}, +{-663609049,-2042378281}, {-743280858,-2014750501}, {-821806581,-1984016118}, +{-899065136,-1950222525}, {-974937397,-1913421827}, {-1049306374,-1873670768}, +{-1122057395,-1831030643}, {-1193078284,-1785567199}, {-1262259116,-1737350839}, +{-1329494061,-1686455323}, {-1394679021,-1632959413}, {-1457713485,-1576945595}, +{-1518500258,-1518500240}, {-1576945613,-1457713466}, {-1632959429,-1394679001}, +{-1686455338,-1329494041}, {-1737350854,-1262259096}, {-1785567498,-1193077837}, +{-1831030924,-1122056937}, {-1873671031,-1049305905}, {-1913422071,-974936918}, +{-1950222750,-899064648}, {-1984016324,-821806084}, {-2014750687,-743280354}, +{-2042378447,-663608538}, {-2066856867,-582913978}, {-2088148499,-501320119}, +{-2106220354,-418953261}, {-2121044568,-335940406}, {-2132598282,-252409555}, +{-2140863681,-168489506}, {-2145828021,-84309659}, {-2147483647,188}, +{-2145828006,84310034}, {-2140863651,168489881}, {-2132598237,252409928}, +{-2121044509,335940777}, {-2106220281,418953629}, {-2088148411,501320484}, +{-2066856765,582914339}, {-2042378331,663608895}, {-2014750557,743280706}, +{-1984016181,821806431}, {-1950222593,899064989}, {-1913421900,974937252}, +{-1873670848,1049306232}, {-1831030728,1122057257}, {-1785567289,1193078149}, +{-1737350633,1262259400}, {-1686455106,1329494336}, {-1632959185,1394679287}, +{-1576945358,1457713742}, {-1518499993,1518500506}, {-1457713209,1576945850}, +{-1394678735,1632959656}, {-1329493766,1686455555}, {-1262258813,1737351059}, +{-1193077546,1785567692}, {-1122056638,1831031107}, {-1049305599,1873671202}, +{-974936606,1913422229}, {-899064330,1950222896}, {-821805761,1984016458}, +{-743280025,2014750808}, {-663609179,2042378239}, {-582914134,2066856823}, +{-501320277,2088148461}, {-418953420,2106220322}, {-335940566,2121044542}, +{-252409716,2132598263}, {-168489668,2140863668}, {-84309821,2145828015}, +}; +static const ne10_fft_cpx_int32_t ne10_twiddles_240[240] = { +{0,0}, {2147483647,0}, {2147483647,0}, +{2147483647,0}, {1961823921,-873460313}, {1436946998,-1595891394}, +{2147483647,0}, {1436946998,-1595891394}, {-224473265,-2135719496}, +{2147483647,0}, {663608871,-2042378339}, {-1737350854,-1262259096}, +{2147483647,0}, {-224473265,-2135719496}, {-2100555935,446487152}, +{2147483647,0}, {2135719506,-224473172}, {2100555974,-446486968}, +{2042378310,-663608960}, {1961823921,-873460313}, {1859775377,-1073741851}, +{1737350743,-1262259248}, {1595891331,-1436947067}, {1436946998,-1595891394}, +{1262259172,-1737350799}, {1073741769,-1859775424}, {873460227,-1961823959}, +{663608871,-2042378339}, {446486876,-2100555994}, {224473078,-2135719516}, +{2147483647,0}, {2100555974,-446486968}, {1961823921,-873460313}, +{1737350743,-1262259248}, {1436946998,-1595891394}, {1073741769,-1859775424}, +{663608871,-2042378339}, {224473078,-2135719516}, {-224473265,-2135719496}, +{-663609049,-2042378281}, {-1073741932,-1859775330}, {-1436947137,-1595891268}, +{-1737350854,-1262259096}, {-1961823997,-873460141}, {-2100556013,-446486785}, +{2147483647,0}, {2042378310,-663608960}, {1737350743,-1262259248}, +{1262259172,-1737350799}, {663608871,-2042378339}, {-94,-2147483647}, +{-663609049,-2042378281}, {-1262259116,-1737350839}, {-1737350854,-1262259096}, +{-2042378447,-663608538}, {-2147483647,188}, {-2042378331,663608895}, +{-1737350633,1262259400}, {-1262258813,1737351059}, {-663609179,2042378239}, +{2147483647,0}, {2146747758,-56214570}, {2144540595,-112390613}, +{2140863671,-168489630}, {2135719506,-224473172}, {2129111626,-280302871}, +{2121044558,-335940465}, {2111523833,-391347822}, {2100555974,-446486968}, +{2088148500,-501320115}, {2074309912,-555809682}, {2059049696,-609918325}, +{2042378310,-663608960}, {2024307180,-716844791}, {2004848691,-769589332}, +{1984016179,-821806435}, {1961823921,-873460313}, {1938287127,-924515564}, +{1913421927,-974937199}, {1887245364,-1024690661}, {1859775377,-1073741851}, +{1831030826,-1122057097}, {1801031311,-1169603450}, {1769797456,-1216348214}, +{1737350743,-1262259248}, {1703713340,-1307305194}, {1668908218,-1351455280}, +{1632959307,-1394679144}, {1595891331,-1436947067}, {1557729613,-1478230181}, +{1518500216,-1518500282}, {1478230113,-1557729677}, {1436946998,-1595891394}, +{1394679073,-1632959368}, {1351455207,-1668908277}, {1307305120,-1703713397}, +{1262259172,-1737350799}, {1216348136,-1769797510}, {1169603371,-1801031362}, +{1122057017,-1831030875}, {1073741769,-1859775424}, {1024690635,-1887245378}, +{974937230,-1913421912}, {924515422,-1938287195}, {873460227,-1961823959}, +{821806407,-1984016190}, {769589125,-2004848771}, {716844642,-2024307233}, +{663608871,-2042378339}, {609918296,-2059049705}, {555809715,-2074309903}, +{501319962,-2088148536}, {446486876,-2100555994}, {391347792,-2111523838}, +{335940246,-2121044593}, {280302715,-2129111646}, {224473078,-2135719516}, +{168489600,-2140863674}, {112390647,-2144540593}, {56214412,-2146747762}, +{2147483647,0}, {2144540595,-112390613}, {2135719506,-224473172}, +{2121044558,-335940465}, {2100555974,-446486968}, {2074309912,-555809682}, +{2042378310,-663608960}, {2004848691,-769589332}, {1961823921,-873460313}, +{1913421927,-974937199}, {1859775377,-1073741851}, {1801031311,-1169603450}, +{1737350743,-1262259248}, {1668908218,-1351455280}, {1595891331,-1436947067}, +{1518500216,-1518500282}, {1436946998,-1595891394}, {1351455207,-1668908277}, +{1262259172,-1737350799}, {1169603371,-1801031362}, {1073741769,-1859775424}, +{974937230,-1913421912}, {873460227,-1961823959}, {769589125,-2004848771}, +{663608871,-2042378339}, {555809715,-2074309903}, {446486876,-2100555994}, +{335940246,-2121044593}, {224473078,-2135719516}, {112390647,-2144540593}, +{-94,-2147483647}, {-112390835,-2144540584}, {-224473265,-2135719496}, +{-335940431,-2121044564}, {-446487060,-2100555955}, {-555809896,-2074309855}, +{-663609049,-2042378281}, {-769589300,-2004848703}, {-873460398,-1961823883}, +{-974937397,-1913421827}, {-1073741932,-1859775330}, {-1169603421,-1801031330}, +{-1262259116,-1737350839}, {-1351455453,-1668908078}, {-1436947137,-1595891268}, +{-1518500258,-1518500240}, {-1595891628,-1436946738}, {-1668908417,-1351455035}, +{-1737350854,-1262259096}, {-1801031344,-1169603400}, {-1859775343,-1073741910}, +{-1913422071,-974936918}, {-1961823997,-873460141}, {-2004848713,-769589276}, +{-2042378447,-663608538}, {-2074309994,-555809377}, {-2100556013,-446486785}, +{-2121044568,-335940406}, {-2135719499,-224473240}, {-2144540612,-112390298}, +{2147483647,0}, {2140863671,-168489630}, {2121044558,-335940465}, +{2088148500,-501320115}, {2042378310,-663608960}, {1984016179,-821806435}, +{1913421927,-974937199}, {1831030826,-1122057097}, {1737350743,-1262259248}, +{1632959307,-1394679144}, {1518500216,-1518500282}, {1394679073,-1632959368}, +{1262259172,-1737350799}, {1122057017,-1831030875}, {974937230,-1913421912}, +{821806407,-1984016190}, {663608871,-2042378339}, {501319962,-2088148536}, +{335940246,-2121044593}, {168489600,-2140863674}, {-94,-2147483647}, +{-168489787,-2140863659}, {-335940431,-2121044564}, {-501320144,-2088148493}, +{-663609049,-2042378281}, {-821806581,-1984016118}, {-974937397,-1913421827}, +{-1122057395,-1831030643}, {-1262259116,-1737350839}, {-1394679021,-1632959413}, +{-1518500258,-1518500240}, {-1632959429,-1394679001}, {-1737350854,-1262259096}, +{-1831030924,-1122056937}, {-1913422071,-974936918}, {-1984016324,-821806084}, +{-2042378447,-663608538}, {-2088148499,-501320119}, {-2121044568,-335940406}, +{-2140863681,-168489506}, {-2147483647,188}, {-2140863651,168489881}, +{-2121044509,335940777}, {-2088148411,501320484}, {-2042378331,663608895}, +{-1984016181,821806431}, {-1913421900,974937252}, {-1831030728,1122057257}, +{-1737350633,1262259400}, {-1632959185,1394679287}, {-1518499993,1518500506}, +{-1394678735,1632959656}, {-1262258813,1737351059}, {-1122056638,1831031107}, +{-974936606,1913422229}, {-821805761,1984016458}, {-663609179,2042378239}, +{-501320277,2088148461}, {-335940566,2121044542}, {-168489668,2140863668}, +}; +static const ne10_fft_cpx_int32_t ne10_twiddles_120[120] = { +{0,0}, {2147483647,0}, {2147483647,0}, +{2147483647,0}, {1961823921,-873460313}, {1436946998,-1595891394}, +{2147483647,0}, {1436946998,-1595891394}, {-224473265,-2135719496}, +{2147483647,0}, {663608871,-2042378339}, {-1737350854,-1262259096}, +{2147483647,0}, {-224473265,-2135719496}, {-2100555935,446487152}, +{2147483647,0}, {2100555974,-446486968}, {1961823921,-873460313}, +{1737350743,-1262259248}, {1436946998,-1595891394}, {1073741769,-1859775424}, +{663608871,-2042378339}, {224473078,-2135719516}, {-224473265,-2135719496}, +{-663609049,-2042378281}, {-1073741932,-1859775330}, {-1436947137,-1595891268}, +{-1737350854,-1262259096}, {-1961823997,-873460141}, {-2100556013,-446486785}, +{2147483647,0}, {2144540595,-112390613}, {2135719506,-224473172}, +{2121044558,-335940465}, {2100555974,-446486968}, {2074309912,-555809682}, +{2042378310,-663608960}, {2004848691,-769589332}, {1961823921,-873460313}, +{1913421927,-974937199}, {1859775377,-1073741851}, {1801031311,-1169603450}, +{1737350743,-1262259248}, {1668908218,-1351455280}, {1595891331,-1436947067}, +{1518500216,-1518500282}, {1436946998,-1595891394}, {1351455207,-1668908277}, +{1262259172,-1737350799}, {1169603371,-1801031362}, {1073741769,-1859775424}, +{974937230,-1913421912}, {873460227,-1961823959}, {769589125,-2004848771}, +{663608871,-2042378339}, {555809715,-2074309903}, {446486876,-2100555994}, +{335940246,-2121044593}, {224473078,-2135719516}, {112390647,-2144540593}, +{2147483647,0}, {2135719506,-224473172}, {2100555974,-446486968}, +{2042378310,-663608960}, {1961823921,-873460313}, {1859775377,-1073741851}, +{1737350743,-1262259248}, {1595891331,-1436947067}, {1436946998,-1595891394}, +{1262259172,-1737350799}, {1073741769,-1859775424}, {873460227,-1961823959}, +{663608871,-2042378339}, {446486876,-2100555994}, {224473078,-2135719516}, +{-94,-2147483647}, {-224473265,-2135719496}, {-446487060,-2100555955}, +{-663609049,-2042378281}, {-873460398,-1961823883}, {-1073741932,-1859775330}, +{-1262259116,-1737350839}, {-1436947137,-1595891268}, {-1595891628,-1436946738}, +{-1737350854,-1262259096}, {-1859775343,-1073741910}, {-1961823997,-873460141}, +{-2042378447,-663608538}, {-2100556013,-446486785}, {-2135719499,-224473240}, +{2147483647,0}, {2121044558,-335940465}, {2042378310,-663608960}, +{1913421927,-974937199}, {1737350743,-1262259248}, {1518500216,-1518500282}, +{1262259172,-1737350799}, {974937230,-1913421912}, {663608871,-2042378339}, +{335940246,-2121044593}, {-94,-2147483647}, {-335940431,-2121044564}, +{-663609049,-2042378281}, {-974937397,-1913421827}, {-1262259116,-1737350839}, +{-1518500258,-1518500240}, {-1737350854,-1262259096}, {-1913422071,-974936918}, +{-2042378447,-663608538}, {-2121044568,-335940406}, {-2147483647,188}, +{-2121044509,335940777}, {-2042378331,663608895}, {-1913421900,974937252}, +{-1737350633,1262259400}, {-1518499993,1518500506}, {-1262258813,1737351059}, +{-974936606,1913422229}, {-663609179,2042378239}, {-335940566,2121044542}, +}; +static const ne10_fft_cpx_int32_t ne10_twiddles_60[60] = { +{0,0}, {2147483647,0}, {2147483647,0}, +{2147483647,0}, {1961823921,-873460313}, {1436946998,-1595891394}, +{2147483647,0}, {1436946998,-1595891394}, {-224473265,-2135719496}, +{2147483647,0}, {663608871,-2042378339}, {-1737350854,-1262259096}, +{2147483647,0}, {-224473265,-2135719496}, {-2100555935,446487152}, +{2147483647,0}, {2135719506,-224473172}, {2100555974,-446486968}, +{2042378310,-663608960}, {1961823921,-873460313}, {1859775377,-1073741851}, +{1737350743,-1262259248}, {1595891331,-1436947067}, {1436946998,-1595891394}, +{1262259172,-1737350799}, {1073741769,-1859775424}, {873460227,-1961823959}, +{663608871,-2042378339}, {446486876,-2100555994}, {224473078,-2135719516}, +{2147483647,0}, {2100555974,-446486968}, {1961823921,-873460313}, +{1737350743,-1262259248}, {1436946998,-1595891394}, {1073741769,-1859775424}, +{663608871,-2042378339}, {224473078,-2135719516}, {-224473265,-2135719496}, +{-663609049,-2042378281}, {-1073741932,-1859775330}, {-1436947137,-1595891268}, +{-1737350854,-1262259096}, {-1961823997,-873460141}, {-2100556013,-446486785}, +{2147483647,0}, {2042378310,-663608960}, {1737350743,-1262259248}, +{1262259172,-1737350799}, {663608871,-2042378339}, {-94,-2147483647}, +{-663609049,-2042378281}, {-1262259116,-1737350839}, {-1737350854,-1262259096}, +{-2042378447,-663608538}, {-2147483647,188}, {-2042378331,663608895}, +{-1737350633,1262259400}, {-1262258813,1737351059}, {-663609179,2042378239}, +}; +static const ne10_fft_state_int32_t ne10_fft_state_int32_t_480 = { +120, +(ne10_int32_t *)ne10_factors_480, +(ne10_fft_cpx_int32_t *)ne10_twiddles_480, +NULL, +(ne10_fft_cpx_int32_t *)&ne10_twiddles_480[120], +}; +static const arch_fft_state cfg_arch_480 = { +1, +(void *)&ne10_fft_state_int32_t_480, +}; + +static const ne10_fft_state_int32_t ne10_fft_state_int32_t_240 = { +60, +(ne10_int32_t *)ne10_factors_240, +(ne10_fft_cpx_int32_t *)ne10_twiddles_240, +NULL, +(ne10_fft_cpx_int32_t *)&ne10_twiddles_240[60], +}; +static const arch_fft_state cfg_arch_240 = { +1, +(void *)&ne10_fft_state_int32_t_240, +}; + +static const ne10_fft_state_int32_t ne10_fft_state_int32_t_120 = { +30, +(ne10_int32_t *)ne10_factors_120, +(ne10_fft_cpx_int32_t *)ne10_twiddles_120, +NULL, +(ne10_fft_cpx_int32_t *)&ne10_twiddles_120[30], +}; +static const arch_fft_state cfg_arch_120 = { +1, +(void *)&ne10_fft_state_int32_t_120, +}; + +static const ne10_fft_state_int32_t ne10_fft_state_int32_t_60 = { +15, +(ne10_int32_t *)ne10_factors_60, +(ne10_fft_cpx_int32_t *)ne10_twiddles_60, +NULL, +(ne10_fft_cpx_int32_t *)&ne10_twiddles_60[15], +}; +static const arch_fft_state cfg_arch_60 = { +1, +(void *)&ne10_fft_state_int32_t_60, +}; + +#endif /* end NE10_FFT_PARAMS48000_960 */ diff --git a/port/third_party/opus/celt/static_modes_float.h b/port/third_party/opus/celt/static_modes_float.h new file mode 100644 index 00000000..e102a383 --- /dev/null +++ b/port/third_party/opus/celt/static_modes_float.h @@ -0,0 +1,888 @@ +/* The contents of this file was automatically generated by dump_modes.c + with arguments: 48000 960 + It contains static definitions for some pre-defined modes. */ +#include "modes.h" +#include "rate.h" + +#ifdef HAVE_ARM_NE10 +#define OVERRIDE_FFT 1 +#include "static_modes_float_arm_ne10.h" +#endif + +#ifndef DEF_WINDOW120 +#define DEF_WINDOW120 +static const opus_val16 window120[120] = { +6.7286966e-05f, 0.00060551348f, 0.0016815970f, 0.0032947962f, 0.0054439943f, +0.0081276923f, 0.011344001f, 0.015090633f, 0.019364886f, 0.024163635f, +0.029483315f, 0.035319905f, 0.041668911f, 0.048525347f, 0.055883718f, +0.063737999f, 0.072081616f, 0.080907428f, 0.090207705f, 0.099974111f, +0.11019769f, 0.12086883f, 0.13197729f, 0.14351214f, 0.15546177f, +0.16781389f, 0.18055550f, 0.19367290f, 0.20715171f, 0.22097682f, +0.23513243f, 0.24960208f, 0.26436860f, 0.27941419f, 0.29472040f, +0.31026818f, 0.32603788f, 0.34200931f, 0.35816177f, 0.37447407f, +0.39092462f, 0.40749142f, 0.42415215f, 0.44088423f, 0.45766484f, +0.47447104f, 0.49127978f, 0.50806798f, 0.52481261f, 0.54149077f, +0.55807973f, 0.57455701f, 0.59090049f, 0.60708841f, 0.62309951f, +0.63891306f, 0.65450896f, 0.66986776f, 0.68497077f, 0.69980010f, +0.71433873f, 0.72857055f, 0.74248043f, 0.75605424f, 0.76927895f, +0.78214257f, 0.79463430f, 0.80674445f, 0.81846456f, 0.82978733f, +0.84070669f, 0.85121779f, 0.86131698f, 0.87100183f, 0.88027111f, +0.88912479f, 0.89756398f, 0.90559094f, 0.91320904f, 0.92042270f, +0.92723738f, 0.93365955f, 0.93969656f, 0.94535671f, 0.95064907f, +0.95558353f, 0.96017067f, 0.96442171f, 0.96834849f, 0.97196334f, +0.97527906f, 0.97830883f, 0.98106616f, 0.98356480f, 0.98581869f, +0.98784191f, 0.98964856f, 0.99125274f, 0.99266849f, 0.99390969f, +0.99499004f, 0.99592297f, 0.99672162f, 0.99739874f, 0.99796667f, +0.99843728f, 0.99882195f, 0.99913147f, 0.99937606f, 0.99956527f, +0.99970802f, 0.99981248f, 0.99988613f, 0.99993565f, 0.99996697f, +0.99998518f, 0.99999457f, 0.99999859f, 0.99999982f, 1.0000000f, +}; +#endif + +#ifndef DEF_LOGN400 +#define DEF_LOGN400 +static const opus_int16 logN400[21] = { +0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 16, 16, 16, 21, 21, 24, 29, 34, 36, }; +#endif + +#ifndef DEF_PULSE_CACHE50 +#define DEF_PULSE_CACHE50 +static const opus_int16 cache_index50[105] = { +-1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 41, 41, 41, +82, 82, 123, 164, 200, 222, 0, 0, 0, 0, 0, 0, 0, 0, 41, +41, 41, 41, 123, 123, 123, 164, 164, 240, 266, 283, 295, 41, 41, 41, +41, 41, 41, 41, 41, 123, 123, 123, 123, 240, 240, 240, 266, 266, 305, +318, 328, 336, 123, 123, 123, 123, 123, 123, 123, 123, 240, 240, 240, 240, +305, 305, 305, 318, 318, 343, 351, 358, 364, 240, 240, 240, 240, 240, 240, +240, 240, 305, 305, 305, 305, 343, 343, 343, 351, 351, 370, 376, 382, 387, +}; +static const unsigned char cache_bits50[392] = { +40, 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, 40, 15, 23, 28, +31, 34, 36, 38, 39, 41, 42, 43, 44, 45, 46, 47, 47, 49, 50, +51, 52, 53, 54, 55, 55, 57, 58, 59, 60, 61, 62, 63, 63, 65, +66, 67, 68, 69, 70, 71, 71, 40, 20, 33, 41, 48, 53, 57, 61, +64, 66, 69, 71, 73, 75, 76, 78, 80, 82, 85, 87, 89, 91, 92, +94, 96, 98, 101, 103, 105, 107, 108, 110, 112, 114, 117, 119, 121, 123, +124, 126, 128, 40, 23, 39, 51, 60, 67, 73, 79, 83, 87, 91, 94, +97, 100, 102, 105, 107, 111, 115, 118, 121, 124, 126, 129, 131, 135, 139, +142, 145, 148, 150, 153, 155, 159, 163, 166, 169, 172, 174, 177, 179, 35, +28, 49, 65, 78, 89, 99, 107, 114, 120, 126, 132, 136, 141, 145, 149, +153, 159, 165, 171, 176, 180, 185, 189, 192, 199, 205, 211, 216, 220, 225, +229, 232, 239, 245, 251, 21, 33, 58, 79, 97, 112, 125, 137, 148, 157, +166, 174, 182, 189, 195, 201, 207, 217, 227, 235, 243, 251, 17, 35, 63, +86, 106, 123, 139, 152, 165, 177, 187, 197, 206, 214, 222, 230, 237, 250, +25, 31, 55, 75, 91, 105, 117, 128, 138, 146, 154, 161, 168, 174, 180, +185, 190, 200, 208, 215, 222, 229, 235, 240, 245, 255, 16, 36, 65, 89, +110, 128, 144, 159, 173, 185, 196, 207, 217, 226, 234, 242, 250, 11, 41, +74, 103, 128, 151, 172, 191, 209, 225, 241, 255, 9, 43, 79, 110, 138, +163, 186, 207, 227, 246, 12, 39, 71, 99, 123, 144, 164, 182, 198, 214, +228, 241, 253, 9, 44, 81, 113, 142, 168, 192, 214, 235, 255, 7, 49, +90, 127, 160, 191, 220, 247, 6, 51, 95, 134, 170, 203, 234, 7, 47, +87, 123, 155, 184, 212, 237, 6, 52, 97, 137, 174, 208, 240, 5, 57, +106, 151, 192, 231, 5, 59, 111, 158, 202, 243, 5, 55, 103, 147, 187, +224, 5, 60, 113, 161, 206, 248, 4, 65, 122, 175, 224, 4, 67, 127, +182, 234, }; +static const unsigned char cache_caps50[168] = { +224, 224, 224, 224, 224, 224, 224, 224, 160, 160, 160, 160, 185, 185, 185, +178, 178, 168, 134, 61, 37, 224, 224, 224, 224, 224, 224, 224, 224, 240, +240, 240, 240, 207, 207, 207, 198, 198, 183, 144, 66, 40, 160, 160, 160, +160, 160, 160, 160, 160, 185, 185, 185, 185, 193, 193, 193, 183, 183, 172, +138, 64, 38, 240, 240, 240, 240, 240, 240, 240, 240, 207, 207, 207, 207, +204, 204, 204, 193, 193, 180, 143, 66, 40, 185, 185, 185, 185, 185, 185, +185, 185, 193, 193, 193, 193, 193, 193, 193, 183, 183, 172, 138, 65, 39, +207, 207, 207, 207, 207, 207, 207, 207, 204, 204, 204, 204, 201, 201, 201, +188, 188, 176, 141, 66, 40, 193, 193, 193, 193, 193, 193, 193, 193, 193, +193, 193, 193, 194, 194, 194, 184, 184, 173, 139, 65, 39, 204, 204, 204, +204, 204, 204, 204, 204, 201, 201, 201, 201, 198, 198, 198, 187, 187, 175, +140, 66, 40, }; +#endif + +#ifndef FFT_TWIDDLES48000_960 +#define FFT_TWIDDLES48000_960 +static const kiss_twiddle_cpx fft_twiddles48000_960[480] = { +{1.0000000f, -0.0000000f}, {0.99991433f, -0.013089596f}, +{0.99965732f, -0.026176948f}, {0.99922904f, -0.039259816f}, +{0.99862953f, -0.052335956f}, {0.99785892f, -0.065403129f}, +{0.99691733f, -0.078459096f}, {0.99580493f, -0.091501619f}, +{0.99452190f, -0.10452846f}, {0.99306846f, -0.11753740f}, +{0.99144486f, -0.13052619f}, {0.98965139f, -0.14349262f}, +{0.98768834f, -0.15643447f}, {0.98555606f, -0.16934950f}, +{0.98325491f, -0.18223553f}, {0.98078528f, -0.19509032f}, +{0.97814760f, -0.20791169f}, {0.97534232f, -0.22069744f}, +{0.97236992f, -0.23344536f}, {0.96923091f, -0.24615329f}, +{0.96592583f, -0.25881905f}, {0.96245524f, -0.27144045f}, +{0.95881973f, -0.28401534f}, {0.95501994f, -0.29654157f}, +{0.95105652f, -0.30901699f}, {0.94693013f, -0.32143947f}, +{0.94264149f, -0.33380686f}, {0.93819134f, -0.34611706f}, +{0.93358043f, -0.35836795f}, {0.92880955f, -0.37055744f}, +{0.92387953f, -0.38268343f}, {0.91879121f, -0.39474386f}, +{0.91354546f, -0.40673664f}, {0.90814317f, -0.41865974f}, +{0.90258528f, -0.43051110f}, {0.89687274f, -0.44228869f}, +{0.89100652f, -0.45399050f}, {0.88498764f, -0.46561452f}, +{0.87881711f, -0.47715876f}, {0.87249601f, -0.48862124f}, +{0.86602540f, -0.50000000f}, {0.85940641f, -0.51129309f}, +{0.85264016f, -0.52249856f}, {0.84572782f, -0.53361452f}, +{0.83867057f, -0.54463904f}, {0.83146961f, -0.55557023f}, +{0.82412619f, -0.56640624f}, {0.81664156f, -0.57714519f}, +{0.80901699f, -0.58778525f}, {0.80125381f, -0.59832460f}, +{0.79335334f, -0.60876143f}, {0.78531693f, -0.61909395f}, +{0.77714596f, -0.62932039f}, {0.76884183f, -0.63943900f}, +{0.76040597f, -0.64944805f}, {0.75183981f, -0.65934582f}, +{0.74314483f, -0.66913061f}, {0.73432251f, -0.67880075f}, +{0.72537437f, -0.68835458f}, {0.71630194f, -0.69779046f}, +{0.70710678f, -0.70710678f}, {0.69779046f, -0.71630194f}, +{0.68835458f, -0.72537437f}, {0.67880075f, -0.73432251f}, +{0.66913061f, -0.74314483f}, {0.65934582f, -0.75183981f}, +{0.64944805f, -0.76040597f}, {0.63943900f, -0.76884183f}, +{0.62932039f, -0.77714596f}, {0.61909395f, -0.78531693f}, +{0.60876143f, -0.79335334f}, {0.59832460f, -0.80125381f}, +{0.58778525f, -0.80901699f}, {0.57714519f, -0.81664156f}, +{0.56640624f, -0.82412619f}, {0.55557023f, -0.83146961f}, +{0.54463904f, -0.83867057f}, {0.53361452f, -0.84572782f}, +{0.52249856f, -0.85264016f}, {0.51129309f, -0.85940641f}, +{0.50000000f, -0.86602540f}, {0.48862124f, -0.87249601f}, +{0.47715876f, -0.87881711f}, {0.46561452f, -0.88498764f}, +{0.45399050f, -0.89100652f}, {0.44228869f, -0.89687274f}, +{0.43051110f, -0.90258528f}, {0.41865974f, -0.90814317f}, +{0.40673664f, -0.91354546f}, {0.39474386f, -0.91879121f}, +{0.38268343f, -0.92387953f}, {0.37055744f, -0.92880955f}, +{0.35836795f, -0.93358043f}, {0.34611706f, -0.93819134f}, +{0.33380686f, -0.94264149f}, {0.32143947f, -0.94693013f}, +{0.30901699f, -0.95105652f}, {0.29654157f, -0.95501994f}, +{0.28401534f, -0.95881973f}, {0.27144045f, -0.96245524f}, +{0.25881905f, -0.96592583f}, {0.24615329f, -0.96923091f}, +{0.23344536f, -0.97236992f}, {0.22069744f, -0.97534232f}, +{0.20791169f, -0.97814760f}, {0.19509032f, -0.98078528f}, +{0.18223553f, -0.98325491f}, {0.16934950f, -0.98555606f}, +{0.15643447f, -0.98768834f}, {0.14349262f, -0.98965139f}, +{0.13052619f, -0.99144486f}, {0.11753740f, -0.99306846f}, +{0.10452846f, -0.99452190f}, {0.091501619f, -0.99580493f}, +{0.078459096f, -0.99691733f}, {0.065403129f, -0.99785892f}, +{0.052335956f, -0.99862953f}, {0.039259816f, -0.99922904f}, +{0.026176948f, -0.99965732f}, {0.013089596f, -0.99991433f}, +{6.1230318e-17f, -1.0000000f}, {-0.013089596f, -0.99991433f}, +{-0.026176948f, -0.99965732f}, {-0.039259816f, -0.99922904f}, +{-0.052335956f, -0.99862953f}, {-0.065403129f, -0.99785892f}, +{-0.078459096f, -0.99691733f}, {-0.091501619f, -0.99580493f}, +{-0.10452846f, -0.99452190f}, {-0.11753740f, -0.99306846f}, +{-0.13052619f, -0.99144486f}, {-0.14349262f, -0.98965139f}, +{-0.15643447f, -0.98768834f}, {-0.16934950f, -0.98555606f}, +{-0.18223553f, -0.98325491f}, {-0.19509032f, -0.98078528f}, +{-0.20791169f, -0.97814760f}, {-0.22069744f, -0.97534232f}, +{-0.23344536f, -0.97236992f}, {-0.24615329f, -0.96923091f}, +{-0.25881905f, -0.96592583f}, {-0.27144045f, -0.96245524f}, +{-0.28401534f, -0.95881973f}, {-0.29654157f, -0.95501994f}, +{-0.30901699f, -0.95105652f}, {-0.32143947f, -0.94693013f}, +{-0.33380686f, -0.94264149f}, {-0.34611706f, -0.93819134f}, +{-0.35836795f, -0.93358043f}, {-0.37055744f, -0.92880955f}, +{-0.38268343f, -0.92387953f}, {-0.39474386f, -0.91879121f}, +{-0.40673664f, -0.91354546f}, {-0.41865974f, -0.90814317f}, +{-0.43051110f, -0.90258528f}, {-0.44228869f, -0.89687274f}, +{-0.45399050f, -0.89100652f}, {-0.46561452f, -0.88498764f}, +{-0.47715876f, -0.87881711f}, {-0.48862124f, -0.87249601f}, +{-0.50000000f, -0.86602540f}, {-0.51129309f, -0.85940641f}, +{-0.52249856f, -0.85264016f}, {-0.53361452f, -0.84572782f}, +{-0.54463904f, -0.83867057f}, {-0.55557023f, -0.83146961f}, +{-0.56640624f, -0.82412619f}, {-0.57714519f, -0.81664156f}, +{-0.58778525f, -0.80901699f}, {-0.59832460f, -0.80125381f}, +{-0.60876143f, -0.79335334f}, {-0.61909395f, -0.78531693f}, +{-0.62932039f, -0.77714596f}, {-0.63943900f, -0.76884183f}, +{-0.64944805f, -0.76040597f}, {-0.65934582f, -0.75183981f}, +{-0.66913061f, -0.74314483f}, {-0.67880075f, -0.73432251f}, +{-0.68835458f, -0.72537437f}, {-0.69779046f, -0.71630194f}, +{-0.70710678f, -0.70710678f}, {-0.71630194f, -0.69779046f}, +{-0.72537437f, -0.68835458f}, {-0.73432251f, -0.67880075f}, +{-0.74314483f, -0.66913061f}, {-0.75183981f, -0.65934582f}, +{-0.76040597f, -0.64944805f}, {-0.76884183f, -0.63943900f}, +{-0.77714596f, -0.62932039f}, {-0.78531693f, -0.61909395f}, +{-0.79335334f, -0.60876143f}, {-0.80125381f, -0.59832460f}, +{-0.80901699f, -0.58778525f}, {-0.81664156f, -0.57714519f}, +{-0.82412619f, -0.56640624f}, {-0.83146961f, -0.55557023f}, +{-0.83867057f, -0.54463904f}, {-0.84572782f, -0.53361452f}, +{-0.85264016f, -0.52249856f}, {-0.85940641f, -0.51129309f}, +{-0.86602540f, -0.50000000f}, {-0.87249601f, -0.48862124f}, +{-0.87881711f, -0.47715876f}, {-0.88498764f, -0.46561452f}, +{-0.89100652f, -0.45399050f}, {-0.89687274f, -0.44228869f}, +{-0.90258528f, -0.43051110f}, {-0.90814317f, -0.41865974f}, +{-0.91354546f, -0.40673664f}, {-0.91879121f, -0.39474386f}, +{-0.92387953f, -0.38268343f}, {-0.92880955f, -0.37055744f}, +{-0.93358043f, -0.35836795f}, {-0.93819134f, -0.34611706f}, +{-0.94264149f, -0.33380686f}, {-0.94693013f, -0.32143947f}, +{-0.95105652f, -0.30901699f}, {-0.95501994f, -0.29654157f}, +{-0.95881973f, -0.28401534f}, {-0.96245524f, -0.27144045f}, +{-0.96592583f, -0.25881905f}, {-0.96923091f, -0.24615329f}, +{-0.97236992f, -0.23344536f}, {-0.97534232f, -0.22069744f}, +{-0.97814760f, -0.20791169f}, {-0.98078528f, -0.19509032f}, +{-0.98325491f, -0.18223553f}, {-0.98555606f, -0.16934950f}, +{-0.98768834f, -0.15643447f}, {-0.98965139f, -0.14349262f}, +{-0.99144486f, -0.13052619f}, {-0.99306846f, -0.11753740f}, +{-0.99452190f, -0.10452846f}, {-0.99580493f, -0.091501619f}, +{-0.99691733f, -0.078459096f}, {-0.99785892f, -0.065403129f}, +{-0.99862953f, -0.052335956f}, {-0.99922904f, -0.039259816f}, +{-0.99965732f, -0.026176948f}, {-0.99991433f, -0.013089596f}, +{-1.0000000f, -1.2246064e-16f}, {-0.99991433f, 0.013089596f}, +{-0.99965732f, 0.026176948f}, {-0.99922904f, 0.039259816f}, +{-0.99862953f, 0.052335956f}, {-0.99785892f, 0.065403129f}, +{-0.99691733f, 0.078459096f}, {-0.99580493f, 0.091501619f}, +{-0.99452190f, 0.10452846f}, {-0.99306846f, 0.11753740f}, +{-0.99144486f, 0.13052619f}, {-0.98965139f, 0.14349262f}, +{-0.98768834f, 0.15643447f}, {-0.98555606f, 0.16934950f}, +{-0.98325491f, 0.18223553f}, {-0.98078528f, 0.19509032f}, +{-0.97814760f, 0.20791169f}, {-0.97534232f, 0.22069744f}, +{-0.97236992f, 0.23344536f}, {-0.96923091f, 0.24615329f}, +{-0.96592583f, 0.25881905f}, {-0.96245524f, 0.27144045f}, +{-0.95881973f, 0.28401534f}, {-0.95501994f, 0.29654157f}, +{-0.95105652f, 0.30901699f}, {-0.94693013f, 0.32143947f}, +{-0.94264149f, 0.33380686f}, {-0.93819134f, 0.34611706f}, +{-0.93358043f, 0.35836795f}, {-0.92880955f, 0.37055744f}, +{-0.92387953f, 0.38268343f}, {-0.91879121f, 0.39474386f}, +{-0.91354546f, 0.40673664f}, {-0.90814317f, 0.41865974f}, +{-0.90258528f, 0.43051110f}, {-0.89687274f, 0.44228869f}, +{-0.89100652f, 0.45399050f}, {-0.88498764f, 0.46561452f}, +{-0.87881711f, 0.47715876f}, {-0.87249601f, 0.48862124f}, +{-0.86602540f, 0.50000000f}, {-0.85940641f, 0.51129309f}, +{-0.85264016f, 0.52249856f}, {-0.84572782f, 0.53361452f}, +{-0.83867057f, 0.54463904f}, {-0.83146961f, 0.55557023f}, +{-0.82412619f, 0.56640624f}, {-0.81664156f, 0.57714519f}, +{-0.80901699f, 0.58778525f}, {-0.80125381f, 0.59832460f}, +{-0.79335334f, 0.60876143f}, {-0.78531693f, 0.61909395f}, +{-0.77714596f, 0.62932039f}, {-0.76884183f, 0.63943900f}, +{-0.76040597f, 0.64944805f}, {-0.75183981f, 0.65934582f}, +{-0.74314483f, 0.66913061f}, {-0.73432251f, 0.67880075f}, +{-0.72537437f, 0.68835458f}, {-0.71630194f, 0.69779046f}, +{-0.70710678f, 0.70710678f}, {-0.69779046f, 0.71630194f}, +{-0.68835458f, 0.72537437f}, {-0.67880075f, 0.73432251f}, +{-0.66913061f, 0.74314483f}, {-0.65934582f, 0.75183981f}, +{-0.64944805f, 0.76040597f}, {-0.63943900f, 0.76884183f}, +{-0.62932039f, 0.77714596f}, {-0.61909395f, 0.78531693f}, +{-0.60876143f, 0.79335334f}, {-0.59832460f, 0.80125381f}, +{-0.58778525f, 0.80901699f}, {-0.57714519f, 0.81664156f}, +{-0.56640624f, 0.82412619f}, {-0.55557023f, 0.83146961f}, +{-0.54463904f, 0.83867057f}, {-0.53361452f, 0.84572782f}, +{-0.52249856f, 0.85264016f}, {-0.51129309f, 0.85940641f}, +{-0.50000000f, 0.86602540f}, {-0.48862124f, 0.87249601f}, +{-0.47715876f, 0.87881711f}, {-0.46561452f, 0.88498764f}, +{-0.45399050f, 0.89100652f}, {-0.44228869f, 0.89687274f}, +{-0.43051110f, 0.90258528f}, {-0.41865974f, 0.90814317f}, +{-0.40673664f, 0.91354546f}, {-0.39474386f, 0.91879121f}, +{-0.38268343f, 0.92387953f}, {-0.37055744f, 0.92880955f}, +{-0.35836795f, 0.93358043f}, {-0.34611706f, 0.93819134f}, +{-0.33380686f, 0.94264149f}, {-0.32143947f, 0.94693013f}, +{-0.30901699f, 0.95105652f}, {-0.29654157f, 0.95501994f}, +{-0.28401534f, 0.95881973f}, {-0.27144045f, 0.96245524f}, +{-0.25881905f, 0.96592583f}, {-0.24615329f, 0.96923091f}, +{-0.23344536f, 0.97236992f}, {-0.22069744f, 0.97534232f}, +{-0.20791169f, 0.97814760f}, {-0.19509032f, 0.98078528f}, +{-0.18223553f, 0.98325491f}, {-0.16934950f, 0.98555606f}, +{-0.15643447f, 0.98768834f}, {-0.14349262f, 0.98965139f}, +{-0.13052619f, 0.99144486f}, {-0.11753740f, 0.99306846f}, +{-0.10452846f, 0.99452190f}, {-0.091501619f, 0.99580493f}, +{-0.078459096f, 0.99691733f}, {-0.065403129f, 0.99785892f}, +{-0.052335956f, 0.99862953f}, {-0.039259816f, 0.99922904f}, +{-0.026176948f, 0.99965732f}, {-0.013089596f, 0.99991433f}, +{-1.8369095e-16f, 1.0000000f}, {0.013089596f, 0.99991433f}, +{0.026176948f, 0.99965732f}, {0.039259816f, 0.99922904f}, +{0.052335956f, 0.99862953f}, {0.065403129f, 0.99785892f}, +{0.078459096f, 0.99691733f}, {0.091501619f, 0.99580493f}, +{0.10452846f, 0.99452190f}, {0.11753740f, 0.99306846f}, +{0.13052619f, 0.99144486f}, {0.14349262f, 0.98965139f}, +{0.15643447f, 0.98768834f}, {0.16934950f, 0.98555606f}, +{0.18223553f, 0.98325491f}, {0.19509032f, 0.98078528f}, +{0.20791169f, 0.97814760f}, {0.22069744f, 0.97534232f}, +{0.23344536f, 0.97236992f}, {0.24615329f, 0.96923091f}, +{0.25881905f, 0.96592583f}, {0.27144045f, 0.96245524f}, +{0.28401534f, 0.95881973f}, {0.29654157f, 0.95501994f}, +{0.30901699f, 0.95105652f}, {0.32143947f, 0.94693013f}, +{0.33380686f, 0.94264149f}, {0.34611706f, 0.93819134f}, +{0.35836795f, 0.93358043f}, {0.37055744f, 0.92880955f}, +{0.38268343f, 0.92387953f}, {0.39474386f, 0.91879121f}, +{0.40673664f, 0.91354546f}, {0.41865974f, 0.90814317f}, +{0.43051110f, 0.90258528f}, {0.44228869f, 0.89687274f}, +{0.45399050f, 0.89100652f}, {0.46561452f, 0.88498764f}, +{0.47715876f, 0.87881711f}, {0.48862124f, 0.87249601f}, +{0.50000000f, 0.86602540f}, {0.51129309f, 0.85940641f}, +{0.52249856f, 0.85264016f}, {0.53361452f, 0.84572782f}, +{0.54463904f, 0.83867057f}, {0.55557023f, 0.83146961f}, +{0.56640624f, 0.82412619f}, {0.57714519f, 0.81664156f}, +{0.58778525f, 0.80901699f}, {0.59832460f, 0.80125381f}, +{0.60876143f, 0.79335334f}, {0.61909395f, 0.78531693f}, +{0.62932039f, 0.77714596f}, {0.63943900f, 0.76884183f}, +{0.64944805f, 0.76040597f}, {0.65934582f, 0.75183981f}, +{0.66913061f, 0.74314483f}, {0.67880075f, 0.73432251f}, +{0.68835458f, 0.72537437f}, {0.69779046f, 0.71630194f}, +{0.70710678f, 0.70710678f}, {0.71630194f, 0.69779046f}, +{0.72537437f, 0.68835458f}, {0.73432251f, 0.67880075f}, +{0.74314483f, 0.66913061f}, {0.75183981f, 0.65934582f}, +{0.76040597f, 0.64944805f}, {0.76884183f, 0.63943900f}, +{0.77714596f, 0.62932039f}, {0.78531693f, 0.61909395f}, +{0.79335334f, 0.60876143f}, {0.80125381f, 0.59832460f}, +{0.80901699f, 0.58778525f}, {0.81664156f, 0.57714519f}, +{0.82412619f, 0.56640624f}, {0.83146961f, 0.55557023f}, +{0.83867057f, 0.54463904f}, {0.84572782f, 0.53361452f}, +{0.85264016f, 0.52249856f}, {0.85940641f, 0.51129309f}, +{0.86602540f, 0.50000000f}, {0.87249601f, 0.48862124f}, +{0.87881711f, 0.47715876f}, {0.88498764f, 0.46561452f}, +{0.89100652f, 0.45399050f}, {0.89687274f, 0.44228869f}, +{0.90258528f, 0.43051110f}, {0.90814317f, 0.41865974f}, +{0.91354546f, 0.40673664f}, {0.91879121f, 0.39474386f}, +{0.92387953f, 0.38268343f}, {0.92880955f, 0.37055744f}, +{0.93358043f, 0.35836795f}, {0.93819134f, 0.34611706f}, +{0.94264149f, 0.33380686f}, {0.94693013f, 0.32143947f}, +{0.95105652f, 0.30901699f}, {0.95501994f, 0.29654157f}, +{0.95881973f, 0.28401534f}, {0.96245524f, 0.27144045f}, +{0.96592583f, 0.25881905f}, {0.96923091f, 0.24615329f}, +{0.97236992f, 0.23344536f}, {0.97534232f, 0.22069744f}, +{0.97814760f, 0.20791169f}, {0.98078528f, 0.19509032f}, +{0.98325491f, 0.18223553f}, {0.98555606f, 0.16934950f}, +{0.98768834f, 0.15643447f}, {0.98965139f, 0.14349262f}, +{0.99144486f, 0.13052619f}, {0.99306846f, 0.11753740f}, +{0.99452190f, 0.10452846f}, {0.99580493f, 0.091501619f}, +{0.99691733f, 0.078459096f}, {0.99785892f, 0.065403129f}, +{0.99862953f, 0.052335956f}, {0.99922904f, 0.039259816f}, +{0.99965732f, 0.026176948f}, {0.99991433f, 0.013089596f}, +}; +#ifndef FFT_BITREV480 +#define FFT_BITREV480 +static const opus_int16 fft_bitrev480[480] = { +0, 96, 192, 288, 384, 32, 128, 224, 320, 416, 64, 160, 256, 352, 448, +8, 104, 200, 296, 392, 40, 136, 232, 328, 424, 72, 168, 264, 360, 456, +16, 112, 208, 304, 400, 48, 144, 240, 336, 432, 80, 176, 272, 368, 464, +24, 120, 216, 312, 408, 56, 152, 248, 344, 440, 88, 184, 280, 376, 472, +4, 100, 196, 292, 388, 36, 132, 228, 324, 420, 68, 164, 260, 356, 452, +12, 108, 204, 300, 396, 44, 140, 236, 332, 428, 76, 172, 268, 364, 460, +20, 116, 212, 308, 404, 52, 148, 244, 340, 436, 84, 180, 276, 372, 468, +28, 124, 220, 316, 412, 60, 156, 252, 348, 444, 92, 188, 284, 380, 476, +1, 97, 193, 289, 385, 33, 129, 225, 321, 417, 65, 161, 257, 353, 449, +9, 105, 201, 297, 393, 41, 137, 233, 329, 425, 73, 169, 265, 361, 457, +17, 113, 209, 305, 401, 49, 145, 241, 337, 433, 81, 177, 273, 369, 465, +25, 121, 217, 313, 409, 57, 153, 249, 345, 441, 89, 185, 281, 377, 473, +5, 101, 197, 293, 389, 37, 133, 229, 325, 421, 69, 165, 261, 357, 453, +13, 109, 205, 301, 397, 45, 141, 237, 333, 429, 77, 173, 269, 365, 461, +21, 117, 213, 309, 405, 53, 149, 245, 341, 437, 85, 181, 277, 373, 469, +29, 125, 221, 317, 413, 61, 157, 253, 349, 445, 93, 189, 285, 381, 477, +2, 98, 194, 290, 386, 34, 130, 226, 322, 418, 66, 162, 258, 354, 450, +10, 106, 202, 298, 394, 42, 138, 234, 330, 426, 74, 170, 266, 362, 458, +18, 114, 210, 306, 402, 50, 146, 242, 338, 434, 82, 178, 274, 370, 466, +26, 122, 218, 314, 410, 58, 154, 250, 346, 442, 90, 186, 282, 378, 474, +6, 102, 198, 294, 390, 38, 134, 230, 326, 422, 70, 166, 262, 358, 454, +14, 110, 206, 302, 398, 46, 142, 238, 334, 430, 78, 174, 270, 366, 462, +22, 118, 214, 310, 406, 54, 150, 246, 342, 438, 86, 182, 278, 374, 470, +30, 126, 222, 318, 414, 62, 158, 254, 350, 446, 94, 190, 286, 382, 478, +3, 99, 195, 291, 387, 35, 131, 227, 323, 419, 67, 163, 259, 355, 451, +11, 107, 203, 299, 395, 43, 139, 235, 331, 427, 75, 171, 267, 363, 459, +19, 115, 211, 307, 403, 51, 147, 243, 339, 435, 83, 179, 275, 371, 467, +27, 123, 219, 315, 411, 59, 155, 251, 347, 443, 91, 187, 283, 379, 475, +7, 103, 199, 295, 391, 39, 135, 231, 327, 423, 71, 167, 263, 359, 455, +15, 111, 207, 303, 399, 47, 143, 239, 335, 431, 79, 175, 271, 367, 463, +23, 119, 215, 311, 407, 55, 151, 247, 343, 439, 87, 183, 279, 375, 471, +31, 127, 223, 319, 415, 63, 159, 255, 351, 447, 95, 191, 287, 383, 479, +}; +#endif + +#ifndef FFT_BITREV240 +#define FFT_BITREV240 +static const opus_int16 fft_bitrev240[240] = { +0, 48, 96, 144, 192, 16, 64, 112, 160, 208, 32, 80, 128, 176, 224, +4, 52, 100, 148, 196, 20, 68, 116, 164, 212, 36, 84, 132, 180, 228, +8, 56, 104, 152, 200, 24, 72, 120, 168, 216, 40, 88, 136, 184, 232, +12, 60, 108, 156, 204, 28, 76, 124, 172, 220, 44, 92, 140, 188, 236, +1, 49, 97, 145, 193, 17, 65, 113, 161, 209, 33, 81, 129, 177, 225, +5, 53, 101, 149, 197, 21, 69, 117, 165, 213, 37, 85, 133, 181, 229, +9, 57, 105, 153, 201, 25, 73, 121, 169, 217, 41, 89, 137, 185, 233, +13, 61, 109, 157, 205, 29, 77, 125, 173, 221, 45, 93, 141, 189, 237, +2, 50, 98, 146, 194, 18, 66, 114, 162, 210, 34, 82, 130, 178, 226, +6, 54, 102, 150, 198, 22, 70, 118, 166, 214, 38, 86, 134, 182, 230, +10, 58, 106, 154, 202, 26, 74, 122, 170, 218, 42, 90, 138, 186, 234, +14, 62, 110, 158, 206, 30, 78, 126, 174, 222, 46, 94, 142, 190, 238, +3, 51, 99, 147, 195, 19, 67, 115, 163, 211, 35, 83, 131, 179, 227, +7, 55, 103, 151, 199, 23, 71, 119, 167, 215, 39, 87, 135, 183, 231, +11, 59, 107, 155, 203, 27, 75, 123, 171, 219, 43, 91, 139, 187, 235, +15, 63, 111, 159, 207, 31, 79, 127, 175, 223, 47, 95, 143, 191, 239, +}; +#endif + +#ifndef FFT_BITREV120 +#define FFT_BITREV120 +static const opus_int16 fft_bitrev120[120] = { +0, 24, 48, 72, 96, 8, 32, 56, 80, 104, 16, 40, 64, 88, 112, +4, 28, 52, 76, 100, 12, 36, 60, 84, 108, 20, 44, 68, 92, 116, +1, 25, 49, 73, 97, 9, 33, 57, 81, 105, 17, 41, 65, 89, 113, +5, 29, 53, 77, 101, 13, 37, 61, 85, 109, 21, 45, 69, 93, 117, +2, 26, 50, 74, 98, 10, 34, 58, 82, 106, 18, 42, 66, 90, 114, +6, 30, 54, 78, 102, 14, 38, 62, 86, 110, 22, 46, 70, 94, 118, +3, 27, 51, 75, 99, 11, 35, 59, 83, 107, 19, 43, 67, 91, 115, +7, 31, 55, 79, 103, 15, 39, 63, 87, 111, 23, 47, 71, 95, 119, +}; +#endif + +#ifndef FFT_BITREV60 +#define FFT_BITREV60 +static const opus_int16 fft_bitrev60[60] = { +0, 12, 24, 36, 48, 4, 16, 28, 40, 52, 8, 20, 32, 44, 56, +1, 13, 25, 37, 49, 5, 17, 29, 41, 53, 9, 21, 33, 45, 57, +2, 14, 26, 38, 50, 6, 18, 30, 42, 54, 10, 22, 34, 46, 58, +3, 15, 27, 39, 51, 7, 19, 31, 43, 55, 11, 23, 35, 47, 59, +}; +#endif + +#ifndef FFT_STATE48000_960_0 +#define FFT_STATE48000_960_0 +static const kiss_fft_state fft_state48000_960_0 = { +480, /* nfft */ +0.002083333f, /* scale */ +-1, /* shift */ +{5, 96, 3, 32, 4, 8, 2, 4, 4, 1, 0, 0, 0, 0, 0, 0, }, /* factors */ +fft_bitrev480, /* bitrev */ +fft_twiddles48000_960, /* bitrev */ +#ifdef OVERRIDE_FFT +(arch_fft_state *)&cfg_arch_480, +#else +NULL, +#endif +}; +#endif + +#ifndef FFT_STATE48000_960_1 +#define FFT_STATE48000_960_1 +static const kiss_fft_state fft_state48000_960_1 = { +240, /* nfft */ +0.004166667f, /* scale */ +1, /* shift */ +{5, 48, 3, 16, 4, 4, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, }, /* factors */ +fft_bitrev240, /* bitrev */ +fft_twiddles48000_960, /* bitrev */ +#ifdef OVERRIDE_FFT +(arch_fft_state *)&cfg_arch_240, +#else +NULL, +#endif +}; +#endif + +#ifndef FFT_STATE48000_960_2 +#define FFT_STATE48000_960_2 +static const kiss_fft_state fft_state48000_960_2 = { +120, /* nfft */ +0.008333333f, /* scale */ +2, /* shift */ +{5, 24, 3, 8, 2, 4, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, }, /* factors */ +fft_bitrev120, /* bitrev */ +fft_twiddles48000_960, /* bitrev */ +#ifdef OVERRIDE_FFT +(arch_fft_state *)&cfg_arch_120, +#else +NULL, +#endif +}; +#endif + +#ifndef FFT_STATE48000_960_3 +#define FFT_STATE48000_960_3 +static const kiss_fft_state fft_state48000_960_3 = { +60, /* nfft */ +0.016666667f, /* scale */ +3, /* shift */ +{5, 12, 3, 4, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, /* factors */ +fft_bitrev60, /* bitrev */ +fft_twiddles48000_960, /* bitrev */ +#ifdef OVERRIDE_FFT +(arch_fft_state *)&cfg_arch_60, +#else +NULL, +#endif +}; +#endif + +#endif + +#ifndef MDCT_TWIDDLES960 +#define MDCT_TWIDDLES960 +static const opus_val16 mdct_twiddles960[1800] = { +0.99999994f, 0.99999321f, 0.99997580f, 0.99994773f, 0.99990886f, +0.99985933f, 0.99979913f, 0.99972820f, 0.99964654f, 0.99955416f, +0.99945110f, 0.99933738f, 0.99921292f, 0.99907774f, 0.99893188f, +0.99877530f, 0.99860805f, 0.99843007f, 0.99824142f, 0.99804211f, +0.99783206f, 0.99761140f, 0.99737996f, 0.99713790f, 0.99688518f, +0.99662173f, 0.99634761f, 0.99606287f, 0.99576741f, 0.99546129f, +0.99514455f, 0.99481714f, 0.99447906f, 0.99413031f, 0.99377096f, +0.99340093f, 0.99302030f, 0.99262899f, 0.99222708f, 0.99181455f, +0.99139136f, 0.99095762f, 0.99051321f, 0.99005818f, 0.98959261f, +0.98911643f, 0.98862964f, 0.98813224f, 0.98762429f, 0.98710573f, +0.98657662f, 0.98603696f, 0.98548669f, 0.98492593f, 0.98435456f, +0.98377270f, 0.98318028f, 0.98257732f, 0.98196387f, 0.98133987f, +0.98070538f, 0.98006040f, 0.97940493f, 0.97873890f, 0.97806245f, +0.97737551f, 0.97667813f, 0.97597027f, 0.97525197f, 0.97452319f, +0.97378403f, 0.97303438f, 0.97227436f, 0.97150391f, 0.97072303f, +0.96993178f, 0.96913016f, 0.96831810f, 0.96749574f, 0.96666300f, +0.96581990f, 0.96496642f, 0.96410263f, 0.96322852f, 0.96234411f, +0.96144938f, 0.96054435f, 0.95962906f, 0.95870346f, 0.95776761f, +0.95682150f, 0.95586514f, 0.95489854f, 0.95392174f, 0.95293468f, +0.95193744f, 0.95093000f, 0.94991243f, 0.94888461f, 0.94784665f, +0.94679856f, 0.94574034f, 0.94467193f, 0.94359344f, 0.94250488f, +0.94140619f, 0.94029742f, 0.93917859f, 0.93804967f, 0.93691075f, +0.93576175f, 0.93460274f, 0.93343377f, 0.93225473f, 0.93106574f, +0.92986679f, 0.92865789f, 0.92743903f, 0.92621022f, 0.92497152f, +0.92372292f, 0.92246443f, 0.92119598f, 0.91991776f, 0.91862965f, +0.91733170f, 0.91602397f, 0.91470635f, 0.91337901f, 0.91204184f, +0.91069490f, 0.90933824f, 0.90797186f, 0.90659571f, 0.90520984f, +0.90381432f, 0.90240908f, 0.90099424f, 0.89956969f, 0.89813554f, +0.89669174f, 0.89523834f, 0.89377540f, 0.89230281f, 0.89082074f, +0.88932908f, 0.88782793f, 0.88631725f, 0.88479710f, 0.88326746f, +0.88172835f, 0.88017982f, 0.87862182f, 0.87705445f, 0.87547767f, +0.87389153f, 0.87229604f, 0.87069118f, 0.86907703f, 0.86745358f, +0.86582077f, 0.86417878f, 0.86252749f, 0.86086690f, 0.85919720f, +0.85751826f, 0.85583007f, 0.85413277f, 0.85242635f, 0.85071075f, +0.84898609f, 0.84725231f, 0.84550947f, 0.84375757f, 0.84199661f, +0.84022665f, 0.83844769f, 0.83665979f, 0.83486289f, 0.83305705f, +0.83124226f, 0.82941860f, 0.82758605f, 0.82574469f, 0.82389444f, +0.82203537f, 0.82016748f, 0.81829083f, 0.81640542f, 0.81451124f, +0.81260836f, 0.81069672f, 0.80877650f, 0.80684757f, 0.80490994f, +0.80296379f, 0.80100900f, 0.79904562f, 0.79707366f, 0.79509324f, +0.79310423f, 0.79110676f, 0.78910083f, 0.78708643f, 0.78506362f, +0.78303236f, 0.78099275f, 0.77894479f, 0.77688843f, 0.77482378f, +0.77275085f, 0.77066964f, 0.76858020f, 0.76648247f, 0.76437658f, +0.76226246f, 0.76014024f, 0.75800985f, 0.75587130f, 0.75372469f, +0.75157005f, 0.74940729f, 0.74723655f, 0.74505776f, 0.74287105f, +0.74067634f, 0.73847371f, 0.73626316f, 0.73404479f, 0.73181850f, +0.72958434f, 0.72734243f, 0.72509271f, 0.72283524f, 0.72057003f, +0.71829706f, 0.71601641f, 0.71372813f, 0.71143216f, 0.70912862f, +0.70681745f, 0.70449871f, 0.70217246f, 0.69983864f, 0.69749737f, +0.69514859f, 0.69279242f, 0.69042879f, 0.68805778f, 0.68567938f, +0.68329364f, 0.68090063f, 0.67850029f, 0.67609268f, 0.67367786f, +0.67125577f, 0.66882652f, 0.66639012f, 0.66394657f, 0.66149592f, +0.65903819f, 0.65657341f, 0.65410155f, 0.65162271f, 0.64913690f, +0.64664418f, 0.64414448f, 0.64163786f, 0.63912445f, 0.63660413f, +0.63407701f, 0.63154310f, 0.62900239f, 0.62645501f, 0.62390089f, +0.62134010f, 0.61877263f, 0.61619854f, 0.61361790f, 0.61103064f, +0.60843682f, 0.60583651f, 0.60322970f, 0.60061646f, 0.59799677f, +0.59537065f, 0.59273821f, 0.59009939f, 0.58745426f, 0.58480281f, +0.58214509f, 0.57948118f, 0.57681108f, 0.57413477f, 0.57145232f, +0.56876373f, 0.56606907f, 0.56336832f, 0.56066155f, 0.55794877f, +0.55523002f, 0.55250537f, 0.54977477f, 0.54703826f, 0.54429591f, +0.54154772f, 0.53879374f, 0.53603399f, 0.53326851f, 0.53049731f, +0.52772039f, 0.52493787f, 0.52214974f, 0.51935595f, 0.51655668f, +0.51375180f, 0.51094145f, 0.50812566f, 0.50530440f, 0.50247771f, +0.49964568f, 0.49680826f, 0.49396557f, 0.49111754f, 0.48826426f, +0.48540577f, 0.48254207f, 0.47967321f, 0.47679919f, 0.47392011f, +0.47103590f, 0.46814668f, 0.46525243f, 0.46235323f, 0.45944905f, +0.45653993f, 0.45362595f, 0.45070711f, 0.44778344f, 0.44485497f, +0.44192174f, 0.43898380f, 0.43604112f, 0.43309379f, 0.43014181f, +0.42718524f, 0.42422408f, 0.42125839f, 0.41828820f, 0.41531351f, +0.41233435f, 0.40935081f, 0.40636289f, 0.40337059f, 0.40037400f, +0.39737311f, 0.39436796f, 0.39135858f, 0.38834500f, 0.38532731f, +0.38230544f, 0.37927949f, 0.37624949f, 0.37321547f, 0.37017745f, +0.36713544f, 0.36408952f, 0.36103970f, 0.35798600f, 0.35492846f, +0.35186714f, 0.34880206f, 0.34573323f, 0.34266070f, 0.33958447f, +0.33650464f, 0.33342120f, 0.33033419f, 0.32724363f, 0.32414958f, +0.32105204f, 0.31795108f, 0.31484672f, 0.31173897f, 0.30862790f, +0.30551350f, 0.30239585f, 0.29927495f, 0.29615086f, 0.29302359f, +0.28989318f, 0.28675964f, 0.28362307f, 0.28048345f, 0.27734083f, +0.27419522f, 0.27104670f, 0.26789525f, 0.26474094f, 0.26158381f, +0.25842386f, 0.25526115f, 0.25209570f, 0.24892756f, 0.24575676f, +0.24258332f, 0.23940729f, 0.23622867f, 0.23304754f, 0.22986393f, +0.22667783f, 0.22348931f, 0.22029841f, 0.21710514f, 0.21390954f, +0.21071166f, 0.20751151f, 0.20430915f, 0.20110460f, 0.19789790f, +0.19468907f, 0.19147816f, 0.18826519f, 0.18505022f, 0.18183327f, +0.17861435f, 0.17539354f, 0.17217083f, 0.16894630f, 0.16571994f, +0.16249183f, 0.15926196f, 0.15603039f, 0.15279715f, 0.14956227f, +0.14632578f, 0.14308774f, 0.13984816f, 0.13660708f, 0.13336454f, +0.13012058f, 0.12687522f, 0.12362850f, 0.12038045f, 0.11713112f, +0.11388054f, 0.11062872f, 0.10737573f, 0.10412160f, 0.10086634f, +0.097609997f, 0.094352618f, 0.091094226f, 0.087834857f, 0.084574550f, +0.081313334f, 0.078051247f, 0.074788325f, 0.071524605f, 0.068260118f, +0.064994894f, 0.061728980f, 0.058462404f, 0.055195201f, 0.051927410f, +0.048659060f, 0.045390189f, 0.042120833f, 0.038851023f, 0.035580799f, +0.032310195f, 0.029039243f, 0.025767982f, 0.022496443f, 0.019224664f, +0.015952680f, 0.012680525f, 0.0094082337f, 0.0061358409f, 0.0028633832f, +-0.00040910527f, -0.0036815894f, -0.0069540343f, -0.010226404f, -0.013498665f, +-0.016770782f, -0.020042717f, -0.023314439f, -0.026585912f, -0.029857099f, +-0.033127967f, -0.036398482f, -0.039668605f, -0.042938303f, -0.046207540f, +-0.049476285f, -0.052744497f, -0.056012146f, -0.059279196f, -0.062545612f, +-0.065811358f, -0.069076397f, -0.072340697f, -0.075604223f, -0.078866936f, +-0.082128808f, -0.085389800f, -0.088649876f, -0.091909006f, -0.095167145f, +-0.098424271f, -0.10168034f, -0.10493532f, -0.10818918f, -0.11144188f, +-0.11469338f, -0.11794366f, -0.12119267f, -0.12444039f, -0.12768677f, +-0.13093179f, -0.13417540f, -0.13741758f, -0.14065829f, -0.14389749f, +-0.14713514f, -0.15037122f, -0.15360570f, -0.15683852f, -0.16006967f, +-0.16329910f, -0.16652679f, -0.16975269f, -0.17297678f, -0.17619900f, +-0.17941935f, -0.18263777f, -0.18585424f, -0.18906870f, -0.19228116f, +-0.19549155f, -0.19869985f, -0.20190603f, -0.20511003f, -0.20831184f, +-0.21151142f, -0.21470875f, -0.21790376f, -0.22109644f, -0.22428675f, +-0.22747467f, -0.23066014f, -0.23384315f, -0.23702365f, -0.24020162f, +-0.24337701f, -0.24654980f, -0.24971995f, -0.25288740f, -0.25605217f, +-0.25921419f, -0.26237345f, -0.26552987f, -0.26868346f, -0.27183419f, +-0.27498198f, -0.27812684f, -0.28126872f, -0.28440759f, -0.28754342f, +-0.29067615f, -0.29380578f, -0.29693225f, -0.30005556f, -0.30317566f, +-0.30629250f, -0.30940607f, -0.31251630f, -0.31562322f, -0.31872672f, +-0.32182685f, -0.32492352f, -0.32801670f, -0.33110636f, -0.33419248f, +-0.33727503f, -0.34035397f, -0.34342924f, -0.34650084f, -0.34956875f, +-0.35263291f, -0.35569328f, -0.35874987f, -0.36180258f, -0.36485144f, +-0.36789638f, -0.37093741f, -0.37397444f, -0.37700745f, -0.38003644f, +-0.38306138f, -0.38608220f, -0.38909888f, -0.39211139f, -0.39511973f, +-0.39812380f, -0.40112361f, -0.40411916f, -0.40711036f, -0.41009718f, +-0.41307965f, -0.41605768f, -0.41903123f, -0.42200032f, -0.42496487f, +-0.42792490f, -0.43088034f, -0.43383113f, -0.43677729f, -0.43971881f, +-0.44265559f, -0.44558764f, -0.44851488f, -0.45143735f, -0.45435500f, +-0.45726776f, -0.46017563f, -0.46307856f, -0.46597654f, -0.46886954f, +-0.47175750f, -0.47464043f, -0.47751826f, -0.48039100f, -0.48325855f, +-0.48612097f, -0.48897815f, -0.49183011f, -0.49467680f, -0.49751821f, +-0.50035429f, -0.50318497f, -0.50601029f, -0.50883019f, -0.51164466f, +-0.51445359f, -0.51725709f, -0.52005500f, -0.52284735f, -0.52563411f, +-0.52841520f, -0.53119069f, -0.53396046f, -0.53672451f, -0.53948283f, +-0.54223537f, -0.54498214f, -0.54772300f, -0.55045801f, -0.55318713f, +-0.55591035f, -0.55862761f, -0.56133890f, -0.56404412f, -0.56674337f, +-0.56943649f, -0.57212353f, -0.57480448f, -0.57747924f, -0.58014780f, +-0.58281022f, -0.58546633f, -0.58811617f, -0.59075975f, -0.59339696f, +-0.59602785f, -0.59865236f, -0.60127044f, -0.60388207f, -0.60648727f, +-0.60908598f, -0.61167812f, -0.61426371f, -0.61684275f, -0.61941516f, +-0.62198097f, -0.62454009f, -0.62709254f, -0.62963831f, -0.63217729f, +-0.63470948f, -0.63723493f, -0.63975352f, -0.64226526f, -0.64477009f, +-0.64726806f, -0.64975911f, -0.65224314f, -0.65472025f, -0.65719032f, +-0.65965337f, -0.66210932f, -0.66455823f, -0.66700000f, -0.66943461f, +-0.67186207f, -0.67428231f, -0.67669535f, -0.67910111f, -0.68149966f, +-0.68389088f, -0.68627477f, -0.68865126f, -0.69102043f, -0.69338220f, +-0.69573659f, -0.69808346f, -0.70042288f, -0.70275480f, -0.70507920f, +-0.70739603f, -0.70970529f, -0.71200693f, -0.71430099f, -0.71658736f, +-0.71886611f, -0.72113711f, -0.72340041f, -0.72565591f, -0.72790372f, +-0.73014367f, -0.73237586f, -0.73460019f, -0.73681659f, -0.73902518f, +-0.74122584f, -0.74341851f, -0.74560326f, -0.74778003f, -0.74994880f, +-0.75210953f, -0.75426215f, -0.75640678f, -0.75854325f, -0.76067162f, +-0.76279181f, -0.76490390f, -0.76700771f, -0.76910341f, -0.77119076f, +-0.77326995f, -0.77534080f, -0.77740335f, -0.77945763f, -0.78150350f, +-0.78354102f, -0.78557014f, -0.78759086f, -0.78960317f, -0.79160696f, +-0.79360235f, -0.79558921f, -0.79756755f, -0.79953730f, -0.80149853f, +-0.80345118f, -0.80539525f, -0.80733067f, -0.80925739f, -0.81117553f, +-0.81308490f, -0.81498563f, -0.81687760f, -0.81876087f, -0.82063532f, +-0.82250100f, -0.82435787f, -0.82620591f, -0.82804507f, -0.82987541f, +-0.83169687f, -0.83350939f, -0.83531296f, -0.83710766f, -0.83889335f, +-0.84067005f, -0.84243774f, -0.84419644f, -0.84594607f, -0.84768665f, +-0.84941816f, -0.85114056f, -0.85285389f, -0.85455805f, -0.85625303f, +-0.85793889f, -0.85961550f, -0.86128294f, -0.86294121f, -0.86459017f, +-0.86622989f, -0.86786032f, -0.86948150f, -0.87109333f, -0.87269586f, +-0.87428904f, -0.87587279f, -0.87744725f, -0.87901229f, -0.88056785f, +-0.88211405f, -0.88365078f, -0.88517809f, -0.88669586f, -0.88820416f, +-0.88970292f, -0.89119220f, -0.89267188f, -0.89414203f, -0.89560264f, +-0.89705360f, -0.89849502f, -0.89992678f, -0.90134889f, -0.90276134f, +-0.90416414f, -0.90555727f, -0.90694070f, -0.90831441f, -0.90967834f, +-0.91103262f, -0.91237706f, -0.91371179f, -0.91503674f, -0.91635185f, +-0.91765714f, -0.91895264f, -0.92023826f, -0.92151409f, -0.92277998f, +-0.92403603f, -0.92528218f, -0.92651838f, -0.92774469f, -0.92896110f, +-0.93016750f, -0.93136400f, -0.93255049f, -0.93372697f, -0.93489349f, +-0.93604994f, -0.93719643f, -0.93833286f, -0.93945926f, -0.94057560f, +-0.94168180f, -0.94277799f, -0.94386405f, -0.94494003f, -0.94600588f, +-0.94706154f, -0.94810712f, -0.94914252f, -0.95016778f, -0.95118284f, +-0.95218778f, -0.95318246f, -0.95416695f, -0.95514119f, -0.95610523f, +-0.95705903f, -0.95800257f, -0.95893586f, -0.95985889f, -0.96077162f, +-0.96167403f, -0.96256620f, -0.96344805f, -0.96431959f, -0.96518075f, +-0.96603161f, -0.96687216f, -0.96770233f, -0.96852213f, -0.96933156f, +-0.97013056f, -0.97091925f, -0.97169751f, -0.97246534f, -0.97322279f, +-0.97396982f, -0.97470641f, -0.97543252f, -0.97614825f, -0.97685349f, +-0.97754824f, -0.97823256f, -0.97890645f, -0.97956979f, -0.98022264f, +-0.98086500f, -0.98149687f, -0.98211825f, -0.98272908f, -0.98332942f, +-0.98391914f, -0.98449844f, -0.98506713f, -0.98562527f, -0.98617285f, +-0.98670989f, -0.98723638f, -0.98775226f, -0.98825759f, -0.98875231f, +-0.98923647f, -0.98971003f, -0.99017298f, -0.99062532f, -0.99106705f, +-0.99149817f, -0.99191868f, -0.99232858f, -0.99272782f, -0.99311644f, +-0.99349445f, -0.99386179f, -0.99421853f, -0.99456459f, -0.99489999f, +-0.99522477f, -0.99553883f, -0.99584228f, -0.99613506f, -0.99641716f, +-0.99668860f, -0.99694937f, -0.99719942f, -0.99743885f, -0.99766755f, +-0.99788558f, -0.99809295f, -0.99828959f, -0.99847561f, -0.99865085f, +-0.99881548f, -0.99896932f, -0.99911255f, -0.99924499f, -0.99936682f, +-0.99947786f, -0.99957830f, -0.99966794f, -0.99974692f, -0.99981517f, +-0.99987274f, -0.99991959f, -0.99995571f, -0.99998116f, -0.99999589f, +0.99999964f, 0.99997288f, 0.99990326f, 0.99979085f, 0.99963558f, +0.99943751f, 0.99919659f, 0.99891287f, 0.99858636f, 0.99821711f, +0.99780506f, 0.99735034f, 0.99685282f, 0.99631262f, 0.99572974f, +0.99510419f, 0.99443603f, 0.99372530f, 0.99297196f, 0.99217612f, +0.99133772f, 0.99045694f, 0.98953366f, 0.98856801f, 0.98756003f, +0.98650974f, 0.98541719f, 0.98428243f, 0.98310548f, 0.98188645f, +0.98062533f, 0.97932225f, 0.97797716f, 0.97659022f, 0.97516143f, +0.97369087f, 0.97217858f, 0.97062469f, 0.96902919f, 0.96739221f, +0.96571374f, 0.96399397f, 0.96223283f, 0.96043050f, 0.95858705f, +0.95670253f, 0.95477700f, 0.95281059f, 0.95080340f, 0.94875544f, +0.94666684f, 0.94453770f, 0.94236809f, 0.94015813f, 0.93790787f, +0.93561745f, 0.93328691f, 0.93091643f, 0.92850608f, 0.92605597f, +0.92356616f, 0.92103678f, 0.91846794f, 0.91585976f, 0.91321236f, +0.91052586f, 0.90780038f, 0.90503591f, 0.90223277f, 0.89939094f, +0.89651060f, 0.89359182f, 0.89063478f, 0.88763964f, 0.88460642f, +0.88153529f, 0.87842643f, 0.87527996f, 0.87209594f, 0.86887461f, +0.86561602f, 0.86232042f, 0.85898781f, 0.85561842f, 0.85221243f, +0.84876984f, 0.84529096f, 0.84177583f, 0.83822471f, 0.83463764f, +0.83101481f, 0.82735640f, 0.82366252f, 0.81993335f, 0.81616908f, +0.81236988f, 0.80853581f, 0.80466717f, 0.80076402f, 0.79682660f, +0.79285502f, 0.78884947f, 0.78481019f, 0.78073722f, 0.77663082f, +0.77249116f, 0.76831841f, 0.76411277f, 0.75987434f, 0.75560343f, +0.75130010f, 0.74696463f, 0.74259710f, 0.73819780f, 0.73376691f, +0.72930455f, 0.72481096f, 0.72028631f, 0.71573079f, 0.71114463f, +0.70652801f, 0.70188117f, 0.69720417f, 0.69249737f, 0.68776089f, +0.68299496f, 0.67819971f, 0.67337549f, 0.66852236f, 0.66364062f, +0.65873051f, 0.65379208f, 0.64882571f, 0.64383155f, 0.63880974f, +0.63376063f, 0.62868434f, 0.62358117f, 0.61845124f, 0.61329484f, +0.60811216f, 0.60290343f, 0.59766883f, 0.59240872f, 0.58712316f, +0.58181250f, 0.57647687f, 0.57111657f, 0.56573176f, 0.56032276f, +0.55488980f, 0.54943299f, 0.54395270f, 0.53844911f, 0.53292239f, +0.52737290f, 0.52180082f, 0.51620632f, 0.51058978f, 0.50495136f, +0.49929130f, 0.49360985f, 0.48790723f, 0.48218375f, 0.47643960f, +0.47067502f, 0.46489030f, 0.45908567f, 0.45326138f, 0.44741765f, +0.44155475f, 0.43567297f, 0.42977250f, 0.42385364f, 0.41791660f, +0.41196167f, 0.40598908f, 0.39999911f, 0.39399201f, 0.38796803f, +0.38192743f, 0.37587047f, 0.36979741f, 0.36370850f, 0.35760403f, +0.35148421f, 0.34534934f, 0.33919969f, 0.33303553f, 0.32685706f, +0.32066461f, 0.31445843f, 0.30823877f, 0.30200592f, 0.29576012f, +0.28950164f, 0.28323078f, 0.27694780f, 0.27065292f, 0.26434645f, +0.25802869f, 0.25169984f, 0.24536023f, 0.23901010f, 0.23264973f, +0.22627939f, 0.21989937f, 0.21350993f, 0.20711134f, 0.20070387f, +0.19428782f, 0.18786344f, 0.18143101f, 0.17499080f, 0.16854310f, +0.16208819f, 0.15562633f, 0.14915779f, 0.14268288f, 0.13620184f, +0.12971498f, 0.12322257f, 0.11672486f, 0.11022217f, 0.10371475f, +0.097202882f, 0.090686858f, 0.084166944f, 0.077643424f, 0.071116582f, +0.064586692f, 0.058054037f, 0.051518895f, 0.044981543f, 0.038442269f, +0.031901345f, 0.025359053f, 0.018815678f, 0.012271495f, 0.0057267868f, +-0.00081816671f, -0.0073630852f, -0.013907688f, -0.020451695f, -0.026994826f, +-0.033536803f, -0.040077340f, -0.046616159f, -0.053152986f, -0.059687532f, +-0.066219524f, -0.072748676f, -0.079274714f, -0.085797355f, -0.092316322f, +-0.098831341f, -0.10534211f, -0.11184838f, -0.11834986f, -0.12484626f, +-0.13133731f, -0.13782275f, -0.14430228f, -0.15077563f, -0.15724251f, +-0.16370267f, -0.17015581f, -0.17660165f, -0.18303993f, -0.18947038f, +-0.19589271f, -0.20230664f, -0.20871192f, -0.21510825f, -0.22149536f, +-0.22787298f, -0.23424086f, -0.24059868f, -0.24694622f, -0.25328314f, +-0.25960925f, -0.26592422f, -0.27222782f, -0.27851975f, -0.28479972f, +-0.29106751f, -0.29732284f, -0.30356544f, -0.30979502f, -0.31601134f, +-0.32221413f, -0.32840309f, -0.33457801f, -0.34073856f, -0.34688455f, +-0.35301566f, -0.35913166f, -0.36523229f, -0.37131724f, -0.37738630f, +-0.38343921f, -0.38947567f, -0.39549544f, -0.40149832f, -0.40748394f, +-0.41345215f, -0.41940263f, -0.42533514f, -0.43124944f, -0.43714526f, +-0.44302234f, -0.44888046f, -0.45471936f, -0.46053877f, -0.46633846f, +-0.47211814f, -0.47787762f, -0.48361665f, -0.48933494f, -0.49503228f, +-0.50070840f, -0.50636309f, -0.51199609f, -0.51760709f, -0.52319598f, +-0.52876246f, -0.53430629f, -0.53982723f, -0.54532504f, -0.55079949f, +-0.55625033f, -0.56167740f, -0.56708032f, -0.57245898f, -0.57781315f, +-0.58314258f, -0.58844697f, -0.59372622f, -0.59897995f, -0.60420811f, +-0.60941035f, -0.61458647f, -0.61973625f, -0.62485951f, -0.62995601f, +-0.63502556f, -0.64006782f, -0.64508271f, -0.65007001f, -0.65502942f, +-0.65996075f, -0.66486382f, -0.66973841f, -0.67458433f, -0.67940134f, +-0.68418926f, -0.68894786f, -0.69367695f, -0.69837630f, -0.70304573f, +-0.70768511f, -0.71229410f, -0.71687263f, -0.72142041f, -0.72593731f, +-0.73042315f, -0.73487765f, -0.73930067f, -0.74369204f, -0.74805158f, +-0.75237900f, -0.75667429f, -0.76093709f, -0.76516730f, -0.76936477f, +-0.77352923f, -0.77766061f, -0.78175867f, -0.78582323f, -0.78985411f, +-0.79385114f, -0.79781419f, -0.80174309f, -0.80563760f, -0.80949765f, +-0.81332302f, -0.81711352f, -0.82086903f, -0.82458937f, -0.82827437f, +-0.83192390f, -0.83553779f, -0.83911592f, -0.84265804f, -0.84616417f, +-0.84963393f, -0.85306740f, -0.85646427f, -0.85982448f, -0.86314780f, +-0.86643422f, -0.86968350f, -0.87289548f, -0.87607014f, -0.87920725f, +-0.88230664f, -0.88536829f, -0.88839203f, -0.89137769f, -0.89432514f, +-0.89723432f, -0.90010506f, -0.90293723f, -0.90573072f, -0.90848541f, +-0.91120118f, -0.91387796f, -0.91651553f, -0.91911387f, -0.92167282f, +-0.92419231f, -0.92667222f, -0.92911243f, -0.93151283f, -0.93387336f, +-0.93619382f, -0.93847424f, -0.94071442f, -0.94291431f, -0.94507378f, +-0.94719279f, -0.94927126f, -0.95130903f, -0.95330608f, -0.95526224f, +-0.95717752f, -0.95905179f, -0.96088499f, -0.96267700f, -0.96442777f, +-0.96613729f, -0.96780539f, -0.96943200f, -0.97101706f, -0.97256058f, +-0.97406244f, -0.97552258f, -0.97694093f, -0.97831738f, -0.97965199f, +-0.98094457f, -0.98219514f, -0.98340368f, -0.98457009f, -0.98569429f, +-0.98677629f, -0.98781598f, -0.98881340f, -0.98976845f, -0.99068111f, +-0.99155134f, -0.99237907f, -0.99316430f, -0.99390697f, -0.99460709f, +-0.99526459f, -0.99587947f, -0.99645168f, -0.99698120f, -0.99746799f, +-0.99791211f, -0.99831343f, -0.99867201f, -0.99898779f, -0.99926084f, +-0.99949104f, -0.99967843f, -0.99982297f, -0.99992472f, -0.99998361f, +0.99999869f, 0.99989158f, 0.99961317f, 0.99916345f, 0.99854255f, +0.99775058f, 0.99678761f, 0.99565387f, 0.99434954f, 0.99287480f, +0.99122995f, 0.98941529f, 0.98743105f, 0.98527765f, 0.98295540f, +0.98046476f, 0.97780609f, 0.97497988f, 0.97198665f, 0.96882683f, +0.96550101f, 0.96200979f, 0.95835376f, 0.95453346f, 0.95054960f, +0.94640291f, 0.94209403f, 0.93762374f, 0.93299282f, 0.92820197f, +0.92325211f, 0.91814411f, 0.91287869f, 0.90745693f, 0.90187967f, +0.89614785f, 0.89026248f, 0.88422459f, 0.87803519f, 0.87169534f, +0.86520612f, 0.85856867f, 0.85178405f, 0.84485358f, 0.83777827f, +0.83055943f, 0.82319832f, 0.81569612f, 0.80805415f, 0.80027372f, +0.79235619f, 0.78430289f, 0.77611518f, 0.76779449f, 0.75934225f, +0.75075996f, 0.74204898f, 0.73321080f, 0.72424710f, 0.71515924f, +0.70594883f, 0.69661748f, 0.68716675f, 0.67759830f, 0.66791373f, +0.65811473f, 0.64820296f, 0.63818014f, 0.62804794f, 0.61780810f, +0.60746247f, 0.59701276f, 0.58646071f, 0.57580817f, 0.56505698f, +0.55420899f, 0.54326600f, 0.53222996f, 0.52110273f, 0.50988621f, +0.49858227f, 0.48719296f, 0.47572014f, 0.46416581f, 0.45253196f, +0.44082057f, 0.42903364f, 0.41717321f, 0.40524128f, 0.39323992f, +0.38117120f, 0.36903715f, 0.35683987f, 0.34458145f, 0.33226398f, +0.31988961f, 0.30746040f, 0.29497850f, 0.28244606f, 0.26986524f, +0.25723818f, 0.24456702f, 0.23185398f, 0.21910121f, 0.20631088f, +0.19348522f, 0.18062639f, 0.16773662f, 0.15481812f, 0.14187308f, +0.12890373f, 0.11591230f, 0.10290100f, 0.089872077f, 0.076827750f, +0.063770257f, 0.050701842f, 0.037624735f, 0.024541186f, 0.011453429f, +-0.0016362892f, -0.014725727f, -0.027812643f, -0.040894791f, -0.053969935f, +-0.067035832f, -0.080090240f, -0.093130924f, -0.10615565f, -0.11916219f, +-0.13214831f, -0.14511178f, -0.15805040f, -0.17096193f, -0.18384418f, +-0.19669491f, -0.20951195f, -0.22229309f, -0.23503613f, -0.24773891f, +-0.26039925f, -0.27301496f, -0.28558388f, -0.29810387f, -0.31057280f, +-0.32298848f, -0.33534884f, -0.34765175f, -0.35989508f, -0.37207675f, +-0.38419467f, -0.39624676f, -0.40823093f, -0.42014518f, -0.43198743f, +-0.44375566f, -0.45544785f, -0.46706200f, -0.47859612f, -0.49004826f, +-0.50141639f, -0.51269865f, -0.52389306f, -0.53499764f, -0.54601061f, +-0.55693001f, -0.56775403f, -0.57848072f, -0.58910829f, -0.59963489f, +-0.61005878f, -0.62037814f, -0.63059121f, -0.64069623f, -0.65069145f, +-0.66057515f, -0.67034572f, -0.68000144f, -0.68954057f, -0.69896162f, +-0.70826286f, -0.71744281f, -0.72649974f, -0.73543227f, -0.74423873f, +-0.75291771f, -0.76146764f, -0.76988715f, -0.77817470f, -0.78632891f, +-0.79434842f, -0.80223179f, -0.80997771f, -0.81758487f, -0.82505190f, +-0.83237761f, -0.83956063f, -0.84659988f, -0.85349399f, -0.86024189f, +-0.86684239f, -0.87329435f, -0.87959671f, -0.88574833f, -0.89174819f, +-0.89759529f, -0.90328854f, -0.90882701f, -0.91420978f, -0.91943592f, +-0.92450452f, -0.92941469f, -0.93416560f, -0.93875647f, -0.94318646f, +-0.94745487f, -0.95156091f, -0.95550388f, -0.95928317f, -0.96289814f, +-0.96634805f, -0.96963239f, -0.97275060f, -0.97570217f, -0.97848648f, +-0.98110318f, -0.98355180f, -0.98583186f, -0.98794299f, -0.98988485f, +-0.99165714f, -0.99325943f, -0.99469161f, -0.99595332f, -0.99704438f, +-0.99796462f, -0.99871385f, -0.99929196f, -0.99969882f, -0.99993443f, +0.99999464f, 0.99956632f, 0.99845290f, 0.99665523f, 0.99417448f, +0.99101239f, 0.98717111f, 0.98265326f, 0.97746199f, 0.97160077f, +0.96507365f, 0.95788515f, 0.95004016f, 0.94154406f, 0.93240267f, +0.92262226f, 0.91220951f, 0.90117162f, 0.88951606f, 0.87725091f, +0.86438453f, 0.85092574f, 0.83688372f, 0.82226819f, 0.80708915f, +0.79135692f, 0.77508235f, 0.75827658f, 0.74095112f, 0.72311783f, +0.70478898f, 0.68597710f, 0.66669506f, 0.64695615f, 0.62677377f, +0.60616189f, 0.58513457f, 0.56370622f, 0.54189157f, 0.51970547f, +0.49716324f, 0.47428027f, 0.45107225f, 0.42755505f, 0.40374488f, +0.37965798f, 0.35531086f, 0.33072025f, 0.30590299f, 0.28087607f, +0.25565663f, 0.23026201f, 0.20470956f, 0.17901683f, 0.15320139f, +0.12728097f, 0.10127331f, 0.075196236f, 0.049067631f, 0.022905400f, +-0.0032725304f, -0.029448219f, -0.055603724f, -0.081721120f, -0.10778251f, +-0.13377003f, -0.15966587f, -0.18545228f, -0.21111161f, -0.23662624f, +-0.26197869f, -0.28715160f, -0.31212771f, -0.33688989f, -0.36142120f, +-0.38570482f, -0.40972409f, -0.43346253f, -0.45690393f, -0.48003218f, +-0.50283146f, -0.52528608f, -0.54738069f, -0.56910020f, -0.59042966f, +-0.61135447f, -0.63186026f, -0.65193301f, -0.67155898f, -0.69072473f, +-0.70941705f, -0.72762316f, -0.74533063f, -0.76252723f, -0.77920127f, +-0.79534131f, -0.81093621f, -0.82597536f, -0.84044844f, -0.85434550f, +-0.86765707f, -0.88037395f, -0.89248747f, -0.90398932f, -0.91487163f, +-0.92512697f, -0.93474823f, -0.94372886f, -0.95206273f, -0.95974404f, +-0.96676767f, -0.97312868f, -0.97882277f, -0.98384601f, -0.98819500f, +-0.99186671f, -0.99485862f, -0.99716878f, -0.99879545f, -0.99973762f, +}; +#endif + +static const CELTMode mode48000_960_120 = { +48000, /* Fs */ +120, /* overlap */ +21, /* nbEBands */ +21, /* effEBands */ +{0.85000610f, 0.0000000f, 1.0000000f, 1.0000000f, }, /* preemph */ +eband5ms, /* eBands */ +3, /* maxLM */ +8, /* nbShortMdcts */ +120, /* shortMdctSize */ +11, /* nbAllocVectors */ +band_allocation, /* allocVectors */ +logN400, /* logN */ +window120, /* window */ +{1920, 3, {&fft_state48000_960_0, &fft_state48000_960_1, &fft_state48000_960_2, &fft_state48000_960_3, }, mdct_twiddles960}, /* mdct */ +{392, cache_index50, cache_bits50, cache_caps50}, /* cache */ +}; + +/* List of all the available modes */ +#define TOTAL_MODES 1 +static const CELTMode * const static_mode_list[TOTAL_MODES] = { +&mode48000_960_120, +}; diff --git a/port/third_party/opus/celt/static_modes_float_arm_ne10.h b/port/third_party/opus/celt/static_modes_float_arm_ne10.h new file mode 100644 index 00000000..66e1abb1 --- /dev/null +++ b/port/third_party/opus/celt/static_modes_float_arm_ne10.h @@ -0,0 +1,404 @@ +/* The contents of this file was automatically generated by + * dump_mode_arm_ne10.c with arguments: 48000 960 + * It contains static definitions for some pre-defined modes. */ +#include + +#ifndef NE10_FFT_PARAMS48000_960 +#define NE10_FFT_PARAMS48000_960 +static const ne10_int32_t ne10_factors_480[64] = { +4, 40, 4, 30, 2, 15, 5, 3, 3, 1, 1, 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, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, }; +static const ne10_int32_t ne10_factors_240[64] = { +3, 20, 4, 15, 5, 3, 3, 1, 1, 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, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, }; +static const ne10_int32_t ne10_factors_120[64] = { +3, 10, 2, 15, 5, 3, 3, 1, 1, 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, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, }; +static const ne10_int32_t ne10_factors_60[64] = { +2, 5, 5, 3, 3, 1, 1, 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, 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, }; +static const ne10_fft_cpx_float32_t ne10_twiddles_480[480] = { +{1.0000000f,0.0000000f}, {1.0000000f,-0.0000000f}, {1.0000000f,-0.0000000f}, +{1.0000000f,-0.0000000f}, {0.91354543f,-0.40673664f}, {0.66913056f,-0.74314487f}, +{1.0000000f,-0.0000000f}, {0.66913056f,-0.74314487f}, {-0.10452851f,-0.99452192f}, +{1.0000000f,-0.0000000f}, {0.30901697f,-0.95105654f}, {-0.80901700f,-0.58778518f}, +{1.0000000f,-0.0000000f}, {-0.10452851f,-0.99452192f}, {-0.97814757f,0.20791179f}, +{1.0000000f,-0.0000000f}, {0.97814763f,-0.20791170f}, {0.91354543f,-0.40673664f}, +{0.80901700f,-0.58778524f}, {0.66913056f,-0.74314487f}, {0.49999997f,-0.86602545f}, +{0.30901697f,-0.95105654f}, {0.10452842f,-0.99452192f}, {-0.10452851f,-0.99452192f}, +{-0.30901703f,-0.95105648f}, {-0.50000006f,-0.86602533f}, {-0.66913068f,-0.74314475f}, +{-0.80901700f,-0.58778518f}, {-0.91354549f,-0.40673658f}, {-0.97814763f,-0.20791161f}, +{1.0000000f,-0.0000000f}, {0.99862951f,-0.052335959f}, {0.99452192f,-0.10452846f}, +{0.98768836f,-0.15643448f}, {0.97814763f,-0.20791170f}, {0.96592581f,-0.25881904f}, +{0.95105648f,-0.30901700f}, {0.93358040f,-0.35836795f}, {0.91354543f,-0.40673664f}, +{0.89100653f,-0.45399052f}, {0.86602545f,-0.50000000f}, {0.83867055f,-0.54463905f}, +{0.80901700f,-0.58778524f}, {0.77714598f,-0.62932038f}, {0.74314475f,-0.66913062f}, +{0.70710677f,-0.70710683f}, {0.66913056f,-0.74314487f}, {0.62932038f,-0.77714598f}, +{0.58778524f,-0.80901700f}, {0.54463899f,-0.83867055f}, {0.49999997f,-0.86602545f}, +{0.45399052f,-0.89100653f}, {0.40673661f,-0.91354549f}, {0.35836786f,-0.93358046f}, +{0.30901697f,-0.95105654f}, {0.25881907f,-0.96592581f}, {0.20791166f,-0.97814763f}, +{0.15643437f,-0.98768836f}, {0.10452842f,-0.99452192f}, {0.052335974f,-0.99862951f}, +{1.0000000f,-0.0000000f}, {0.99452192f,-0.10452846f}, {0.97814763f,-0.20791170f}, +{0.95105648f,-0.30901700f}, {0.91354543f,-0.40673664f}, {0.86602545f,-0.50000000f}, +{0.80901700f,-0.58778524f}, {0.74314475f,-0.66913062f}, {0.66913056f,-0.74314487f}, +{0.58778524f,-0.80901700f}, {0.49999997f,-0.86602545f}, {0.40673661f,-0.91354549f}, +{0.30901697f,-0.95105654f}, {0.20791166f,-0.97814763f}, {0.10452842f,-0.99452192f}, +{-4.3711388e-08f,-1.0000000f}, {-0.10452851f,-0.99452192f}, {-0.20791174f,-0.97814757f}, +{-0.30901703f,-0.95105648f}, {-0.40673670f,-0.91354543f}, {-0.50000006f,-0.86602533f}, +{-0.58778518f,-0.80901700f}, {-0.66913068f,-0.74314475f}, {-0.74314493f,-0.66913044f}, +{-0.80901700f,-0.58778518f}, {-0.86602539f,-0.50000006f}, {-0.91354549f,-0.40673658f}, +{-0.95105654f,-0.30901679f}, {-0.97814763f,-0.20791161f}, {-0.99452192f,-0.10452849f}, +{1.0000000f,-0.0000000f}, {0.98768836f,-0.15643448f}, {0.95105648f,-0.30901700f}, +{0.89100653f,-0.45399052f}, {0.80901700f,-0.58778524f}, {0.70710677f,-0.70710683f}, +{0.58778524f,-0.80901700f}, {0.45399052f,-0.89100653f}, {0.30901697f,-0.95105654f}, +{0.15643437f,-0.98768836f}, {-4.3711388e-08f,-1.0000000f}, {-0.15643445f,-0.98768836f}, +{-0.30901703f,-0.95105648f}, {-0.45399061f,-0.89100647f}, {-0.58778518f,-0.80901700f}, +{-0.70710677f,-0.70710677f}, {-0.80901700f,-0.58778518f}, {-0.89100659f,-0.45399037f}, +{-0.95105654f,-0.30901679f}, {-0.98768836f,-0.15643445f}, {-1.0000000f,8.7422777e-08f}, +{-0.98768830f,0.15643461f}, {-0.95105654f,0.30901697f}, {-0.89100653f,0.45399055f}, +{-0.80901694f,0.58778536f}, {-0.70710665f,0.70710689f}, {-0.58778507f,0.80901712f}, +{-0.45399022f,0.89100665f}, {-0.30901709f,0.95105648f}, {-0.15643452f,0.98768830f}, +{1.0000000f,-0.0000000f}, {0.99991435f,-0.013089596f}, {0.99965733f,-0.026176950f}, +{0.99922901f,-0.039259817f}, {0.99862951f,-0.052335959f}, {0.99785894f,-0.065403134f}, +{0.99691731f,-0.078459099f}, {0.99580491f,-0.091501623f}, {0.99452192f,-0.10452846f}, +{0.99306846f,-0.11753740f}, {0.99144489f,-0.13052620f}, {0.98965138f,-0.14349262f}, +{0.98768836f,-0.15643448f}, {0.98555607f,-0.16934951f}, {0.98325491f,-0.18223552f}, +{0.98078525f,-0.19509032f}, {0.97814763f,-0.20791170f}, {0.97534233f,-0.22069745f}, +{0.97236991f,-0.23344538f}, {0.96923089f,-0.24615330f}, {0.96592581f,-0.25881904f}, +{0.96245521f,-0.27144045f}, {0.95881975f,-0.28401536f}, {0.95501995f,-0.29654160f}, +{0.95105648f,-0.30901700f}, {0.94693011f,-0.32143945f}, {0.94264150f,-0.33380687f}, +{0.93819129f,-0.34611708f}, {0.93358040f,-0.35836795f}, {0.92880952f,-0.37055743f}, +{0.92387956f,-0.38268346f}, {0.91879117f,-0.39474389f}, {0.91354543f,-0.40673664f}, +{0.90814316f,-0.41865975f}, {0.90258527f,-0.43051112f}, {0.89687270f,-0.44228873f}, +{0.89100653f,-0.45399052f}, {0.88498765f,-0.46561453f}, {0.87881708f,-0.47715878f}, +{0.87249601f,-0.48862126f}, {0.86602545f,-0.50000000f}, {0.85940641f,-0.51129311f}, +{0.85264015f,-0.52249855f}, {0.84572786f,-0.53361452f}, {0.83867055f,-0.54463905f}, +{0.83146960f,-0.55557024f}, {0.82412618f,-0.56640625f}, {0.81664151f,-0.57714522f}, +{0.80901700f,-0.58778524f}, {0.80125380f,-0.59832460f}, {0.79335332f,-0.60876143f}, +{0.78531694f,-0.61909395f}, {0.77714598f,-0.62932038f}, {0.76884180f,-0.63943899f}, +{0.76040596f,-0.64944810f}, {0.75183982f,-0.65934587f}, {0.74314475f,-0.66913062f}, +{0.73432249f,-0.67880076f}, {0.72537434f,-0.68835455f}, {0.71630192f,-0.69779050f}, +{0.70710677f,-0.70710683f}, {0.69779044f,-0.71630198f}, {0.68835455f,-0.72537440f}, +{0.67880070f,-0.73432255f}, {0.66913056f,-0.74314487f}, {0.65934581f,-0.75183982f}, +{0.64944804f,-0.76040596f}, {0.63943899f,-0.76884186f}, {0.62932038f,-0.77714598f}, +{0.61909395f,-0.78531694f}, {0.60876137f,-0.79335338f}, {0.59832460f,-0.80125386f}, +{0.58778524f,-0.80901700f}, {0.57714516f,-0.81664151f}, {0.56640625f,-0.82412618f}, +{0.55557019f,-0.83146960f}, {0.54463899f,-0.83867055f}, {0.53361452f,-0.84572786f}, +{0.52249849f,-0.85264015f}, {0.51129311f,-0.85940641f}, {0.49999997f,-0.86602545f}, +{0.48862118f,-0.87249601f}, {0.47715876f,-0.87881708f}, {0.46561447f,-0.88498765f}, +{0.45399052f,-0.89100653f}, {0.44228867f,-0.89687276f}, {0.43051103f,-0.90258533f}, +{0.41865975f,-0.90814316f}, {0.40673661f,-0.91354549f}, {0.39474380f,-0.91879129f}, +{0.38268343f,-0.92387956f}, {0.37055740f,-0.92880958f}, {0.35836786f,-0.93358046f}, +{0.34611705f,-0.93819135f}, {0.33380681f,-0.94264150f}, {0.32143947f,-0.94693011f}, +{0.30901697f,-0.95105654f}, {0.29654151f,-0.95501995f}, {0.28401533f,-0.95881975f}, +{0.27144039f,-0.96245527f}, {0.25881907f,-0.96592581f}, {0.24615327f,-0.96923089f}, +{0.23344530f,-0.97236991f}, {0.22069745f,-0.97534233f}, {0.20791166f,-0.97814763f}, +{0.19509023f,-0.98078531f}, {0.18223552f,-0.98325491f}, {0.16934945f,-0.98555607f}, +{0.15643437f,-0.98768836f}, {0.14349259f,-0.98965138f}, {0.13052613f,-0.99144489f}, +{0.11753740f,-0.99306846f}, {0.10452842f,-0.99452192f}, {0.091501534f,-0.99580491f}, +{0.078459084f,-0.99691731f}, {0.065403074f,-0.99785894f}, {0.052335974f,-0.99862951f}, +{0.039259788f,-0.99922901f}, {0.026176875f,-0.99965733f}, {0.013089597f,-0.99991435f}, +{1.0000000f,-0.0000000f}, {0.99965733f,-0.026176950f}, {0.99862951f,-0.052335959f}, +{0.99691731f,-0.078459099f}, {0.99452192f,-0.10452846f}, {0.99144489f,-0.13052620f}, +{0.98768836f,-0.15643448f}, {0.98325491f,-0.18223552f}, {0.97814763f,-0.20791170f}, +{0.97236991f,-0.23344538f}, {0.96592581f,-0.25881904f}, {0.95881975f,-0.28401536f}, +{0.95105648f,-0.30901700f}, {0.94264150f,-0.33380687f}, {0.93358040f,-0.35836795f}, +{0.92387956f,-0.38268346f}, {0.91354543f,-0.40673664f}, {0.90258527f,-0.43051112f}, +{0.89100653f,-0.45399052f}, {0.87881708f,-0.47715878f}, {0.86602545f,-0.50000000f}, +{0.85264015f,-0.52249855f}, {0.83867055f,-0.54463905f}, {0.82412618f,-0.56640625f}, +{0.80901700f,-0.58778524f}, {0.79335332f,-0.60876143f}, {0.77714598f,-0.62932038f}, +{0.76040596f,-0.64944810f}, {0.74314475f,-0.66913062f}, {0.72537434f,-0.68835455f}, +{0.70710677f,-0.70710683f}, {0.68835455f,-0.72537440f}, {0.66913056f,-0.74314487f}, +{0.64944804f,-0.76040596f}, {0.62932038f,-0.77714598f}, {0.60876137f,-0.79335338f}, +{0.58778524f,-0.80901700f}, {0.56640625f,-0.82412618f}, {0.54463899f,-0.83867055f}, +{0.52249849f,-0.85264015f}, {0.49999997f,-0.86602545f}, {0.47715876f,-0.87881708f}, +{0.45399052f,-0.89100653f}, {0.43051103f,-0.90258533f}, {0.40673661f,-0.91354549f}, +{0.38268343f,-0.92387956f}, {0.35836786f,-0.93358046f}, {0.33380681f,-0.94264150f}, +{0.30901697f,-0.95105654f}, {0.28401533f,-0.95881975f}, {0.25881907f,-0.96592581f}, +{0.23344530f,-0.97236991f}, {0.20791166f,-0.97814763f}, {0.18223552f,-0.98325491f}, +{0.15643437f,-0.98768836f}, {0.13052613f,-0.99144489f}, {0.10452842f,-0.99452192f}, +{0.078459084f,-0.99691731f}, {0.052335974f,-0.99862951f}, {0.026176875f,-0.99965733f}, +{-4.3711388e-08f,-1.0000000f}, {-0.026176963f,-0.99965733f}, {-0.052336060f,-0.99862951f}, +{-0.078459173f,-0.99691731f}, {-0.10452851f,-0.99452192f}, {-0.13052621f,-0.99144489f}, +{-0.15643445f,-0.98768836f}, {-0.18223560f,-0.98325491f}, {-0.20791174f,-0.97814757f}, +{-0.23344538f,-0.97236991f}, {-0.25881916f,-0.96592581f}, {-0.28401542f,-0.95881969f}, +{-0.30901703f,-0.95105648f}, {-0.33380687f,-0.94264150f}, {-0.35836795f,-0.93358040f}, +{-0.38268352f,-0.92387950f}, {-0.40673670f,-0.91354543f}, {-0.43051112f,-0.90258527f}, +{-0.45399061f,-0.89100647f}, {-0.47715873f,-0.87881708f}, {-0.50000006f,-0.86602533f}, +{-0.52249867f,-0.85264009f}, {-0.54463905f,-0.83867055f}, {-0.56640631f,-0.82412612f}, +{-0.58778518f,-0.80901700f}, {-0.60876143f,-0.79335332f}, {-0.62932050f,-0.77714586f}, +{-0.64944804f,-0.76040596f}, {-0.66913068f,-0.74314475f}, {-0.68835467f,-0.72537428f}, +{-0.70710677f,-0.70710677f}, {-0.72537446f,-0.68835449f}, {-0.74314493f,-0.66913044f}, +{-0.76040596f,-0.64944804f}, {-0.77714604f,-0.62932026f}, {-0.79335332f,-0.60876143f}, +{-0.80901700f,-0.58778518f}, {-0.82412624f,-0.56640613f}, {-0.83867055f,-0.54463899f}, +{-0.85264021f,-0.52249849f}, {-0.86602539f,-0.50000006f}, {-0.87881714f,-0.47715873f}, +{-0.89100659f,-0.45399037f}, {-0.90258527f,-0.43051112f}, {-0.91354549f,-0.40673658f}, +{-0.92387956f,-0.38268328f}, {-0.93358040f,-0.35836792f}, {-0.94264150f,-0.33380675f}, +{-0.95105654f,-0.30901679f}, {-0.95881975f,-0.28401530f}, {-0.96592587f,-0.25881892f}, +{-0.97236991f,-0.23344538f}, {-0.97814763f,-0.20791161f}, {-0.98325491f,-0.18223536f}, +{-0.98768836f,-0.15643445f}, {-0.99144489f,-0.13052608f}, {-0.99452192f,-0.10452849f}, +{-0.99691737f,-0.078459039f}, {-0.99862957f,-0.052335810f}, {-0.99965733f,-0.026176952f}, +{1.0000000f,-0.0000000f}, {0.99922901f,-0.039259817f}, {0.99691731f,-0.078459099f}, +{0.99306846f,-0.11753740f}, {0.98768836f,-0.15643448f}, {0.98078525f,-0.19509032f}, +{0.97236991f,-0.23344538f}, {0.96245521f,-0.27144045f}, {0.95105648f,-0.30901700f}, +{0.93819129f,-0.34611708f}, {0.92387956f,-0.38268346f}, {0.90814316f,-0.41865975f}, +{0.89100653f,-0.45399052f}, {0.87249601f,-0.48862126f}, {0.85264015f,-0.52249855f}, +{0.83146960f,-0.55557024f}, {0.80901700f,-0.58778524f}, {0.78531694f,-0.61909395f}, +{0.76040596f,-0.64944810f}, {0.73432249f,-0.67880076f}, {0.70710677f,-0.70710683f}, +{0.67880070f,-0.73432255f}, {0.64944804f,-0.76040596f}, {0.61909395f,-0.78531694f}, +{0.58778524f,-0.80901700f}, {0.55557019f,-0.83146960f}, {0.52249849f,-0.85264015f}, +{0.48862118f,-0.87249601f}, {0.45399052f,-0.89100653f}, {0.41865975f,-0.90814316f}, +{0.38268343f,-0.92387956f}, {0.34611705f,-0.93819135f}, {0.30901697f,-0.95105654f}, +{0.27144039f,-0.96245527f}, {0.23344530f,-0.97236991f}, {0.19509023f,-0.98078531f}, +{0.15643437f,-0.98768836f}, {0.11753740f,-0.99306846f}, {0.078459084f,-0.99691731f}, +{0.039259788f,-0.99922901f}, {-4.3711388e-08f,-1.0000000f}, {-0.039259877f,-0.99922901f}, +{-0.078459173f,-0.99691731f}, {-0.11753749f,-0.99306846f}, {-0.15643445f,-0.98768836f}, +{-0.19509032f,-0.98078525f}, {-0.23344538f,-0.97236991f}, {-0.27144048f,-0.96245521f}, +{-0.30901703f,-0.95105648f}, {-0.34611711f,-0.93819129f}, {-0.38268352f,-0.92387950f}, +{-0.41865984f,-0.90814310f}, {-0.45399061f,-0.89100647f}, {-0.48862135f,-0.87249595f}, +{-0.52249867f,-0.85264009f}, {-0.55557036f,-0.83146954f}, {-0.58778518f,-0.80901700f}, +{-0.61909389f,-0.78531694f}, {-0.64944804f,-0.76040596f}, {-0.67880076f,-0.73432249f}, +{-0.70710677f,-0.70710677f}, {-0.73432249f,-0.67880070f}, {-0.76040596f,-0.64944804f}, +{-0.78531694f,-0.61909389f}, {-0.80901700f,-0.58778518f}, {-0.83146966f,-0.55557019f}, +{-0.85264021f,-0.52249849f}, {-0.87249607f,-0.48862115f}, {-0.89100659f,-0.45399037f}, +{-0.90814322f,-0.41865960f}, {-0.92387956f,-0.38268328f}, {-0.93819135f,-0.34611690f}, +{-0.95105654f,-0.30901679f}, {-0.96245521f,-0.27144048f}, {-0.97236991f,-0.23344538f}, +{-0.98078531f,-0.19509031f}, {-0.98768836f,-0.15643445f}, {-0.99306846f,-0.11753736f}, +{-0.99691737f,-0.078459039f}, {-0.99922901f,-0.039259743f}, {-1.0000000f,8.7422777e-08f}, +{-0.99922901f,0.039259918f}, {-0.99691731f,0.078459218f}, {-0.99306846f,0.11753753f}, +{-0.98768830f,0.15643461f}, {-0.98078525f,0.19509049f}, {-0.97236985f,0.23344554f}, +{-0.96245515f,0.27144065f}, {-0.95105654f,0.30901697f}, {-0.93819135f,0.34611705f}, +{-0.92387956f,0.38268346f}, {-0.90814316f,0.41865975f}, {-0.89100653f,0.45399055f}, +{-0.87249601f,0.48862129f}, {-0.85264015f,0.52249861f}, {-0.83146960f,0.55557030f}, +{-0.80901694f,0.58778536f}, {-0.78531688f,0.61909401f}, {-0.76040590f,0.64944816f}, +{-0.73432243f,0.67880082f}, {-0.70710665f,0.70710689f}, {-0.67880058f,0.73432261f}, +{-0.64944792f,0.76040608f}, {-0.61909378f,0.78531706f}, {-0.58778507f,0.80901712f}, +{-0.55557001f,0.83146977f}, {-0.52249837f,0.85264033f}, {-0.48862100f,0.87249613f}, +{-0.45399022f,0.89100665f}, {-0.41865945f,0.90814328f}, {-0.38268313f,0.92387968f}, +{-0.34611672f,0.93819147f}, {-0.30901709f,0.95105648f}, {-0.27144054f,0.96245521f}, +{-0.23344545f,0.97236991f}, {-0.19509038f,0.98078525f}, {-0.15643452f,0.98768830f}, +{-0.11753743f,0.99306846f}, {-0.078459114f,0.99691731f}, {-0.039259821f,0.99922901f}, +}; +static const ne10_fft_cpx_float32_t ne10_twiddles_240[240] = { +{1.0000000f,0.0000000f}, {1.0000000f,-0.0000000f}, {1.0000000f,-0.0000000f}, +{1.0000000f,-0.0000000f}, {0.91354543f,-0.40673664f}, {0.66913056f,-0.74314487f}, +{1.0000000f,-0.0000000f}, {0.66913056f,-0.74314487f}, {-0.10452851f,-0.99452192f}, +{1.0000000f,-0.0000000f}, {0.30901697f,-0.95105654f}, {-0.80901700f,-0.58778518f}, +{1.0000000f,-0.0000000f}, {-0.10452851f,-0.99452192f}, {-0.97814757f,0.20791179f}, +{1.0000000f,-0.0000000f}, {0.99452192f,-0.10452846f}, {0.97814763f,-0.20791170f}, +{0.95105648f,-0.30901700f}, {0.91354543f,-0.40673664f}, {0.86602545f,-0.50000000f}, +{0.80901700f,-0.58778524f}, {0.74314475f,-0.66913062f}, {0.66913056f,-0.74314487f}, +{0.58778524f,-0.80901700f}, {0.49999997f,-0.86602545f}, {0.40673661f,-0.91354549f}, +{0.30901697f,-0.95105654f}, {0.20791166f,-0.97814763f}, {0.10452842f,-0.99452192f}, +{1.0000000f,-0.0000000f}, {0.97814763f,-0.20791170f}, {0.91354543f,-0.40673664f}, +{0.80901700f,-0.58778524f}, {0.66913056f,-0.74314487f}, {0.49999997f,-0.86602545f}, +{0.30901697f,-0.95105654f}, {0.10452842f,-0.99452192f}, {-0.10452851f,-0.99452192f}, +{-0.30901703f,-0.95105648f}, {-0.50000006f,-0.86602533f}, {-0.66913068f,-0.74314475f}, +{-0.80901700f,-0.58778518f}, {-0.91354549f,-0.40673658f}, {-0.97814763f,-0.20791161f}, +{1.0000000f,-0.0000000f}, {0.95105648f,-0.30901700f}, {0.80901700f,-0.58778524f}, +{0.58778524f,-0.80901700f}, {0.30901697f,-0.95105654f}, {-4.3711388e-08f,-1.0000000f}, +{-0.30901703f,-0.95105648f}, {-0.58778518f,-0.80901700f}, {-0.80901700f,-0.58778518f}, +{-0.95105654f,-0.30901679f}, {-1.0000000f,8.7422777e-08f}, {-0.95105654f,0.30901697f}, +{-0.80901694f,0.58778536f}, {-0.58778507f,0.80901712f}, {-0.30901709f,0.95105648f}, +{1.0000000f,-0.0000000f}, {0.99965733f,-0.026176950f}, {0.99862951f,-0.052335959f}, +{0.99691731f,-0.078459099f}, {0.99452192f,-0.10452846f}, {0.99144489f,-0.13052620f}, +{0.98768836f,-0.15643448f}, {0.98325491f,-0.18223552f}, {0.97814763f,-0.20791170f}, +{0.97236991f,-0.23344538f}, {0.96592581f,-0.25881904f}, {0.95881975f,-0.28401536f}, +{0.95105648f,-0.30901700f}, {0.94264150f,-0.33380687f}, {0.93358040f,-0.35836795f}, +{0.92387956f,-0.38268346f}, {0.91354543f,-0.40673664f}, {0.90258527f,-0.43051112f}, +{0.89100653f,-0.45399052f}, {0.87881708f,-0.47715878f}, {0.86602545f,-0.50000000f}, +{0.85264015f,-0.52249855f}, {0.83867055f,-0.54463905f}, {0.82412618f,-0.56640625f}, +{0.80901700f,-0.58778524f}, {0.79335332f,-0.60876143f}, {0.77714598f,-0.62932038f}, +{0.76040596f,-0.64944810f}, {0.74314475f,-0.66913062f}, {0.72537434f,-0.68835455f}, +{0.70710677f,-0.70710683f}, {0.68835455f,-0.72537440f}, {0.66913056f,-0.74314487f}, +{0.64944804f,-0.76040596f}, {0.62932038f,-0.77714598f}, {0.60876137f,-0.79335338f}, +{0.58778524f,-0.80901700f}, {0.56640625f,-0.82412618f}, {0.54463899f,-0.83867055f}, +{0.52249849f,-0.85264015f}, {0.49999997f,-0.86602545f}, {0.47715876f,-0.87881708f}, +{0.45399052f,-0.89100653f}, {0.43051103f,-0.90258533f}, {0.40673661f,-0.91354549f}, +{0.38268343f,-0.92387956f}, {0.35836786f,-0.93358046f}, {0.33380681f,-0.94264150f}, +{0.30901697f,-0.95105654f}, {0.28401533f,-0.95881975f}, {0.25881907f,-0.96592581f}, +{0.23344530f,-0.97236991f}, {0.20791166f,-0.97814763f}, {0.18223552f,-0.98325491f}, +{0.15643437f,-0.98768836f}, {0.13052613f,-0.99144489f}, {0.10452842f,-0.99452192f}, +{0.078459084f,-0.99691731f}, {0.052335974f,-0.99862951f}, {0.026176875f,-0.99965733f}, +{1.0000000f,-0.0000000f}, {0.99862951f,-0.052335959f}, {0.99452192f,-0.10452846f}, +{0.98768836f,-0.15643448f}, {0.97814763f,-0.20791170f}, {0.96592581f,-0.25881904f}, +{0.95105648f,-0.30901700f}, {0.93358040f,-0.35836795f}, {0.91354543f,-0.40673664f}, +{0.89100653f,-0.45399052f}, {0.86602545f,-0.50000000f}, {0.83867055f,-0.54463905f}, +{0.80901700f,-0.58778524f}, {0.77714598f,-0.62932038f}, {0.74314475f,-0.66913062f}, +{0.70710677f,-0.70710683f}, {0.66913056f,-0.74314487f}, {0.62932038f,-0.77714598f}, +{0.58778524f,-0.80901700f}, {0.54463899f,-0.83867055f}, {0.49999997f,-0.86602545f}, +{0.45399052f,-0.89100653f}, {0.40673661f,-0.91354549f}, {0.35836786f,-0.93358046f}, +{0.30901697f,-0.95105654f}, {0.25881907f,-0.96592581f}, {0.20791166f,-0.97814763f}, +{0.15643437f,-0.98768836f}, {0.10452842f,-0.99452192f}, {0.052335974f,-0.99862951f}, +{-4.3711388e-08f,-1.0000000f}, {-0.052336060f,-0.99862951f}, {-0.10452851f,-0.99452192f}, +{-0.15643445f,-0.98768836f}, {-0.20791174f,-0.97814757f}, {-0.25881916f,-0.96592581f}, +{-0.30901703f,-0.95105648f}, {-0.35836795f,-0.93358040f}, {-0.40673670f,-0.91354543f}, +{-0.45399061f,-0.89100647f}, {-0.50000006f,-0.86602533f}, {-0.54463905f,-0.83867055f}, +{-0.58778518f,-0.80901700f}, {-0.62932050f,-0.77714586f}, {-0.66913068f,-0.74314475f}, +{-0.70710677f,-0.70710677f}, {-0.74314493f,-0.66913044f}, {-0.77714604f,-0.62932026f}, +{-0.80901700f,-0.58778518f}, {-0.83867055f,-0.54463899f}, {-0.86602539f,-0.50000006f}, +{-0.89100659f,-0.45399037f}, {-0.91354549f,-0.40673658f}, {-0.93358040f,-0.35836792f}, +{-0.95105654f,-0.30901679f}, {-0.96592587f,-0.25881892f}, {-0.97814763f,-0.20791161f}, +{-0.98768836f,-0.15643445f}, {-0.99452192f,-0.10452849f}, {-0.99862957f,-0.052335810f}, +{1.0000000f,-0.0000000f}, {0.99691731f,-0.078459099f}, {0.98768836f,-0.15643448f}, +{0.97236991f,-0.23344538f}, {0.95105648f,-0.30901700f}, {0.92387956f,-0.38268346f}, +{0.89100653f,-0.45399052f}, {0.85264015f,-0.52249855f}, {0.80901700f,-0.58778524f}, +{0.76040596f,-0.64944810f}, {0.70710677f,-0.70710683f}, {0.64944804f,-0.76040596f}, +{0.58778524f,-0.80901700f}, {0.52249849f,-0.85264015f}, {0.45399052f,-0.89100653f}, +{0.38268343f,-0.92387956f}, {0.30901697f,-0.95105654f}, {0.23344530f,-0.97236991f}, +{0.15643437f,-0.98768836f}, {0.078459084f,-0.99691731f}, {-4.3711388e-08f,-1.0000000f}, +{-0.078459173f,-0.99691731f}, {-0.15643445f,-0.98768836f}, {-0.23344538f,-0.97236991f}, +{-0.30901703f,-0.95105648f}, {-0.38268352f,-0.92387950f}, {-0.45399061f,-0.89100647f}, +{-0.52249867f,-0.85264009f}, {-0.58778518f,-0.80901700f}, {-0.64944804f,-0.76040596f}, +{-0.70710677f,-0.70710677f}, {-0.76040596f,-0.64944804f}, {-0.80901700f,-0.58778518f}, +{-0.85264021f,-0.52249849f}, {-0.89100659f,-0.45399037f}, {-0.92387956f,-0.38268328f}, +{-0.95105654f,-0.30901679f}, {-0.97236991f,-0.23344538f}, {-0.98768836f,-0.15643445f}, +{-0.99691737f,-0.078459039f}, {-1.0000000f,8.7422777e-08f}, {-0.99691731f,0.078459218f}, +{-0.98768830f,0.15643461f}, {-0.97236985f,0.23344554f}, {-0.95105654f,0.30901697f}, +{-0.92387956f,0.38268346f}, {-0.89100653f,0.45399055f}, {-0.85264015f,0.52249861f}, +{-0.80901694f,0.58778536f}, {-0.76040590f,0.64944816f}, {-0.70710665f,0.70710689f}, +{-0.64944792f,0.76040608f}, {-0.58778507f,0.80901712f}, {-0.52249837f,0.85264033f}, +{-0.45399022f,0.89100665f}, {-0.38268313f,0.92387968f}, {-0.30901709f,0.95105648f}, +{-0.23344545f,0.97236991f}, {-0.15643452f,0.98768830f}, {-0.078459114f,0.99691731f}, +}; +static const ne10_fft_cpx_float32_t ne10_twiddles_120[120] = { +{1.0000000f,0.0000000f}, {1.0000000f,-0.0000000f}, {1.0000000f,-0.0000000f}, +{1.0000000f,-0.0000000f}, {0.91354543f,-0.40673664f}, {0.66913056f,-0.74314487f}, +{1.0000000f,-0.0000000f}, {0.66913056f,-0.74314487f}, {-0.10452851f,-0.99452192f}, +{1.0000000f,-0.0000000f}, {0.30901697f,-0.95105654f}, {-0.80901700f,-0.58778518f}, +{1.0000000f,-0.0000000f}, {-0.10452851f,-0.99452192f}, {-0.97814757f,0.20791179f}, +{1.0000000f,-0.0000000f}, {0.97814763f,-0.20791170f}, {0.91354543f,-0.40673664f}, +{0.80901700f,-0.58778524f}, {0.66913056f,-0.74314487f}, {0.49999997f,-0.86602545f}, +{0.30901697f,-0.95105654f}, {0.10452842f,-0.99452192f}, {-0.10452851f,-0.99452192f}, +{-0.30901703f,-0.95105648f}, {-0.50000006f,-0.86602533f}, {-0.66913068f,-0.74314475f}, +{-0.80901700f,-0.58778518f}, {-0.91354549f,-0.40673658f}, {-0.97814763f,-0.20791161f}, +{1.0000000f,-0.0000000f}, {0.99862951f,-0.052335959f}, {0.99452192f,-0.10452846f}, +{0.98768836f,-0.15643448f}, {0.97814763f,-0.20791170f}, {0.96592581f,-0.25881904f}, +{0.95105648f,-0.30901700f}, {0.93358040f,-0.35836795f}, {0.91354543f,-0.40673664f}, +{0.89100653f,-0.45399052f}, {0.86602545f,-0.50000000f}, {0.83867055f,-0.54463905f}, +{0.80901700f,-0.58778524f}, {0.77714598f,-0.62932038f}, {0.74314475f,-0.66913062f}, +{0.70710677f,-0.70710683f}, {0.66913056f,-0.74314487f}, {0.62932038f,-0.77714598f}, +{0.58778524f,-0.80901700f}, {0.54463899f,-0.83867055f}, {0.49999997f,-0.86602545f}, +{0.45399052f,-0.89100653f}, {0.40673661f,-0.91354549f}, {0.35836786f,-0.93358046f}, +{0.30901697f,-0.95105654f}, {0.25881907f,-0.96592581f}, {0.20791166f,-0.97814763f}, +{0.15643437f,-0.98768836f}, {0.10452842f,-0.99452192f}, {0.052335974f,-0.99862951f}, +{1.0000000f,-0.0000000f}, {0.99452192f,-0.10452846f}, {0.97814763f,-0.20791170f}, +{0.95105648f,-0.30901700f}, {0.91354543f,-0.40673664f}, {0.86602545f,-0.50000000f}, +{0.80901700f,-0.58778524f}, {0.74314475f,-0.66913062f}, {0.66913056f,-0.74314487f}, +{0.58778524f,-0.80901700f}, {0.49999997f,-0.86602545f}, {0.40673661f,-0.91354549f}, +{0.30901697f,-0.95105654f}, {0.20791166f,-0.97814763f}, {0.10452842f,-0.99452192f}, +{-4.3711388e-08f,-1.0000000f}, {-0.10452851f,-0.99452192f}, {-0.20791174f,-0.97814757f}, +{-0.30901703f,-0.95105648f}, {-0.40673670f,-0.91354543f}, {-0.50000006f,-0.86602533f}, +{-0.58778518f,-0.80901700f}, {-0.66913068f,-0.74314475f}, {-0.74314493f,-0.66913044f}, +{-0.80901700f,-0.58778518f}, {-0.86602539f,-0.50000006f}, {-0.91354549f,-0.40673658f}, +{-0.95105654f,-0.30901679f}, {-0.97814763f,-0.20791161f}, {-0.99452192f,-0.10452849f}, +{1.0000000f,-0.0000000f}, {0.98768836f,-0.15643448f}, {0.95105648f,-0.30901700f}, +{0.89100653f,-0.45399052f}, {0.80901700f,-0.58778524f}, {0.70710677f,-0.70710683f}, +{0.58778524f,-0.80901700f}, {0.45399052f,-0.89100653f}, {0.30901697f,-0.95105654f}, +{0.15643437f,-0.98768836f}, {-4.3711388e-08f,-1.0000000f}, {-0.15643445f,-0.98768836f}, +{-0.30901703f,-0.95105648f}, {-0.45399061f,-0.89100647f}, {-0.58778518f,-0.80901700f}, +{-0.70710677f,-0.70710677f}, {-0.80901700f,-0.58778518f}, {-0.89100659f,-0.45399037f}, +{-0.95105654f,-0.30901679f}, {-0.98768836f,-0.15643445f}, {-1.0000000f,8.7422777e-08f}, +{-0.98768830f,0.15643461f}, {-0.95105654f,0.30901697f}, {-0.89100653f,0.45399055f}, +{-0.80901694f,0.58778536f}, {-0.70710665f,0.70710689f}, {-0.58778507f,0.80901712f}, +{-0.45399022f,0.89100665f}, {-0.30901709f,0.95105648f}, {-0.15643452f,0.98768830f}, +}; +static const ne10_fft_cpx_float32_t ne10_twiddles_60[60] = { +{1.0000000f,0.0000000f}, {1.0000000f,-0.0000000f}, {1.0000000f,-0.0000000f}, +{1.0000000f,-0.0000000f}, {0.91354543f,-0.40673664f}, {0.66913056f,-0.74314487f}, +{1.0000000f,-0.0000000f}, {0.66913056f,-0.74314487f}, {-0.10452851f,-0.99452192f}, +{1.0000000f,-0.0000000f}, {0.30901697f,-0.95105654f}, {-0.80901700f,-0.58778518f}, +{1.0000000f,-0.0000000f}, {-0.10452851f,-0.99452192f}, {-0.97814757f,0.20791179f}, +{1.0000000f,-0.0000000f}, {0.99452192f,-0.10452846f}, {0.97814763f,-0.20791170f}, +{0.95105648f,-0.30901700f}, {0.91354543f,-0.40673664f}, {0.86602545f,-0.50000000f}, +{0.80901700f,-0.58778524f}, {0.74314475f,-0.66913062f}, {0.66913056f,-0.74314487f}, +{0.58778524f,-0.80901700f}, {0.49999997f,-0.86602545f}, {0.40673661f,-0.91354549f}, +{0.30901697f,-0.95105654f}, {0.20791166f,-0.97814763f}, {0.10452842f,-0.99452192f}, +{1.0000000f,-0.0000000f}, {0.97814763f,-0.20791170f}, {0.91354543f,-0.40673664f}, +{0.80901700f,-0.58778524f}, {0.66913056f,-0.74314487f}, {0.49999997f,-0.86602545f}, +{0.30901697f,-0.95105654f}, {0.10452842f,-0.99452192f}, {-0.10452851f,-0.99452192f}, +{-0.30901703f,-0.95105648f}, {-0.50000006f,-0.86602533f}, {-0.66913068f,-0.74314475f}, +{-0.80901700f,-0.58778518f}, {-0.91354549f,-0.40673658f}, {-0.97814763f,-0.20791161f}, +{1.0000000f,-0.0000000f}, {0.95105648f,-0.30901700f}, {0.80901700f,-0.58778524f}, +{0.58778524f,-0.80901700f}, {0.30901697f,-0.95105654f}, {-4.3711388e-08f,-1.0000000f}, +{-0.30901703f,-0.95105648f}, {-0.58778518f,-0.80901700f}, {-0.80901700f,-0.58778518f}, +{-0.95105654f,-0.30901679f}, {-1.0000000f,8.7422777e-08f}, {-0.95105654f,0.30901697f}, +{-0.80901694f,0.58778536f}, {-0.58778507f,0.80901712f}, {-0.30901709f,0.95105648f}, +}; +static const ne10_fft_state_float32_t ne10_fft_state_float32_t_480 = { +120, +(ne10_int32_t *)ne10_factors_480, +(ne10_fft_cpx_float32_t *)ne10_twiddles_480, +NULL, +(ne10_fft_cpx_float32_t *)&ne10_twiddles_480[120], +/* is_forward_scaled = true */ +(ne10_int32_t) 1, +/* is_backward_scaled = false */ +(ne10_int32_t) 0, +}; +static const arch_fft_state cfg_arch_480 = { +1, +(void *)&ne10_fft_state_float32_t_480, +}; + +static const ne10_fft_state_float32_t ne10_fft_state_float32_t_240 = { +60, +(ne10_int32_t *)ne10_factors_240, +(ne10_fft_cpx_float32_t *)ne10_twiddles_240, +NULL, +(ne10_fft_cpx_float32_t *)&ne10_twiddles_240[60], +/* is_forward_scaled = true */ +(ne10_int32_t) 1, +/* is_backward_scaled = false */ +(ne10_int32_t) 0, +}; +static const arch_fft_state cfg_arch_240 = { +1, +(void *)&ne10_fft_state_float32_t_240, +}; + +static const ne10_fft_state_float32_t ne10_fft_state_float32_t_120 = { +30, +(ne10_int32_t *)ne10_factors_120, +(ne10_fft_cpx_float32_t *)ne10_twiddles_120, +NULL, +(ne10_fft_cpx_float32_t *)&ne10_twiddles_120[30], +/* is_forward_scaled = true */ +(ne10_int32_t) 1, +/* is_backward_scaled = false */ +(ne10_int32_t) 0, +}; +static const arch_fft_state cfg_arch_120 = { +1, +(void *)&ne10_fft_state_float32_t_120, +}; + +static const ne10_fft_state_float32_t ne10_fft_state_float32_t_60 = { +15, +(ne10_int32_t *)ne10_factors_60, +(ne10_fft_cpx_float32_t *)ne10_twiddles_60, +NULL, +(ne10_fft_cpx_float32_t *)&ne10_twiddles_60[15], +/* is_forward_scaled = true */ +(ne10_int32_t) 1, +/* is_backward_scaled = false */ +(ne10_int32_t) 0, +}; +static const arch_fft_state cfg_arch_60 = { +1, +(void *)&ne10_fft_state_float32_t_60, +}; + +#endif /* end NE10_FFT_PARAMS48000_960 */ diff --git a/port/third_party/opus/celt/vq.c b/port/third_party/opus/celt/vq.c new file mode 100644 index 00000000..8011e225 --- /dev/null +++ b/port/third_party/opus/celt/vq.c @@ -0,0 +1,442 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "mathops.h" +#include "cwrs.h" +#include "vq.h" +#include "arch.h" +#include "os_support.h" +#include "bands.h" +#include "rate.h" +#include "pitch.h" + +#if defined(MIPSr1_ASM) +#include "mips/vq_mipsr1.h" +#endif + +#ifndef OVERRIDE_vq_exp_rotation1 +static void exp_rotation1(celt_norm *X, int len, int stride, opus_val16 c, opus_val16 s) +{ + int i; + opus_val16 ms; + celt_norm *Xptr; + Xptr = X; + ms = NEG16(s); + for (i=0;i=0;i--) + { + celt_norm x1, x2; + x1 = Xptr[0]; + x2 = Xptr[stride]; + Xptr[stride] = EXTRACT16(PSHR32(MAC16_16(MULT16_16(c, x2), s, x1), 15)); + *Xptr-- = EXTRACT16(PSHR32(MAC16_16(MULT16_16(c, x1), ms, x2), 15)); + } +} +#endif /* OVERRIDE_vq_exp_rotation1 */ + +void exp_rotation(celt_norm *X, int len, int dir, int stride, int K, int spread) +{ + static const int SPREAD_FACTOR[3]={15,10,5}; + int i; + opus_val16 c, s; + opus_val16 gain, theta; + int stride2=0; + int factor; + + if (2*K>=len || spread==SPREAD_NONE) + return; + factor = SPREAD_FACTOR[spread-1]; + + gain = celt_div((opus_val32)MULT16_16(Q15_ONE,len),(opus_val32)(len+factor*K)); + theta = HALF16(MULT16_16_Q15(gain,gain)); + + c = celt_cos_norm(EXTEND32(theta)); + s = celt_cos_norm(EXTEND32(SUB16(Q15ONE,theta))); /* sin(theta) */ + + if (len>=8*stride) + { + stride2 = 1; + /* This is just a simple (equivalent) way of computing sqrt(len/stride) with rounding. + It's basically incrementing long as (stride2+0.5)^2 < len/stride. */ + while ((stride2*stride2+stride2)*stride + (stride>>2) < len) + stride2++; + } + /*NOTE: As a minor optimization, we could be passing around log2(B), not B, for both this and for + extract_collapse_mask().*/ + len = celt_udiv(len, stride); + for (i=0;i>1; +#endif + t = VSHR32(Ryy, 2*(k-7)); + g = MULT16_16_P15(celt_rsqrt_norm(t),gain); + + i=0; + do + X[i] = EXTRACT16(PSHR32(MULT16_16(g, iy[i]), k+1)); + while (++i < N); +} + +static unsigned extract_collapse_mask(int *iy, int N, int B) +{ + unsigned collapse_mask; + int N0; + int i; + if (B<=1) + return 1; + /*NOTE: As a minor optimization, we could be passing around log2(B), not B, for both this and for + exp_rotation().*/ + N0 = celt_udiv(N, B); + collapse_mask = 0; + i=0; do { + int j; + unsigned tmp=0; + j=0; do { + tmp |= iy[i*N0+j]; + } while (++j (N>>1)) + { + opus_val16 rcp; + j=0; do { + sum += X[j]; + } while (++j EPSILON && sum < 64)) +#endif + { + X[0] = QCONST16(1.f,14); + j=1; do + X[j]=0; + while (++j=0); + + /* This should never happen, but just in case it does (e.g. on silence) + we fill the first bin with pulses. */ +#ifdef FIXED_POINT_DEBUG + celt_sig_assert(pulsesLeft<=N+3); +#endif + if (pulsesLeft > N+3) + { + opus_val16 tmp = (opus_val16)pulsesLeft; + yy = MAC16_16(yy, tmp, tmp); + yy = MAC16_16(yy, tmp, y[0]); + iy[0] += pulsesLeft; + pulsesLeft=0; + } + + for (i=0;i= best_num/best_den, but that way + we can do it without any division */ + /* OPT: It's not clear whether a cmov is faster than a branch here + since the condition is more often false than true and using + a cmov introduces data dependencies across iterations. The optimal + choice may be architecture-dependent. */ + if (opus_unlikely(MULT16_16(best_den, Rxy) > MULT16_16(Ryy, best_num))) + { + best_den = Ryy; + best_num = Rxy; + best_id = j; + } + } while (++j0, "alg_quant() needs at least one pulse"); + celt_assert2(N>1, "alg_quant() needs at least two dimensions"); + + /* Covers vectorization by up to 4. */ + ALLOC(iy, N+3, int); + + exp_rotation(X, N, 1, B, K, spread); + + yy = op_pvq_search(X, iy, K, N, arch); + + encode_pulses(iy, N, K, enc); + + if (resynth) + { + normalise_residual(iy, X, N, yy, gain); + exp_rotation(X, N, -1, B, K, spread); + } + + collapse_mask = extract_collapse_mask(iy, N, B); + RESTORE_STACK; + return collapse_mask; +} + +/** Decode pulse vector and combine the result with the pitch vector to produce + the final normalised signal in the current band. */ +unsigned alg_unquant(celt_norm *X, int N, int K, int spread, int B, + ec_dec *dec, opus_val16 gain) +{ + opus_val32 Ryy; + unsigned collapse_mask; + VARDECL(int, iy); + SAVE_STACK; + + celt_assert2(K>0, "alg_unquant() needs at least one pulse"); + celt_assert2(N>1, "alg_unquant() needs at least two dimensions"); + ALLOC(iy, N, int); + Ryy = decode_pulses(iy, N, K, dec); + normalise_residual(iy, X, N, Ryy, gain); + exp_rotation(X, N, -1, B, K, spread); + collapse_mask = extract_collapse_mask(iy, N, B); + RESTORE_STACK; + return collapse_mask; +} + +#ifndef OVERRIDE_renormalise_vector +void renormalise_vector(celt_norm *X, int N, opus_val16 gain, int arch) +{ + int i; +#ifdef FIXED_POINT + int k; +#endif + opus_val32 E; + opus_val16 g; + opus_val32 t; + celt_norm *xptr; + E = EPSILON + celt_inner_prod(X, X, N, arch); +#ifdef FIXED_POINT + k = celt_ilog2(E)>>1; +#endif + t = VSHR32(E, 2*(k-7)); + g = MULT16_16_P15(celt_rsqrt_norm(t),gain); + + xptr = X; + for (i=0;i + *
  • audio_frame is the audio data in opus_int16 (or float for opus_encode_float())
  • + *
  • frame_size is the duration of the frame in samples (per channel)
  • + *
  • packet is the byte array to which the compressed data is written
  • + *
  • max_packet is the maximum number of bytes that can be written in the packet (4000 bytes is recommended). + * Do not use max_packet to control VBR target bitrate, instead use the #OPUS_SET_BITRATE CTL.
  • + * + * + * opus_encode() and opus_encode_float() return the number of bytes actually written to the packet. + * The return value can be negative, which indicates that an error has occurred. If the return value + * is 2 bytes or less, then the packet does not need to be transmitted (DTX). + * + * Once the encoder state if no longer needed, it can be destroyed with + * + * @code + * opus_encoder_destroy(enc); + * @endcode + * + * If the encoder was created with opus_encoder_init() rather than opus_encoder_create(), + * then no action is required aside from potentially freeing the memory that was manually + * allocated for it (calling free(enc) for the example above) + * + */ + +/** Opus encoder state. + * This contains the complete state of an Opus encoder. + * It is position independent and can be freely copied. + * @see opus_encoder_create,opus_encoder_init + */ +typedef struct OpusEncoder OpusEncoder; + +/** Gets the size of an OpusEncoder structure. + * @param[in] channels int: Number of channels. + * This must be 1 or 2. + * @returns The size in bytes. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_encoder_get_size(int channels); + +/** + */ + +/** Allocates and initializes an encoder state. + * There are three coding modes: + * + * @ref OPUS_APPLICATION_VOIP gives best quality at a given bitrate for voice + * signals. It enhances the input signal by high-pass filtering and + * emphasizing formants and harmonics. Optionally it includes in-band + * forward error correction to protect against packet loss. Use this + * mode for typical VoIP applications. Because of the enhancement, + * even at high bitrates the output may sound different from the input. + * + * @ref OPUS_APPLICATION_AUDIO gives best quality at a given bitrate for most + * non-voice signals like music. Use this mode for music and mixed + * (music/voice) content, broadcast, and applications requiring less + * than 15 ms of coding delay. + * + * @ref OPUS_APPLICATION_RESTRICTED_LOWDELAY configures low-delay mode that + * disables the speech-optimized mode in exchange for slightly reduced delay. + * This mode can only be set on an newly initialized or freshly reset encoder + * because it changes the codec delay. + * + * This is useful when the caller knows that the speech-optimized modes will not be needed (use with caution). + * @param [in] Fs opus_int32: Sampling rate of input signal (Hz) + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) in input signal + * @param [in] application int: Coding mode (one of @ref OPUS_APPLICATION_VOIP, @ref OPUS_APPLICATION_AUDIO, or @ref OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @param [out] error int*: @ref opus_errorcodes + * @note Regardless of the sampling rate and number channels selected, the Opus encoder + * can switch to a lower audio bandwidth or number of channels if the bitrate + * selected is too low. This also means that it is safe to always use 48 kHz stereo input + * and let the encoder optimize the encoding. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusEncoder *opus_encoder_create( + opus_int32 Fs, + int channels, + int application, + int *error +); + +/** Initializes a previously allocated encoder state + * The memory pointed to by st must be at least the size returned by opus_encoder_get_size(). + * This is intended for applications which use their own allocator instead of malloc. + * @see opus_encoder_create(),opus_encoder_get_size() + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @param [in] st OpusEncoder*: Encoder state + * @param [in] Fs opus_int32: Sampling rate of input signal (Hz) + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) in input signal + * @param [in] application int: Coding mode (one of OPUS_APPLICATION_VOIP, OPUS_APPLICATION_AUDIO, or OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @retval #OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_EXPORT int opus_encoder_init( + OpusEncoder *st, + opus_int32 Fs, + int channels, + int application +) OPUS_ARG_NONNULL(1); + +/** Encodes an Opus frame. + * @param [in] st OpusEncoder*: Encoder state + * @param [in] pcm opus_int16*: Input signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16) + * @param [in] frame_size int: Number of samples per channel in the + * input signal. + * This must be an Opus frame size for + * the encoder's sampling rate. + * For example, at 48 kHz the permitted + * values are 120, 240, 480, 960, 1920, + * and 2880. + * Passing in a duration of less than + * 10 ms (480 samples at 48 kHz) will + * prevent the encoder from using the LPC + * or hybrid modes. + * @param [out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode( + OpusEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Encodes an Opus frame from floating point input. + * @param [in] st OpusEncoder*: Encoder state + * @param [in] pcm float*: Input in float format (interleaved if 2 channels), with a normal range of +/-1.0. + * Samples with a range beyond +/-1.0 are supported but will + * be clipped by decoders using the integer API and should + * only be used if it is known that the far end supports + * extended dynamic range. + * length is frame_size*channels*sizeof(float) + * @param [in] frame_size int: Number of samples per channel in the + * input signal. + * This must be an Opus frame size for + * the encoder's sampling rate. + * For example, at 48 kHz the permitted + * values are 120, 240, 480, 960, 1920, + * and 2880. + * Passing in a duration of less than + * 10 ms (480 samples at 48 kHz) will + * prevent the encoder from using the LPC + * or hybrid modes. + * @param [out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode_float( + OpusEncoder *st, + const float *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Frees an OpusEncoder allocated by opus_encoder_create(). + * @param[in] st OpusEncoder*: State to be freed. + */ +OPUS_EXPORT void opus_encoder_destroy(OpusEncoder *st); + +/** Perform a CTL function on an Opus encoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @param st OpusEncoder*: Encoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls or + * @ref opus_encoderctls. + * @see opus_genericctls + * @see opus_encoderctls + */ +OPUS_EXPORT int opus_encoder_ctl(OpusEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); +/**@}*/ + +/** @defgroup opus_decoder Opus Decoder + * @{ + * + * @brief This page describes the process and functions used to decode Opus. + * + * The decoding process also starts with creating a decoder + * state. This can be done with: + * @code + * int error; + * OpusDecoder *dec; + * dec = opus_decoder_create(Fs, channels, &error); + * @endcode + * where + * @li Fs is the sampling rate and must be 8000, 12000, 16000, 24000, or 48000 + * @li channels is the number of channels (1 or 2) + * @li error will hold the error code in case of failure (or #OPUS_OK on success) + * @li the return value is a newly created decoder state to be used for decoding + * + * While opus_decoder_create() allocates memory for the state, it's also possible + * to initialize pre-allocated memory: + * @code + * int size; + * int error; + * OpusDecoder *dec; + * size = opus_decoder_get_size(channels); + * dec = malloc(size); + * error = opus_decoder_init(dec, Fs, channels); + * @endcode + * where opus_decoder_get_size() returns the required size for the decoder state. Note that + * future versions of this code may change the size, so no assumptions should be made about it. + * + * The decoder state is always continuous in memory and only a shallow copy is sufficient + * to copy it (e.g. memcpy()) + * + * To decode a frame, opus_decode() or opus_decode_float() must be called with a packet of compressed audio data: + * @code + * frame_size = opus_decode(dec, packet, len, decoded, max_size, 0); + * @endcode + * where + * + * @li packet is the byte array containing the compressed data + * @li len is the exact number of bytes contained in the packet + * @li decoded is the decoded audio data in opus_int16 (or float for opus_decode_float()) + * @li max_size is the max duration of the frame in samples (per channel) that can fit into the decoded_frame array + * + * opus_decode() and opus_decode_float() return the number of samples (per channel) decoded from the packet. + * If that value is negative, then an error has occurred. This can occur if the packet is corrupted or if the audio + * buffer is too small to hold the decoded audio. + * + * Opus is a stateful codec with overlapping blocks and as a result Opus + * packets are not coded independently of each other. Packets must be + * passed into the decoder serially and in the correct order for a correct + * decode. Lost packets can be replaced with loss concealment by calling + * the decoder with a null pointer and zero length for the missing packet. + * + * A single codec state may only be accessed from a single thread at + * a time and any required locking must be performed by the caller. Separate + * streams must be decoded with separate decoder states and can be decoded + * in parallel unless the library was compiled with NONTHREADSAFE_PSEUDOSTACK + * defined. + * + */ + +/** Opus decoder state. + * This contains the complete state of an Opus decoder. + * It is position independent and can be freely copied. + * @see opus_decoder_create,opus_decoder_init + */ +typedef struct OpusDecoder OpusDecoder; + +/** Opus DRED decoder. + * This contains the complete state of an Opus DRED decoder. + * It is position independent and can be freely copied. + * @see opus_dred_decoder_create,opus_dred_decoder_init + */ +typedef struct OpusDREDDecoder OpusDREDDecoder; + + +/** Opus DRED state. + * This contains the complete state of an Opus DRED packet. + * It is position independent and can be freely copied. + * @see opus_dred_create,opus_dred_init + */ +typedef struct OpusDRED OpusDRED; + +/** Gets the size of an OpusDecoder structure. + * @param [in] channels int: Number of channels. + * This must be 1 or 2. + * @returns The size in bytes. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_size(int channels); + +/** Allocates and initializes a decoder state. + * @param [in] Fs opus_int32: Sample rate to decode at (Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) to decode + * @param [out] error int*: #OPUS_OK Success or @ref opus_errorcodes + * + * Internally Opus stores data at 48000 Hz, so that should be the default + * value for Fs. However, the decoder can efficiently decode to buffers + * at 8, 12, 16, and 24 kHz so if for some reason the caller cannot use + * data at the full sample rate, or knows the compressed data doesn't + * use the full frequency range, it can request decoding at a reduced + * rate. Likewise, the decoder is capable of filling in either mono or + * interleaved stereo pcm buffers, at the caller's request. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusDecoder *opus_decoder_create( + opus_int32 Fs, + int channels, + int *error +); + +/** Initializes a previously allocated decoder state. + * The state must be at least the size returned by opus_decoder_get_size(). + * This is intended for applications which use their own allocator instead of malloc. @see opus_decoder_create,opus_decoder_get_size + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @param [in] st OpusDecoder*: Decoder state. + * @param [in] Fs opus_int32: Sampling rate to decode to (Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param [in] channels int: Number of channels (1 or 2) to decode + * @retval #OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_EXPORT int opus_decoder_init( + OpusDecoder *st, + opus_int32 Fs, + int channels +) OPUS_ARG_NONNULL(1); + +/** Decode an Opus packet. + * @param [in] st OpusDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len opus_int32: Number of bytes in payload* + * @param [out] pcm opus_int16*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(opus_int16) + * @param [in] frame_size Number of samples per channel of available space in \a pcm. + * If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will + * not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1), + * then frame_size needs to be exactly the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the next incoming packet. For the PLC and + * FEC cases, frame_size must be a multiple of 2.5 ms. + * @param [in] decode_fec int: Flag (0 or 1) to request that any in-band forward error correction data be + * decoded. If no such data is available, the frame is decoded as if it were lost. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode( + OpusDecoder *st, + const unsigned char *data, + opus_int32 len, + opus_int16 *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Decode an Opus packet with floating point output. + * @param [in] st OpusDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len opus_int32: Number of bytes in payload + * @param [out] pcm float*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(float) + * @param [in] frame_size Number of samples per channel of available space in \a pcm. + * If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will + * not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1), + * then frame_size needs to be exactly the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the next incoming packet. For the PLC and + * FEC cases, frame_size must be a multiple of 2.5 ms. + * @param [in] decode_fec int: Flag (0 or 1) to request that any in-band forward error correction data be + * decoded. If no such data is available the frame is decoded as if it were lost. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode_float( + OpusDecoder *st, + const unsigned char *data, + opus_int32 len, + float *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on an Opus decoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @param st OpusDecoder*: Decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls or + * @ref opus_decoderctls. + * @see opus_genericctls + * @see opus_decoderctls + */ +OPUS_EXPORT int opus_decoder_ctl(OpusDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/** Frees an OpusDecoder allocated by opus_decoder_create(). + * @param[in] st OpusDecoder*: State to be freed. + */ +OPUS_EXPORT void opus_decoder_destroy(OpusDecoder *st); + +/** Gets the size of an OpusDREDDecoder structure. + * @returns The size in bytes. + */ +OPUS_EXPORT int opus_dred_decoder_get_size(void); + +/** Allocates and initializes an OpusDREDDecoder state. + * @param [out] error int*: #OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_EXPORT OpusDREDDecoder *opus_dred_decoder_create(int *error); + +/** Initializes an OpusDREDDecoder state. + * @param[in] dec OpusDREDDecoder*: State to be initialized. + */ +OPUS_EXPORT int opus_dred_decoder_init(OpusDREDDecoder *dec); + +/** Frees an OpusDREDDecoder allocated by opus_dred_decoder_create(). + * @param[in] dec OpusDREDDecoder*: State to be freed. + */ +OPUS_EXPORT void opus_dred_decoder_destroy(OpusDREDDecoder *dec); + +/** Perform a CTL function on an Opus DRED decoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @param dred_dec OpusDREDDecoder*: DRED Decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls or + * @ref opus_decoderctls. + * @see opus_genericctls + * @see opus_decoderctls + */ +OPUS_EXPORT int opus_dred_decoder_ctl(OpusDREDDecoder *dred_dec, int request, ...); + +/** Gets the size of an OpusDRED structure. + * @returns The size in bytes. + */ +OPUS_EXPORT int opus_dred_get_size(void); + +/** Allocates and initializes a DRED state. + * @param [out] error int*: #OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_EXPORT OpusDRED *opus_dred_alloc(int *error); + +/** Frees an OpusDRED allocated by opus_dred_create(). + * @param[in] dec OpusDRED*: State to be freed. + */ +OPUS_EXPORT void opus_dred_free(OpusDRED *dec); + +/** Decode an Opus DRED packet. + * @param [in] dred_dec OpusDRED*: DRED Decoder state + * @param [in] dred OpusDRED*: DRED state + * @param [in] data char*: Input payload + * @param [in] len opus_int32: Number of bytes in payload + * @param [in] max_dred_samples opus_int32: Maximum number of DRED samples that may be needed (if available in the packet). + * @param [in] sampling_rate opus_int32: Sampling rate used for max_dred_samples argument. Needs not match the actual sampling rate of the decoder. + * @param [out] dred_end opus_int32*: Number of non-encoded (silence) samples between the DRED timestamp and the last DRED sample. + * @param [in] defer_processing int: Flag (0 or 1). If set to one, the CPU-intensive part of the DRED decoding is deferred until opus_dred_process() is called. + * @returns Offset (positive) of the first decoded DRED samples, zero if no DRED is present, or @ref opus_errorcodes + */ +OPUS_EXPORT int opus_dred_parse(OpusDREDDecoder *dred_dec, OpusDRED *dred, const unsigned char *data, opus_int32 len, opus_int32 max_dred_samples, opus_int32 sampling_rate, int *dred_end, int defer_processing) OPUS_ARG_NONNULL(1); + +/** Finish decoding an Opus DRED packet. The function only needs to be called if opus_dred_parse() was called with defer_processing=1. + * The source and destination will often be the same DRED state. + * @param [in] dred_dec OpusDRED*: DRED Decoder state + * @param [in] src OpusDRED*: Source DRED state to start the processing from. + * @param [out] dst OpusDRED*: Destination DRED state to store the updated state after processing. + * @returns @ref opus_errorcodes + */ +OPUS_EXPORT int opus_dred_process(OpusDREDDecoder *dred_dec, const OpusDRED *src, OpusDRED *dst); + +/** Decode audio from an Opus DRED packet with floating point output. + * @param [in] st OpusDecoder*: Decoder state + * @param [in] dred OpusDRED*: DRED state + * @param [in] dred_offset opus_int32: position of the redundancy to decode (in samples before the beginning of the real audio data in the packet). + * @param [out] pcm opus_int16*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(opus_int16) + * @param [in] frame_size Number of samples per channel to decode in \a pcm. + * frame_size must be a multiple of 2.5 ms. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_EXPORT int opus_decoder_dred_decode(OpusDecoder *st, const OpusDRED *dred, opus_int32 dred_offset, opus_int16 *pcm, opus_int32 frame_size); + +/** Decode audio from an Opus DRED packet with floating point output. + * @param [in] st OpusDecoder*: Decoder state + * @param [in] dred OpusDRED*: DRED state + * @param [in] dred_offset opus_int32: position of the redundancy to decode (in samples before the beginning of the real audio data in the packet). + * @param [out] pcm float*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(float) + * @param [in] frame_size Number of samples per channel to decode in \a pcm. + * frame_size must be a multiple of 2.5 ms. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_EXPORT int opus_decoder_dred_decode_float(OpusDecoder *st, const OpusDRED *dred, opus_int32 dred_offset, float *pcm, opus_int32 frame_size); + + +/** Parse an opus packet into one or more frames. + * Opus_decode will perform this operation internally so most applications do + * not need to use this function. + * This function does not copy the frames, the returned pointers are pointers into + * the input packet. + * @param [in] data char*: Opus packet to be parsed + * @param [in] len opus_int32: size of data + * @param [out] out_toc char*: TOC pointer + * @param [out] frames char*[48] encapsulated frames + * @param [out] size opus_int16[48] sizes of the encapsulated frames + * @param [out] payload_offset int*: returns the position of the payload within the packet (in bytes) + * @returns number of frames + */ +OPUS_EXPORT int opus_packet_parse( + const unsigned char *data, + opus_int32 len, + unsigned char *out_toc, + const unsigned char *frames[48], + opus_int16 size[48], + int *payload_offset +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5); + +/** Gets the bandwidth of an Opus packet. + * @param [in] data char*: Opus packet + * @retval OPUS_BANDWIDTH_NARROWBAND Narrowband (4kHz bandpass) + * @retval OPUS_BANDWIDTH_MEDIUMBAND Mediumband (6kHz bandpass) + * @retval OPUS_BANDWIDTH_WIDEBAND Wideband (8kHz bandpass) + * @retval OPUS_BANDWIDTH_SUPERWIDEBAND Superwideband (12kHz bandpass) + * @retval OPUS_BANDWIDTH_FULLBAND Fullband (20kHz bandpass) + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_bandwidth(const unsigned char *data) OPUS_ARG_NONNULL(1); + +/** Gets the number of samples per frame from an Opus packet. + * @param [in] data char*: Opus packet. + * This must contain at least one byte of + * data. + * @param [in] Fs opus_int32: Sampling rate in Hz. + * This must be a multiple of 400, or + * inaccurate results will be returned. + * @returns Number of samples per frame. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_samples_per_frame(const unsigned char *data, opus_int32 Fs) OPUS_ARG_NONNULL(1); + +/** Gets the number of channels from an Opus packet. + * @param [in] data char*: Opus packet + * @returns Number of channels + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_channels(const unsigned char *data) OPUS_ARG_NONNULL(1); + +/** Gets the number of frames in an Opus packet. + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @returns Number of frames + * @retval OPUS_BAD_ARG Insufficient data was passed to the function + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_frames(const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1); + +/** Gets the number of samples of an Opus packet. + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @param [in] Fs opus_int32: Sampling rate in Hz. + * This must be a multiple of 400, or + * inaccurate results will be returned. + * @returns Number of samples + * @retval OPUS_BAD_ARG Insufficient data was passed to the function + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_samples(const unsigned char packet[], opus_int32 len, opus_int32 Fs) OPUS_ARG_NONNULL(1); + +/** Checks whether an Opus packet has LBRR. + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @returns 1 is LBRR is present, 0 otherwise + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_has_lbrr(const unsigned char packet[], opus_int32 len); + +/** Gets the number of samples of an Opus packet. + * @param [in] dec OpusDecoder*: Decoder state + * @param [in] packet char*: Opus packet + * @param [in] len opus_int32: Length of packet + * @returns Number of samples + * @retval OPUS_BAD_ARG Insufficient data was passed to the function + * @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_nb_samples(const OpusDecoder *dec, const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); + +/** Applies soft-clipping to bring a float signal within the [-1,1] range. If + * the signal is already in that range, nothing is done. If there are values + * outside of [-1,1], then the signal is clipped as smoothly as possible to + * both fit in the range and avoid creating excessive distortion in the + * process. + * @param [in,out] pcm float*: Input PCM and modified PCM + * @param [in] frame_size int Number of samples per channel to process + * @param [in] channels int: Number of channels + * @param [in,out] softclip_mem float*: State memory for the soft clipping process (one float per channel, initialized to zero) + */ +OPUS_EXPORT void opus_pcm_soft_clip(float *pcm, int frame_size, int channels, float *softclip_mem); + + +/**@}*/ + +/** @defgroup opus_repacketizer Repacketizer + * @{ + * + * The repacketizer can be used to merge multiple Opus packets into a single + * packet or alternatively to split Opus packets that have previously been + * merged. Splitting valid Opus packets is always guaranteed to succeed, + * whereas merging valid packets only succeeds if all frames have the same + * mode, bandwidth, and frame size, and when the total duration of the merged + * packet is no more than 120 ms. The 120 ms limit comes from the + * specification and limits decoder memory requirements at a point where + * framing overhead becomes negligible. + * + * The repacketizer currently only operates on elementary Opus + * streams. It will not manipualte multistream packets successfully, except in + * the degenerate case where they consist of data from a single stream. + * + * The repacketizing process starts with creating a repacketizer state, either + * by calling opus_repacketizer_create() or by allocating the memory yourself, + * e.g., + * @code + * OpusRepacketizer *rp; + * rp = (OpusRepacketizer*)malloc(opus_repacketizer_get_size()); + * if (rp != NULL) + * opus_repacketizer_init(rp); + * @endcode + * + * Then the application should submit packets with opus_repacketizer_cat(), + * extract new packets with opus_repacketizer_out() or + * opus_repacketizer_out_range(), and then reset the state for the next set of + * input packets via opus_repacketizer_init(). + * + * For example, to split a sequence of packets into individual frames: + * @code + * unsigned char *data; + * int len; + * while (get_next_packet(&data, &len)) + * { + * unsigned char out[1276]; + * opus_int32 out_len; + * int nb_frames; + * int err; + * int i; + * err = opus_repacketizer_cat(rp, data, len); + * if (err != OPUS_OK) + * { + * release_packet(data); + * return err; + * } + * nb_frames = opus_repacketizer_get_nb_frames(rp); + * for (i = 0; i < nb_frames; i++) + * { + * out_len = opus_repacketizer_out_range(rp, i, i+1, out, sizeof(out)); + * if (out_len < 0) + * { + * release_packet(data); + * return (int)out_len; + * } + * output_next_packet(out, out_len); + * } + * opus_repacketizer_init(rp); + * release_packet(data); + * } + * @endcode + * + * Alternatively, to combine a sequence of frames into packets that each + * contain up to TARGET_DURATION_MS milliseconds of data: + * @code + * // The maximum number of packets with duration TARGET_DURATION_MS occurs + * // when the frame size is 2.5 ms, for a total of (TARGET_DURATION_MS*2/5) + * // packets. + * unsigned char *data[(TARGET_DURATION_MS*2/5)+1]; + * opus_int32 len[(TARGET_DURATION_MS*2/5)+1]; + * int nb_packets; + * unsigned char out[1277*(TARGET_DURATION_MS*2/2)]; + * opus_int32 out_len; + * int prev_toc; + * nb_packets = 0; + * while (get_next_packet(data+nb_packets, len+nb_packets)) + * { + * int nb_frames; + * int err; + * nb_frames = opus_packet_get_nb_frames(data[nb_packets], len[nb_packets]); + * if (nb_frames < 1) + * { + * release_packets(data, nb_packets+1); + * return nb_frames; + * } + * nb_frames += opus_repacketizer_get_nb_frames(rp); + * // If adding the next packet would exceed our target, or it has an + * // incompatible TOC sequence, output the packets we already have before + * // submitting it. + * // N.B., The nb_packets > 0 check ensures we've submitted at least one + * // packet since the last call to opus_repacketizer_init(). Otherwise a + * // single packet longer than TARGET_DURATION_MS would cause us to try to + * // output an (invalid) empty packet. It also ensures that prev_toc has + * // been set to a valid value. Additionally, len[nb_packets] > 0 is + * // guaranteed by the call to opus_packet_get_nb_frames() above, so the + * // reference to data[nb_packets][0] should be valid. + * if (nb_packets > 0 && ( + * ((prev_toc & 0xFC) != (data[nb_packets][0] & 0xFC)) || + * opus_packet_get_samples_per_frame(data[nb_packets], 48000)*nb_frames > + * TARGET_DURATION_MS*48)) + * { + * out_len = opus_repacketizer_out(rp, out, sizeof(out)); + * if (out_len < 0) + * { + * release_packets(data, nb_packets+1); + * return (int)out_len; + * } + * output_next_packet(out, out_len); + * opus_repacketizer_init(rp); + * release_packets(data, nb_packets); + * data[0] = data[nb_packets]; + * len[0] = len[nb_packets]; + * nb_packets = 0; + * } + * err = opus_repacketizer_cat(rp, data[nb_packets], len[nb_packets]); + * if (err != OPUS_OK) + * { + * release_packets(data, nb_packets+1); + * return err; + * } + * prev_toc = data[nb_packets][0]; + * nb_packets++; + * } + * // Output the final, partial packet. + * if (nb_packets > 0) + * { + * out_len = opus_repacketizer_out(rp, out, sizeof(out)); + * release_packets(data, nb_packets); + * if (out_len < 0) + * return (int)out_len; + * output_next_packet(out, out_len); + * } + * @endcode + * + * An alternate way of merging packets is to simply call opus_repacketizer_cat() + * unconditionally until it fails. At that point, the merged packet can be + * obtained with opus_repacketizer_out() and the input packet for which + * opus_repacketizer_cat() needs to be re-added to a newly reinitialized + * repacketizer state. + */ + +typedef struct OpusRepacketizer OpusRepacketizer; + +/** Gets the size of an OpusRepacketizer structure. + * @returns The size in bytes. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_size(void); + +/** (Re)initializes a previously allocated repacketizer state. + * The state must be at least the size returned by opus_repacketizer_get_size(). + * This can be used for applications which use their own allocator instead of + * malloc(). + * It must also be called to reset the queue of packets waiting to be + * repacketized, which is necessary if the maximum packet duration of 120 ms + * is reached or if you wish to submit packets with a different Opus + * configuration (coding mode, audio bandwidth, frame size, or channel count). + * Failure to do so will prevent a new packet from being added with + * opus_repacketizer_cat(). + * @see opus_repacketizer_create + * @see opus_repacketizer_get_size + * @see opus_repacketizer_cat + * @param rp OpusRepacketizer*: The repacketizer state to + * (re)initialize. + * @returns A pointer to the same repacketizer state that was passed in. + */ +OPUS_EXPORT OpusRepacketizer *opus_repacketizer_init(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1); + +/** Allocates memory and initializes the new repacketizer with + * opus_repacketizer_init(). + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusRepacketizer *opus_repacketizer_create(void); + +/** Frees an OpusRepacketizer allocated by + * opus_repacketizer_create(). + * @param[in] rp OpusRepacketizer*: State to be freed. + */ +OPUS_EXPORT void opus_repacketizer_destroy(OpusRepacketizer *rp); + +/** Add a packet to the current repacketizer state. + * This packet must match the configuration of any packets already submitted + * for repacketization since the last call to opus_repacketizer_init(). + * This means that it must have the same coding mode, audio bandwidth, frame + * size, and channel count. + * This can be checked in advance by examining the top 6 bits of the first + * byte of the packet, and ensuring they match the top 6 bits of the first + * byte of any previously submitted packet. + * The total duration of audio in the repacketizer state also must not exceed + * 120 ms, the maximum duration of a single packet, after adding this packet. + * + * The contents of the current repacketizer state can be extracted into new + * packets using opus_repacketizer_out() or opus_repacketizer_out_range(). + * + * In order to add a packet with a different configuration or to add more + * audio beyond 120 ms, you must clear the repacketizer state by calling + * opus_repacketizer_init(). + * If a packet is too large to add to the current repacketizer state, no part + * of it is added, even if it contains multiple frames, some of which might + * fit. + * If you wish to be able to add parts of such packets, you should first use + * another repacketizer to split the packet into pieces and add them + * individually. + * @see opus_repacketizer_out_range + * @see opus_repacketizer_out + * @see opus_repacketizer_init + * @param rp OpusRepacketizer*: The repacketizer state to which to + * add the packet. + * @param[in] data const unsigned char*: The packet data. + * The application must ensure + * this pointer remains valid + * until the next call to + * opus_repacketizer_init() or + * opus_repacketizer_destroy(). + * @param len opus_int32: The number of bytes in the packet data. + * @returns An error code indicating whether or not the operation succeeded. + * @retval #OPUS_OK The packet's contents have been added to the repacketizer + * state. + * @retval #OPUS_INVALID_PACKET The packet did not have a valid TOC sequence, + * the packet's TOC sequence was not compatible + * with previously submitted packets (because + * the coding mode, audio bandwidth, frame size, + * or channel count did not match), or adding + * this packet would increase the total amount of + * audio stored in the repacketizer state to more + * than 120 ms. + */ +OPUS_EXPORT int opus_repacketizer_cat(OpusRepacketizer *rp, const unsigned char *data, opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); + + +/** Construct a new packet from data previously submitted to the repacketizer + * state via opus_repacketizer_cat(). + * @param rp OpusRepacketizer*: The repacketizer state from which to + * construct the new packet. + * @param begin int: The index of the first frame in the current + * repacketizer state to include in the output. + * @param end int: One past the index of the last frame in the + * current repacketizer state to include in the + * output. + * @param[out] data const unsigned char*: The buffer in which to + * store the output packet. + * @param maxlen opus_int32: The maximum number of bytes to store in + * the output buffer. In order to guarantee + * success, this should be at least + * 1276 for a single frame, + * or for multiple frames, + * 1277*(end-begin). + * However, 1*(end-begin) plus + * the size of all packet data submitted to + * the repacketizer since the last call to + * opus_repacketizer_init() or + * opus_repacketizer_create() is also + * sufficient, and possibly much smaller. + * @returns The total size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BAD_ARG [begin,end) was an invalid range of + * frames (begin < 0, begin >= end, or end > + * opus_repacketizer_get_nb_frames()). + * @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the + * complete output packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out_range(OpusRepacketizer *rp, int begin, int end, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Return the total number of frames contained in packet data submitted to + * the repacketizer state so far via opus_repacketizer_cat() since the last + * call to opus_repacketizer_init() or opus_repacketizer_create(). + * This defines the valid range of packets that can be extracted with + * opus_repacketizer_out_range() or opus_repacketizer_out(). + * @param rp OpusRepacketizer*: The repacketizer state containing the + * frames. + * @returns The total number of frames contained in the packet data submitted + * to the repacketizer state. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_nb_frames(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1); + +/** Construct a new packet from data previously submitted to the repacketizer + * state via opus_repacketizer_cat(). + * This is a convenience routine that returns all the data submitted so far + * in a single packet. + * It is equivalent to calling + * @code + * opus_repacketizer_out_range(rp, 0, opus_repacketizer_get_nb_frames(rp), + * data, maxlen) + * @endcode + * @param rp OpusRepacketizer*: The repacketizer state from which to + * construct the new packet. + * @param[out] data const unsigned char*: The buffer in which to + * store the output packet. + * @param maxlen opus_int32: The maximum number of bytes to store in + * the output buffer. In order to guarantee + * success, this should be at least + * 1277*opus_repacketizer_get_nb_frames(rp). + * However, + * 1*opus_repacketizer_get_nb_frames(rp) + * plus the size of all packet data + * submitted to the repacketizer since the + * last call to opus_repacketizer_init() or + * opus_repacketizer_create() is also + * sufficient, and possibly much smaller. + * @returns The total size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the + * complete output packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out(OpusRepacketizer *rp, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1); + +/** Pads a given Opus packet to a larger size (possibly changing the TOC sequence). + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to pad. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @param new_len opus_int32: The desired size of the packet after padding. + * This must be at least as large as len. + * @returns an error code + * @retval #OPUS_OK \a on success. + * @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT int opus_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len); + +/** Remove all padding from a given Opus packet and rewrite the TOC sequence to + * minimize space usage. + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to strip. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @returns The new size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BAD_ARG \a len was less than 1. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_packet_unpad(unsigned char *data, opus_int32 len); + +/** Pads a given Opus multi-stream packet to a larger size (possibly changing the TOC sequence). + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to pad. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @param new_len opus_int32: The desired size of the packet after padding. + * This must be at least 1. + * @param nb_streams opus_int32: The number of streams (not channels) in the packet. + * This must be at least as large as len. + * @returns an error code + * @retval #OPUS_OK \a on success. + * @retval #OPUS_BAD_ARG \a len was less than 1. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT int opus_multistream_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len, int nb_streams); + +/** Remove all padding from a given Opus multi-stream packet and rewrite the TOC sequence to + * minimize space usage. + * @param[in,out] data const unsigned char*: The buffer containing the + * packet to strip. + * @param len opus_int32: The size of the packet. + * This must be at least 1. + * @param nb_streams opus_int32: The number of streams (not channels) in the packet. + * This must be at least 1. + * @returns The new size of the output packet on success, or an error code + * on failure. + * @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len. + * @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_packet_unpad(unsigned char *data, opus_int32 len, int nb_streams); + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_H */ diff --git a/port/third_party/opus/include/opus_custom.h b/port/third_party/opus/include/opus_custom.h new file mode 100644 index 00000000..2f22d4b3 --- /dev/null +++ b/port/third_party/opus/include/opus_custom.h @@ -0,0 +1,343 @@ +/* Copyright (c) 2007-2008 CSIRO + Copyright (c) 2007-2009 Xiph.Org Foundation + Copyright (c) 2008-2012 Gregory Maxwell + Written by Jean-Marc Valin and Gregory Maxwell */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + @file opus_custom.h + @brief Opus-Custom reference implementation API + */ + +#ifndef OPUS_CUSTOM_H +#define OPUS_CUSTOM_H + +#include "opus_defines.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef CUSTOM_MODES +# define OPUS_CUSTOM_EXPORT OPUS_EXPORT +# define OPUS_CUSTOM_EXPORT_STATIC OPUS_EXPORT +#else +# define OPUS_CUSTOM_EXPORT +# ifdef OPUS_BUILD +# define OPUS_CUSTOM_EXPORT_STATIC static OPUS_INLINE +# else +# define OPUS_CUSTOM_EXPORT_STATIC +# endif +#endif + +/** @defgroup opus_custom Opus Custom + * @{ + * Opus Custom is an optional part of the Opus specification and + * reference implementation which uses a distinct API from the regular + * API and supports frame sizes that are not normally supported.\ Use + * of Opus Custom is discouraged for all but very special applications + * for which a frame size different from 2.5, 5, 10, or 20 ms is needed + * (for either complexity or latency reasons) and where interoperability + * is less important. + * + * In addition to the interoperability limitations the use of Opus custom + * disables a substantial chunk of the codec and generally lowers the + * quality available at a given bitrate. Normally when an application needs + * a different frame size from the codec it should buffer to match the + * sizes but this adds a small amount of delay which may be important + * in some very low latency applications. Some transports (especially + * constant rate RF transports) may also work best with frames of + * particular durations. + * + * Libopus only supports custom modes if they are enabled at compile time. + * + * The Opus Custom API is similar to the regular API but the + * @ref opus_encoder_create and @ref opus_decoder_create calls take + * an additional mode parameter which is a structure produced by + * a call to @ref opus_custom_mode_create. Both the encoder and decoder + * must create a mode using the same sample rate (fs) and frame size + * (frame size) so these parameters must either be signaled out of band + * or fixed in a particular implementation. + * + * Similar to regular Opus the custom modes support on the fly frame size + * switching, but the sizes available depend on the particular frame size in + * use. For some initial frame sizes on a single on the fly size is available. + */ + +/** Contains the state of an encoder. One encoder state is needed + for each stream. It is initialized once at the beginning of the + stream. Do *not* re-initialize the state for every frame. + @brief Encoder state + */ +typedef struct OpusCustomEncoder OpusCustomEncoder; + +/** State of the decoder. One decoder state is needed for each stream. + It is initialized once at the beginning of the stream. Do *not* + re-initialize the state for every frame. + @brief Decoder state + */ +typedef struct OpusCustomDecoder OpusCustomDecoder; + +/** The mode contains all the information necessary to create an + encoder. Both the encoder and decoder need to be initialized + with exactly the same mode, otherwise the output will be + corrupted. The mode MUST NOT BE DESTROYED until the encoders and + decoders that use it are destroyed as well. + @brief Mode configuration + */ +typedef struct OpusCustomMode OpusCustomMode; + +/** Creates a new mode struct. This will be passed to an encoder or + * decoder. The mode MUST NOT BE DESTROYED until the encoders and + * decoders that use it are destroyed as well. + * @param [in] Fs int: Sampling rate (8000 to 96000 Hz) + * @param [in] frame_size int: Number of samples (per channel) to encode in each + * packet (64 - 1024, prime factorization must contain zero or more 2s, 3s, or 5s and no other primes) + * @param [out] error int*: Returned error code (if NULL, no error will be returned) + * @return A newly created mode + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomMode *opus_custom_mode_create(opus_int32 Fs, int frame_size, int *error); + +/** Destroys a mode struct. Only call this after all encoders and + * decoders using this mode are destroyed as well. + * @param [in] mode OpusCustomMode*: Mode to be freed. + */ +OPUS_CUSTOM_EXPORT void opus_custom_mode_destroy(OpusCustomMode *mode); + + +#if !defined(OPUS_BUILD) || defined(CELT_ENCODER_C) + +/* Encoder */ +/** Gets the size of an OpusCustomEncoder structure. + * @param [in] mode OpusCustomMode *: Mode configuration + * @param [in] channels int: Number of channels + * @returns size + */ +OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_encoder_get_size( + const OpusCustomMode *mode, + int channels +) OPUS_ARG_NONNULL(1); + +# ifdef CUSTOM_MODES +/** Initializes a previously allocated encoder state + * The memory pointed to by st must be the size returned by opus_custom_encoder_get_size. + * This is intended for applications which use their own allocator instead of malloc. + * @see opus_custom_encoder_create(),opus_custom_encoder_get_size() + * To reset a previously initialized state use the OPUS_RESET_STATE CTL. + * @param [in] st OpusCustomEncoder*: Encoder state + * @param [in] mode OpusCustomMode *: Contains all the information about the characteristics of + * the stream (must be the same characteristics as used for the + * decoder) + * @param [in] channels int: Number of channels + * @return OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_CUSTOM_EXPORT int opus_custom_encoder_init( + OpusCustomEncoder *st, + const OpusCustomMode *mode, + int channels +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); +# endif +#endif + + +/** Creates a new encoder state. Each stream needs its own encoder + * state (can't be shared across simultaneous streams). + * @param [in] mode OpusCustomMode*: Contains all the information about the characteristics of + * the stream (must be the same characteristics as used for the + * decoder) + * @param [in] channels int: Number of channels + * @param [out] error int*: Returns an error code + * @return Newly created encoder state. +*/ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomEncoder *opus_custom_encoder_create( + const OpusCustomMode *mode, + int channels, + int *error +) OPUS_ARG_NONNULL(1); + + +/** Destroys an encoder state. + * @param[in] st OpusCustomEncoder*: State to be freed. + */ +OPUS_CUSTOM_EXPORT void opus_custom_encoder_destroy(OpusCustomEncoder *st); + +/** Encodes a frame of audio. + * @param [in] st OpusCustomEncoder*: Encoder state + * @param [in] pcm float*: PCM audio in float format, with a normal range of +/-1.0. + * Samples with a range beyond +/-1.0 are supported but will + * be clipped by decoders using the integer API and should + * only be used if it is known that the far end supports + * extended dynamic range. There must be exactly + * frame_size samples per channel. + * @param [in] frame_size int: Number of samples per frame of input signal + * @param [out] compressed char *: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long. + * @param [in] maxCompressedBytes int: Maximum number of bytes to use for compressing the frame + * (can change from one frame to another) + * @return Number of bytes written to "compressed". + * If negative, an error has occurred (see error codes). It is IMPORTANT that + * the length returned be somehow transmitted to the decoder. Otherwise, no + * decoding is possible. + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode_float( + OpusCustomEncoder *st, + const float *pcm, + int frame_size, + unsigned char *compressed, + int maxCompressedBytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Encodes a frame of audio. + * @param [in] st OpusCustomEncoder*: Encoder state + * @param [in] pcm opus_int16*: PCM audio in signed 16-bit format (native endian). + * There must be exactly frame_size samples per channel. + * @param [in] frame_size int: Number of samples per frame of input signal + * @param [out] compressed char *: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long. + * @param [in] maxCompressedBytes int: Maximum number of bytes to use for compressing the frame + * (can change from one frame to another) + * @return Number of bytes written to "compressed". + * If negative, an error has occurred (see error codes). It is IMPORTANT that + * the length returned be somehow transmitted to the decoder. Otherwise, no + * decoding is possible. + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode( + OpusCustomEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *compressed, + int maxCompressedBytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on an Opus custom encoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @see opus_encoderctls + */ +OPUS_CUSTOM_EXPORT int opus_custom_encoder_ctl(OpusCustomEncoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1); + + +#if !defined(OPUS_BUILD) || defined(CELT_DECODER_C) +/* Decoder */ + +/** Gets the size of an OpusCustomDecoder structure. + * @param [in] mode OpusCustomMode *: Mode configuration + * @param [in] channels int: Number of channels + * @returns size + */ +OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_decoder_get_size( + const OpusCustomMode *mode, + int channels +) OPUS_ARG_NONNULL(1); + +/** Initializes a previously allocated decoder state + * The memory pointed to by st must be the size returned by opus_custom_decoder_get_size. + * This is intended for applications which use their own allocator instead of malloc. + * @see opus_custom_decoder_create(),opus_custom_decoder_get_size() + * To reset a previously initialized state use the OPUS_RESET_STATE CTL. + * @param [in] st OpusCustomDecoder*: Decoder state + * @param [in] mode OpusCustomMode *: Contains all the information about the characteristics of + * the stream (must be the same characteristics as used for the + * encoder) + * @param [in] channels int: Number of channels + * @return OPUS_OK Success or @ref opus_errorcodes + */ +OPUS_CUSTOM_EXPORT_STATIC int opus_custom_decoder_init( + OpusCustomDecoder *st, + const OpusCustomMode *mode, + int channels +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); + +#endif + + +/** Creates a new decoder state. Each stream needs its own decoder state (can't + * be shared across simultaneous streams). + * @param [in] mode OpusCustomMode: Contains all the information about the characteristics of the + * stream (must be the same characteristics as used for the encoder) + * @param [in] channels int: Number of channels + * @param [out] error int*: Returns an error code + * @return Newly created decoder state. + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomDecoder *opus_custom_decoder_create( + const OpusCustomMode *mode, + int channels, + int *error +) OPUS_ARG_NONNULL(1); + +/** Destroys a decoder state. + * @param[in] st OpusCustomDecoder*: State to be freed. + */ +OPUS_CUSTOM_EXPORT void opus_custom_decoder_destroy(OpusCustomDecoder *st); + +/** Decode an opus custom frame with floating point output + * @param [in] st OpusCustomDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len int: Number of bytes in payload + * @param [out] pcm float*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(float) + * @param [in] frame_size Number of samples per channel of available space in *pcm. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode_float( + OpusCustomDecoder *st, + const unsigned char *data, + int len, + float *pcm, + int frame_size +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Decode an opus custom frame + * @param [in] st OpusCustomDecoder*: Decoder state + * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss + * @param [in] len int: Number of bytes in payload + * @param [out] pcm opus_int16*: Output signal (interleaved if 2 channels). length + * is frame_size*channels*sizeof(opus_int16) + * @param [in] frame_size Number of samples per channel of available space in *pcm. + * @returns Number of decoded samples or @ref opus_errorcodes + */ +OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode( + OpusCustomDecoder *st, + const unsigned char *data, + int len, + opus_int16 *pcm, + int frame_size +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on an Opus custom decoder. + * + * Generally the request and subsequent arguments are generated + * by a convenience macro. + * @see opus_genericctls + */ +OPUS_CUSTOM_EXPORT int opus_custom_decoder_ctl(OpusCustomDecoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1); + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_CUSTOM_H */ diff --git a/port/third_party/opus/include/opus_defines.h b/port/third_party/opus/include/opus_defines.h new file mode 100644 index 00000000..cd8f4dde --- /dev/null +++ b/port/third_party/opus/include/opus_defines.h @@ -0,0 +1,830 @@ +/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited + Written by Jean-Marc Valin and Koen Vos */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * @file opus_defines.h + * @brief Opus reference implementation constants + */ + +#ifndef OPUS_DEFINES_H +#define OPUS_DEFINES_H + +#include "opus_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup opus_errorcodes Error codes + * @{ + */ +/** No error @hideinitializer*/ +#define OPUS_OK 0 +/** One or more invalid/out of range arguments @hideinitializer*/ +#define OPUS_BAD_ARG -1 +/** Not enough bytes allocated in the buffer @hideinitializer*/ +#define OPUS_BUFFER_TOO_SMALL -2 +/** An internal error was detected @hideinitializer*/ +#define OPUS_INTERNAL_ERROR -3 +/** The compressed data passed is corrupted @hideinitializer*/ +#define OPUS_INVALID_PACKET -4 +/** Invalid/unsupported request number @hideinitializer*/ +#define OPUS_UNIMPLEMENTED -5 +/** An encoder or decoder structure is invalid or already freed @hideinitializer*/ +#define OPUS_INVALID_STATE -6 +/** Memory allocation has failed @hideinitializer*/ +#define OPUS_ALLOC_FAIL -7 +/**@}*/ + +/** @cond OPUS_INTERNAL_DOC */ +/**Export control for opus functions */ + +#ifndef OPUS_EXPORT +# if defined(_WIN32) +# if defined(OPUS_BUILD) && defined(DLL_EXPORT) +# define OPUS_EXPORT __declspec(dllexport) +# else +# define OPUS_EXPORT +# endif +# elif defined(__GNUC__) && defined(OPUS_BUILD) +# define OPUS_EXPORT __attribute__ ((visibility ("default"))) +# else +# define OPUS_EXPORT +# endif +#endif + +# if !defined(OPUS_GNUC_PREREQ) +# if defined(__GNUC__)&&defined(__GNUC_MINOR__) +# define OPUS_GNUC_PREREQ(_maj,_min) \ + ((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min)) +# else +# define OPUS_GNUC_PREREQ(_maj,_min) 0 +# endif +# endif + +#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) ) +# if OPUS_GNUC_PREREQ(3,0) +# define OPUS_RESTRICT __restrict__ +# elif (defined(_MSC_VER) && _MSC_VER >= 1400) +# define OPUS_RESTRICT __restrict +# else +# define OPUS_RESTRICT +# endif +#else +# define OPUS_RESTRICT restrict +#endif + +#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) ) +# if OPUS_GNUC_PREREQ(2,7) +# define OPUS_INLINE __inline__ +# elif (defined(_MSC_VER)) +# define OPUS_INLINE __inline +# else +# define OPUS_INLINE +# endif +#else +# define OPUS_INLINE inline +#endif + +/**Warning attributes for opus functions + * NONNULL is not used in OPUS_BUILD to avoid the compiler optimizing out + * some paranoid null checks. */ +#if defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4) +# define OPUS_WARN_UNUSED_RESULT __attribute__ ((__warn_unused_result__)) +#else +# define OPUS_WARN_UNUSED_RESULT +#endif +#if !defined(OPUS_BUILD) && defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4) +# define OPUS_ARG_NONNULL(_x) __attribute__ ((__nonnull__(_x))) +#else +# define OPUS_ARG_NONNULL(_x) +#endif + +/** These are the actual Encoder CTL ID numbers. + * They should not be used directly by applications. + * In general, SETs should be even and GETs should be odd.*/ +#define OPUS_SET_APPLICATION_REQUEST 4000 +#define OPUS_GET_APPLICATION_REQUEST 4001 +#define OPUS_SET_BITRATE_REQUEST 4002 +#define OPUS_GET_BITRATE_REQUEST 4003 +#define OPUS_SET_MAX_BANDWIDTH_REQUEST 4004 +#define OPUS_GET_MAX_BANDWIDTH_REQUEST 4005 +#define OPUS_SET_VBR_REQUEST 4006 +#define OPUS_GET_VBR_REQUEST 4007 +#define OPUS_SET_BANDWIDTH_REQUEST 4008 +#define OPUS_GET_BANDWIDTH_REQUEST 4009 +#define OPUS_SET_COMPLEXITY_REQUEST 4010 +#define OPUS_GET_COMPLEXITY_REQUEST 4011 +#define OPUS_SET_INBAND_FEC_REQUEST 4012 +#define OPUS_GET_INBAND_FEC_REQUEST 4013 +#define OPUS_SET_PACKET_LOSS_PERC_REQUEST 4014 +#define OPUS_GET_PACKET_LOSS_PERC_REQUEST 4015 +#define OPUS_SET_DTX_REQUEST 4016 +#define OPUS_GET_DTX_REQUEST 4017 +#define OPUS_SET_VBR_CONSTRAINT_REQUEST 4020 +#define OPUS_GET_VBR_CONSTRAINT_REQUEST 4021 +#define OPUS_SET_FORCE_CHANNELS_REQUEST 4022 +#define OPUS_GET_FORCE_CHANNELS_REQUEST 4023 +#define OPUS_SET_SIGNAL_REQUEST 4024 +#define OPUS_GET_SIGNAL_REQUEST 4025 +#define OPUS_GET_LOOKAHEAD_REQUEST 4027 +/* #define OPUS_RESET_STATE 4028 */ +#define OPUS_GET_SAMPLE_RATE_REQUEST 4029 +#define OPUS_GET_FINAL_RANGE_REQUEST 4031 +#define OPUS_GET_PITCH_REQUEST 4033 +#define OPUS_SET_GAIN_REQUEST 4034 +#define OPUS_GET_GAIN_REQUEST 4045 /* Should have been 4035 */ +#define OPUS_SET_LSB_DEPTH_REQUEST 4036 +#define OPUS_GET_LSB_DEPTH_REQUEST 4037 +#define OPUS_GET_LAST_PACKET_DURATION_REQUEST 4039 +#define OPUS_SET_EXPERT_FRAME_DURATION_REQUEST 4040 +#define OPUS_GET_EXPERT_FRAME_DURATION_REQUEST 4041 +#define OPUS_SET_PREDICTION_DISABLED_REQUEST 4042 +#define OPUS_GET_PREDICTION_DISABLED_REQUEST 4043 +/* Don't use 4045, it's already taken by OPUS_GET_GAIN_REQUEST */ +#define OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST 4046 +#define OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST 4047 +#define OPUS_GET_IN_DTX_REQUEST 4049 +#define OPUS_SET_DRED_DURATION_REQUEST 4050 +#define OPUS_GET_DRED_DURATION_REQUEST 4051 +#define OPUS_SET_DNN_BLOB_REQUEST 4052 +/*#define OPUS_GET_DNN_BLOB_REQUEST 4053 */ + +/** Defines for the presence of extended APIs. */ +#define OPUS_HAVE_OPUS_PROJECTION_H + +/* Macros to trigger compilation errors when the wrong types are provided to a CTL */ +#define __opus_check_int(x) (((void)((x) == (opus_int32)0)), (opus_int32)(x)) + +#ifdef DISABLE_PTR_CHECK +/* Disable checks to prevent ubsan from complaining about NULL checks + in test_opus_api. */ +#define __opus_check_int_ptr(ptr) (ptr) +#define __opus_check_uint_ptr(ptr) (ptr) +#define __opus_check_uint8_ptr(ptr) (ptr) +#define __opus_check_val16_ptr(ptr) (ptr) +#define __opus_check_void_ptr(ptr) (ptr) +#else +#define __opus_check_int_ptr(ptr) ((ptr) + ((ptr) - (opus_int32*)(ptr))) +#define __opus_check_uint_ptr(ptr) ((ptr) + ((ptr) - (opus_uint32*)(ptr))) +#define __opus_check_uint8_ptr(ptr) ((ptr) + ((ptr) - (opus_uint8*)(ptr))) +#define __opus_check_val16_ptr(ptr) ((ptr) + ((ptr) - (opus_val16*)(ptr))) +#define __opus_check_void_ptr(x) ((void)((void *)0 == (x)), (x)) +#endif +/** @endcond */ + +/** @defgroup opus_ctlvalues Pre-defined values for CTL interface + * @see opus_genericctls, opus_encoderctls + * @{ + */ +/* Values for the various encoder CTLs */ +#define OPUS_AUTO -1000 /**opus_int32: Allowed values: 0-10, inclusive. + * + * @hideinitializer */ +#define OPUS_SET_COMPLEXITY(x) OPUS_SET_COMPLEXITY_REQUEST, __opus_check_int(x) +/** Gets the encoder's complexity configuration. + * @see OPUS_SET_COMPLEXITY + * @param[out] x opus_int32 *: Returns a value in the range 0-10, + * inclusive. + * @hideinitializer */ +#define OPUS_GET_COMPLEXITY(x) OPUS_GET_COMPLEXITY_REQUEST, __opus_check_int_ptr(x) + +/** Configures the bitrate in the encoder. + * Rates from 500 to 512000 bits per second are meaningful, as well as the + * special values #OPUS_AUTO and #OPUS_BITRATE_MAX. + * The value #OPUS_BITRATE_MAX can be used to cause the codec to use as much + * rate as it can, which is useful for controlling the rate by adjusting the + * output buffer size. + * @see OPUS_GET_BITRATE + * @param[in] x opus_int32: Bitrate in bits per second. The default + * is determined based on the number of + * channels and the input sampling rate. + * @hideinitializer */ +#define OPUS_SET_BITRATE(x) OPUS_SET_BITRATE_REQUEST, __opus_check_int(x) +/** Gets the encoder's bitrate configuration. + * @see OPUS_SET_BITRATE + * @param[out] x opus_int32 *: Returns the bitrate in bits per second. + * The default is determined based on the + * number of channels and the input + * sampling rate. + * @hideinitializer */ +#define OPUS_GET_BITRATE(x) OPUS_GET_BITRATE_REQUEST, __opus_check_int_ptr(x) + +/** Enables or disables variable bitrate (VBR) in the encoder. + * The configured bitrate may not be met exactly because frames must + * be an integer number of bytes in length. + * @see OPUS_GET_VBR + * @see OPUS_SET_VBR_CONSTRAINT + * @param[in] x opus_int32: Allowed values: + *
    + *
    0
    Hard CBR. For LPC/hybrid modes at very low bit-rate, this can + * cause noticeable quality degradation.
    + *
    1
    VBR (default). The exact type of VBR is controlled by + * #OPUS_SET_VBR_CONSTRAINT.
    + *
    + * @hideinitializer */ +#define OPUS_SET_VBR(x) OPUS_SET_VBR_REQUEST, __opus_check_int(x) +/** Determine if variable bitrate (VBR) is enabled in the encoder. + * @see OPUS_SET_VBR + * @see OPUS_GET_VBR_CONSTRAINT + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    0
    Hard CBR.
    + *
    1
    VBR (default). The exact type of VBR may be retrieved via + * #OPUS_GET_VBR_CONSTRAINT.
    + *
    + * @hideinitializer */ +#define OPUS_GET_VBR(x) OPUS_GET_VBR_REQUEST, __opus_check_int_ptr(x) + +/** Enables or disables constrained VBR in the encoder. + * This setting is ignored when the encoder is in CBR mode. + * @warning Only the MDCT mode of Opus currently heeds the constraint. + * Speech mode ignores it completely, hybrid mode may fail to obey it + * if the LPC layer uses more bitrate than the constraint would have + * permitted. + * @see OPUS_GET_VBR_CONSTRAINT + * @see OPUS_SET_VBR + * @param[in] x opus_int32: Allowed values: + *
    + *
    0
    Unconstrained VBR.
    + *
    1
    Constrained VBR (default). This creates a maximum of one + * frame of buffering delay assuming a transport with a + * serialization speed of the nominal bitrate.
    + *
    + * @hideinitializer */ +#define OPUS_SET_VBR_CONSTRAINT(x) OPUS_SET_VBR_CONSTRAINT_REQUEST, __opus_check_int(x) +/** Determine if constrained VBR is enabled in the encoder. + * @see OPUS_SET_VBR_CONSTRAINT + * @see OPUS_GET_VBR + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    0
    Unconstrained VBR.
    + *
    1
    Constrained VBR (default).
    + *
    + * @hideinitializer */ +#define OPUS_GET_VBR_CONSTRAINT(x) OPUS_GET_VBR_CONSTRAINT_REQUEST, __opus_check_int_ptr(x) + +/** Configures mono/stereo forcing in the encoder. + * This can force the encoder to produce packets encoded as either mono or + * stereo, regardless of the format of the input audio. This is useful when + * the caller knows that the input signal is currently a mono source embedded + * in a stereo stream. + * @see OPUS_GET_FORCE_CHANNELS + * @param[in] x opus_int32: Allowed values: + *
    + *
    #OPUS_AUTO
    Not forced (default)
    + *
    1
    Forced mono
    + *
    2
    Forced stereo
    + *
    + * @hideinitializer */ +#define OPUS_SET_FORCE_CHANNELS(x) OPUS_SET_FORCE_CHANNELS_REQUEST, __opus_check_int(x) +/** Gets the encoder's forced channel configuration. + * @see OPUS_SET_FORCE_CHANNELS + * @param[out] x opus_int32 *: + *
    + *
    #OPUS_AUTO
    Not forced (default)
    + *
    1
    Forced mono
    + *
    2
    Forced stereo
    + *
    + * @hideinitializer */ +#define OPUS_GET_FORCE_CHANNELS(x) OPUS_GET_FORCE_CHANNELS_REQUEST, __opus_check_int_ptr(x) + +/** Configures the maximum bandpass that the encoder will select automatically. + * Applications should normally use this instead of #OPUS_SET_BANDWIDTH + * (leaving that set to the default, #OPUS_AUTO). This allows the + * application to set an upper bound based on the type of input it is + * providing, but still gives the encoder the freedom to reduce the bandpass + * when the bitrate becomes too low, for better overall quality. + * @see OPUS_GET_MAX_BANDWIDTH + * @param[in] x opus_int32: Allowed values: + *
    + *
    OPUS_BANDWIDTH_NARROWBAND
    4 kHz passband
    + *
    OPUS_BANDWIDTH_MEDIUMBAND
    6 kHz passband
    + *
    OPUS_BANDWIDTH_WIDEBAND
    8 kHz passband
    + *
    OPUS_BANDWIDTH_SUPERWIDEBAND
    12 kHz passband
    + *
    OPUS_BANDWIDTH_FULLBAND
    20 kHz passband (default)
    + *
    + * @hideinitializer */ +#define OPUS_SET_MAX_BANDWIDTH(x) OPUS_SET_MAX_BANDWIDTH_REQUEST, __opus_check_int(x) + +/** Gets the encoder's configured maximum allowed bandpass. + * @see OPUS_SET_MAX_BANDWIDTH + * @param[out] x opus_int32 *: Allowed values: + *
    + *
    #OPUS_BANDWIDTH_NARROWBAND
    4 kHz passband
    + *
    #OPUS_BANDWIDTH_MEDIUMBAND
    6 kHz passband
    + *
    #OPUS_BANDWIDTH_WIDEBAND
    8 kHz passband
    + *
    #OPUS_BANDWIDTH_SUPERWIDEBAND
    12 kHz passband
    + *
    #OPUS_BANDWIDTH_FULLBAND
    20 kHz passband (default)
    + *
    + * @hideinitializer */ +#define OPUS_GET_MAX_BANDWIDTH(x) OPUS_GET_MAX_BANDWIDTH_REQUEST, __opus_check_int_ptr(x) + +/** Sets the encoder's bandpass to a specific value. + * This prevents the encoder from automatically selecting the bandpass based + * on the available bitrate. If an application knows the bandpass of the input + * audio it is providing, it should normally use #OPUS_SET_MAX_BANDWIDTH + * instead, which still gives the encoder the freedom to reduce the bandpass + * when the bitrate becomes too low, for better overall quality. + * @see OPUS_GET_BANDWIDTH + * @param[in] x opus_int32: Allowed values: + *
    + *
    #OPUS_AUTO
    (default)
    + *
    #OPUS_BANDWIDTH_NARROWBAND
    4 kHz passband
    + *
    #OPUS_BANDWIDTH_MEDIUMBAND
    6 kHz passband
    + *
    #OPUS_BANDWIDTH_WIDEBAND
    8 kHz passband
    + *
    #OPUS_BANDWIDTH_SUPERWIDEBAND
    12 kHz passband
    + *
    #OPUS_BANDWIDTH_FULLBAND
    20 kHz passband
    + *
    + * @hideinitializer */ +#define OPUS_SET_BANDWIDTH(x) OPUS_SET_BANDWIDTH_REQUEST, __opus_check_int(x) + +/** Configures the type of signal being encoded. + * This is a hint which helps the encoder's mode selection. + * @see OPUS_GET_SIGNAL + * @param[in] x opus_int32: Allowed values: + *
    + *
    #OPUS_AUTO
    (default)
    + *
    #OPUS_SIGNAL_VOICE
    Bias thresholds towards choosing LPC or Hybrid modes.
    + *
    #OPUS_SIGNAL_MUSIC
    Bias thresholds towards choosing MDCT modes.
    + *
    + * @hideinitializer */ +#define OPUS_SET_SIGNAL(x) OPUS_SET_SIGNAL_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured signal type. + * @see OPUS_SET_SIGNAL + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    #OPUS_AUTO
    (default)
    + *
    #OPUS_SIGNAL_VOICE
    Bias thresholds towards choosing LPC or Hybrid modes.
    + *
    #OPUS_SIGNAL_MUSIC
    Bias thresholds towards choosing MDCT modes.
    + *
    + * @hideinitializer */ +#define OPUS_GET_SIGNAL(x) OPUS_GET_SIGNAL_REQUEST, __opus_check_int_ptr(x) + + +/** Configures the encoder's intended application. + * The initial value is a mandatory argument to the encoder_create function. + * @see OPUS_GET_APPLICATION + * @param[in] x opus_int32: Returns one of the following values: + *
    + *
    #OPUS_APPLICATION_VOIP
    + *
    Process signal for improved speech intelligibility.
    + *
    #OPUS_APPLICATION_AUDIO
    + *
    Favor faithfulness to the original input.
    + *
    #OPUS_APPLICATION_RESTRICTED_LOWDELAY
    + *
    Configure the minimum possible coding delay by disabling certain modes + * of operation.
    + *
    + * @hideinitializer */ +#define OPUS_SET_APPLICATION(x) OPUS_SET_APPLICATION_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured application. + * @see OPUS_SET_APPLICATION + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    #OPUS_APPLICATION_VOIP
    + *
    Process signal for improved speech intelligibility.
    + *
    #OPUS_APPLICATION_AUDIO
    + *
    Favor faithfulness to the original input.
    + *
    #OPUS_APPLICATION_RESTRICTED_LOWDELAY
    + *
    Configure the minimum possible coding delay by disabling certain modes + * of operation.
    + *
    + * @hideinitializer */ +#define OPUS_GET_APPLICATION(x) OPUS_GET_APPLICATION_REQUEST, __opus_check_int_ptr(x) + +/** Gets the total samples of delay added by the entire codec. + * This can be queried by the encoder and then the provided number of samples can be + * skipped on from the start of the decoder's output to provide time aligned input + * and output. From the perspective of a decoding application the real data begins this many + * samples late. + * + * The decoder contribution to this delay is identical for all decoders, but the + * encoder portion of the delay may vary from implementation to implementation, + * version to version, or even depend on the encoder's initial configuration. + * Applications needing delay compensation should call this CTL rather than + * hard-coding a value. + * @param[out] x opus_int32 *: Number of lookahead samples + * @hideinitializer */ +#define OPUS_GET_LOOKAHEAD(x) OPUS_GET_LOOKAHEAD_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's use of inband forward error correction (FEC). + * @note This is only applicable to the LPC layer + * @see OPUS_GET_INBAND_FEC + * @param[in] x opus_int32: Allowed values: + *
    + *
    0
    Disable inband FEC (default).
    + *
    1
    Inband FEC enabled. If the packet loss rate is sufficiently high, Opus will automatically switch to SILK even at high rates to enable use of that FEC.
    + *
    2
    Inband FEC enabled, but does not necessarily switch to SILK if we have music.
    + *
    + * @hideinitializer */ +#define OPUS_SET_INBAND_FEC(x) OPUS_SET_INBAND_FEC_REQUEST, __opus_check_int(x) +/** Gets encoder's configured use of inband forward error correction. + * @see OPUS_SET_INBAND_FEC + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    0
    Inband FEC disabled (default).
    + *
    1
    Inband FEC enabled. If the packet loss rate is sufficiently high, Opus will automatically switch to SILK even at high rates to enable use of that FEC.
    + *
    2
    Inband FEC enabled, but does not necessarily switch to SILK if we have music.
    + *
    + * @hideinitializer */ +#define OPUS_GET_INBAND_FEC(x) OPUS_GET_INBAND_FEC_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's expected packet loss percentage. + * Higher values trigger progressively more loss resistant behavior in the encoder + * at the expense of quality at a given bitrate in the absence of packet loss, but + * greater quality under loss. + * @see OPUS_GET_PACKET_LOSS_PERC + * @param[in] x opus_int32: Loss percentage in the range 0-100, inclusive (default: 0). + * @hideinitializer */ +#define OPUS_SET_PACKET_LOSS_PERC(x) OPUS_SET_PACKET_LOSS_PERC_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured packet loss percentage. + * @see OPUS_SET_PACKET_LOSS_PERC + * @param[out] x opus_int32 *: Returns the configured loss percentage + * in the range 0-100, inclusive (default: 0). + * @hideinitializer */ +#define OPUS_GET_PACKET_LOSS_PERC(x) OPUS_GET_PACKET_LOSS_PERC_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's use of discontinuous transmission (DTX). + * @note This is only applicable to the LPC layer + * @see OPUS_GET_DTX + * @param[in] x opus_int32: Allowed values: + *
    + *
    0
    Disable DTX (default).
    + *
    1
    Enabled DTX.
    + *
    + * @hideinitializer */ +#define OPUS_SET_DTX(x) OPUS_SET_DTX_REQUEST, __opus_check_int(x) +/** Gets encoder's configured use of discontinuous transmission. + * @see OPUS_SET_DTX + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    0
    DTX disabled (default).
    + *
    1
    DTX enabled.
    + *
    + * @hideinitializer */ +#define OPUS_GET_DTX(x) OPUS_GET_DTX_REQUEST, __opus_check_int_ptr(x) +/** Configures the depth of signal being encoded. + * + * This is a hint which helps the encoder identify silence and near-silence. + * It represents the number of significant bits of linear intensity below + * which the signal contains ignorable quantization or other noise. + * + * For example, OPUS_SET_LSB_DEPTH(14) would be an appropriate setting + * for G.711 u-law input. OPUS_SET_LSB_DEPTH(16) would be appropriate + * for 16-bit linear pcm input with opus_encode_float(). + * + * When using opus_encode() instead of opus_encode_float(), or when libopus + * is compiled for fixed-point, the encoder uses the minimum of the value + * set here and the value 16. + * + * @see OPUS_GET_LSB_DEPTH + * @param[in] x opus_int32: Input precision in bits, between 8 and 24 + * (default: 24). + * @hideinitializer */ +#define OPUS_SET_LSB_DEPTH(x) OPUS_SET_LSB_DEPTH_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured signal depth. + * @see OPUS_SET_LSB_DEPTH + * @param[out] x opus_int32 *: Input precision in bits, between 8 and + * 24 (default: 24). + * @hideinitializer */ +#define OPUS_GET_LSB_DEPTH(x) OPUS_GET_LSB_DEPTH_REQUEST, __opus_check_int_ptr(x) + +/** Configures the encoder's use of variable duration frames. + * When variable duration is enabled, the encoder is free to use a shorter frame + * size than the one requested in the opus_encode*() call. + * It is then the user's responsibility + * to verify how much audio was encoded by checking the ToC byte of the encoded + * packet. The part of the audio that was not encoded needs to be resent to the + * encoder for the next call. Do not use this option unless you really + * know what you are doing. + * @see OPUS_GET_EXPERT_FRAME_DURATION + * @param[in] x opus_int32: Allowed values: + *
    + *
    OPUS_FRAMESIZE_ARG
    Select frame size from the argument (default).
    + *
    OPUS_FRAMESIZE_2_5_MS
    Use 2.5 ms frames.
    + *
    OPUS_FRAMESIZE_5_MS
    Use 5 ms frames.
    + *
    OPUS_FRAMESIZE_10_MS
    Use 10 ms frames.
    + *
    OPUS_FRAMESIZE_20_MS
    Use 20 ms frames.
    + *
    OPUS_FRAMESIZE_40_MS
    Use 40 ms frames.
    + *
    OPUS_FRAMESIZE_60_MS
    Use 60 ms frames.
    + *
    OPUS_FRAMESIZE_80_MS
    Use 80 ms frames.
    + *
    OPUS_FRAMESIZE_100_MS
    Use 100 ms frames.
    + *
    OPUS_FRAMESIZE_120_MS
    Use 120 ms frames.
    + *
    + * @hideinitializer */ +#define OPUS_SET_EXPERT_FRAME_DURATION(x) OPUS_SET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured use of variable duration frames. + * @see OPUS_SET_EXPERT_FRAME_DURATION + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    OPUS_FRAMESIZE_ARG
    Select frame size from the argument (default).
    + *
    OPUS_FRAMESIZE_2_5_MS
    Use 2.5 ms frames.
    + *
    OPUS_FRAMESIZE_5_MS
    Use 5 ms frames.
    + *
    OPUS_FRAMESIZE_10_MS
    Use 10 ms frames.
    + *
    OPUS_FRAMESIZE_20_MS
    Use 20 ms frames.
    + *
    OPUS_FRAMESIZE_40_MS
    Use 40 ms frames.
    + *
    OPUS_FRAMESIZE_60_MS
    Use 60 ms frames.
    + *
    OPUS_FRAMESIZE_80_MS
    Use 80 ms frames.
    + *
    OPUS_FRAMESIZE_100_MS
    Use 100 ms frames.
    + *
    OPUS_FRAMESIZE_120_MS
    Use 120 ms frames.
    + *
    + * @hideinitializer */ +#define OPUS_GET_EXPERT_FRAME_DURATION(x) OPUS_GET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int_ptr(x) + +/** If set to 1, disables almost all use of prediction, making frames almost + * completely independent. This reduces quality. + * @see OPUS_GET_PREDICTION_DISABLED + * @param[in] x opus_int32: Allowed values: + *
    + *
    0
    Enable prediction (default).
    + *
    1
    Disable prediction.
    + *
    + * @hideinitializer */ +#define OPUS_SET_PREDICTION_DISABLED(x) OPUS_SET_PREDICTION_DISABLED_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured prediction status. + * @see OPUS_SET_PREDICTION_DISABLED + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    0
    Prediction enabled (default).
    + *
    1
    Prediction disabled.
    + *
    + * @hideinitializer */ +#define OPUS_GET_PREDICTION_DISABLED(x) OPUS_GET_PREDICTION_DISABLED_REQUEST, __opus_check_int_ptr(x) + +/** If non-zero, enables Deep Redundancy (DRED) and use the specified maximum number of 10-ms redundant frames + * @hideinitializer */ +#define OPUS_SET_DRED_DURATION(x) OPUS_SET_DRED_DURATION_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured Deep Redundancy (DRED) maximum number of frames. + * @hideinitializer */ +#define OPUS_GET_DRED_DURATION(x) OPUS_GET_DRED_DURATION_REQUEST, __opus_check_int_ptr(x) + +/** Provide external DNN weights from binary object (only when explicitly built without the weights) + * @hideinitializer */ +#define OPUS_SET_DNN_BLOB(data, len) OPUS_SET_DNN_BLOB_REQUEST, __opus_check_void_ptr(data), __opus_check_int(len) + + +/**@}*/ + +/** @defgroup opus_genericctls Generic CTLs + * + * These macros are used with the \c opus_decoder_ctl and + * \c opus_encoder_ctl calls to generate a particular + * request. + * + * When called on an \c OpusDecoder they apply to that + * particular decoder instance. When called on an + * \c OpusEncoder they apply to the corresponding setting + * on that encoder instance, if present. + * + * Some usage examples: + * + * @code + * int ret; + * opus_int32 pitch; + * ret = opus_decoder_ctl(dec_ctx, OPUS_GET_PITCH(&pitch)); + * if (ret == OPUS_OK) return ret; + * + * opus_encoder_ctl(enc_ctx, OPUS_RESET_STATE); + * opus_decoder_ctl(dec_ctx, OPUS_RESET_STATE); + * + * opus_int32 enc_bw, dec_bw; + * opus_encoder_ctl(enc_ctx, OPUS_GET_BANDWIDTH(&enc_bw)); + * opus_decoder_ctl(dec_ctx, OPUS_GET_BANDWIDTH(&dec_bw)); + * if (enc_bw != dec_bw) { + * printf("packet bandwidth mismatch!\n"); + * } + * @endcode + * + * @see opus_encoder, opus_decoder_ctl, opus_encoder_ctl, opus_decoderctls, opus_encoderctls + * @{ + */ + +/** Resets the codec state to be equivalent to a freshly initialized state. + * This should be called when switching streams in order to prevent + * the back to back decoding from giving different results from + * one at a time decoding. + * @hideinitializer */ +#define OPUS_RESET_STATE 4028 + +/** Gets the final state of the codec's entropy coder. + * This is used for testing purposes, + * The encoder and decoder state should be identical after coding a payload + * (assuming no data corruption or software bugs) + * + * @param[out] x opus_uint32 *: Entropy coder state + * + * @hideinitializer */ +#define OPUS_GET_FINAL_RANGE(x) OPUS_GET_FINAL_RANGE_REQUEST, __opus_check_uint_ptr(x) + +/** Gets the encoder's configured bandpass or the decoder's last bandpass. + * @see OPUS_SET_BANDWIDTH + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    #OPUS_AUTO
    (default)
    + *
    #OPUS_BANDWIDTH_NARROWBAND
    4 kHz passband
    + *
    #OPUS_BANDWIDTH_MEDIUMBAND
    6 kHz passband
    + *
    #OPUS_BANDWIDTH_WIDEBAND
    8 kHz passband
    + *
    #OPUS_BANDWIDTH_SUPERWIDEBAND
    12 kHz passband
    + *
    #OPUS_BANDWIDTH_FULLBAND
    20 kHz passband
    + *
    + * @hideinitializer */ +#define OPUS_GET_BANDWIDTH(x) OPUS_GET_BANDWIDTH_REQUEST, __opus_check_int_ptr(x) + +/** Gets the sampling rate the encoder or decoder was initialized with. + * This simply returns the Fs value passed to opus_encoder_init() + * or opus_decoder_init(). + * @param[out] x opus_int32 *: Sampling rate of encoder or decoder. + * @hideinitializer + */ +#define OPUS_GET_SAMPLE_RATE(x) OPUS_GET_SAMPLE_RATE_REQUEST, __opus_check_int_ptr(x) + +/** If set to 1, disables the use of phase inversion for intensity stereo, + * improving the quality of mono downmixes, but slightly reducing normal + * stereo quality. Disabling phase inversion in the decoder does not comply + * with RFC 6716, although it does not cause any interoperability issue and + * is expected to become part of the Opus standard once RFC 6716 is updated + * by draft-ietf-codec-opus-update. + * @see OPUS_GET_PHASE_INVERSION_DISABLED + * @param[in] x opus_int32: Allowed values: + *
    + *
    0
    Enable phase inversion (default).
    + *
    1
    Disable phase inversion.
    + *
    + * @hideinitializer */ +#define OPUS_SET_PHASE_INVERSION_DISABLED(x) OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured phase inversion status. + * @see OPUS_SET_PHASE_INVERSION_DISABLED + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    0
    Stereo phase inversion enabled (default).
    + *
    1
    Stereo phase inversion disabled.
    + *
    + * @hideinitializer */ +#define OPUS_GET_PHASE_INVERSION_DISABLED(x) OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST, __opus_check_int_ptr(x) +/** Gets the DTX state of the encoder. + * Returns whether the last encoded frame was either a comfort noise update + * during DTX or not encoded because of DTX. + * @param[out] x opus_int32 *: Returns one of the following values: + *
    + *
    0
    The encoder is not in DTX.
    + *
    1
    The encoder is in DTX.
    + *
    + * @hideinitializer */ +#define OPUS_GET_IN_DTX(x) OPUS_GET_IN_DTX_REQUEST, __opus_check_int_ptr(x) + +/**@}*/ + +/** @defgroup opus_decoderctls Decoder related CTLs + * @see opus_genericctls, opus_encoderctls, opus_decoder + * @{ + */ + +/** Configures decoder gain adjustment. + * Scales the decoded output by a factor specified in Q8 dB units. + * This has a maximum range of -32768 to 32767 inclusive, and returns + * OPUS_BAD_ARG otherwise. The default is zero indicating no adjustment. + * This setting survives decoder reset. + * + * gain = pow(10, x/(20.0*256)) + * + * @param[in] x opus_int32: Amount to scale PCM signal by in Q8 dB units. + * @hideinitializer */ +#define OPUS_SET_GAIN(x) OPUS_SET_GAIN_REQUEST, __opus_check_int(x) +/** Gets the decoder's configured gain adjustment. @see OPUS_SET_GAIN + * + * @param[out] x opus_int32 *: Amount to scale PCM signal by in Q8 dB units. + * @hideinitializer */ +#define OPUS_GET_GAIN(x) OPUS_GET_GAIN_REQUEST, __opus_check_int_ptr(x) + +/** Gets the duration (in samples) of the last packet successfully decoded or concealed. + * @param[out] x opus_int32 *: Number of samples (at current sampling rate). + * @hideinitializer */ +#define OPUS_GET_LAST_PACKET_DURATION(x) OPUS_GET_LAST_PACKET_DURATION_REQUEST, __opus_check_int_ptr(x) + +/** Gets the pitch of the last decoded frame, if available. + * This can be used for any post-processing algorithm requiring the use of pitch, + * e.g. time stretching/shortening. If the last frame was not voiced, or if the + * pitch was not coded in the frame, then zero is returned. + * + * This CTL is only implemented for decoder instances. + * + * @param[out] x opus_int32 *: pitch period at 48 kHz (or 0 if not available) + * + * @hideinitializer */ +#define OPUS_GET_PITCH(x) OPUS_GET_PITCH_REQUEST, __opus_check_int_ptr(x) + +/**@}*/ + +/** @defgroup opus_libinfo Opus library information functions + * @{ + */ + +/** Converts an opus error code into a human readable string. + * + * @param[in] error int: Error number + * @returns Error string + */ +OPUS_EXPORT const char *opus_strerror(int error); + +/** Gets the libopus version string. + * + * Applications may look for the substring "-fixed" in the version string to + * determine whether they have a fixed-point or floating-point build at + * runtime. + * + * @returns Version string + */ +OPUS_EXPORT const char *opus_get_version_string(void); +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_DEFINES_H */ diff --git a/port/third_party/opus/include/opus_multistream.h b/port/third_party/opus/include/opus_multistream.h new file mode 100644 index 00000000..824cc55a --- /dev/null +++ b/port/third_party/opus/include/opus_multistream.h @@ -0,0 +1,660 @@ +/* Copyright (c) 2011 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * @file opus_multistream.h + * @brief Opus reference implementation multistream API + */ + +#ifndef OPUS_MULTISTREAM_H +#define OPUS_MULTISTREAM_H + +#include "opus.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @cond OPUS_INTERNAL_DOC */ + +/** Macros to trigger compilation errors when the wrong types are provided to a + * CTL. */ +/**@{*/ +#define __opus_check_encstate_ptr(ptr) ((ptr) + ((ptr) - (OpusEncoder**)(ptr))) +#define __opus_check_decstate_ptr(ptr) ((ptr) + ((ptr) - (OpusDecoder**)(ptr))) +/**@}*/ + +/** These are the actual encoder and decoder CTL ID numbers. + * They should not be used directly by applications. + * In general, SETs should be even and GETs should be odd.*/ +/**@{*/ +#define OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST 5120 +#define OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST 5122 +/**@}*/ + +/** @endcond */ + +/** @defgroup opus_multistream_ctls Multistream specific encoder and decoder CTLs + * + * These are convenience macros that are specific to the + * opus_multistream_encoder_ctl() and opus_multistream_decoder_ctl() + * interface. + * The CTLs from @ref opus_genericctls, @ref opus_encoderctls, and + * @ref opus_decoderctls may be applied to a multistream encoder or decoder as + * well. + * In addition, you may retrieve the encoder or decoder state for an specific + * stream via #OPUS_MULTISTREAM_GET_ENCODER_STATE or + * #OPUS_MULTISTREAM_GET_DECODER_STATE and apply CTLs to it individually. + */ +/**@{*/ + +/** Gets the encoder state for an individual stream of a multistream encoder. + * @param[in] x opus_int32: The index of the stream whose encoder you + * wish to retrieve. + * This must be non-negative and less than + * the streams parameter used + * to initialize the encoder. + * @param[out] y OpusEncoder**: Returns a pointer to the given + * encoder state. + * @retval OPUS_BAD_ARG The index of the requested stream was out of range. + * @hideinitializer + */ +#define OPUS_MULTISTREAM_GET_ENCODER_STATE(x,y) OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST, __opus_check_int(x), __opus_check_encstate_ptr(y) + +/** Gets the decoder state for an individual stream of a multistream decoder. + * @param[in] x opus_int32: The index of the stream whose decoder you + * wish to retrieve. + * This must be non-negative and less than + * the streams parameter used + * to initialize the decoder. + * @param[out] y OpusDecoder**: Returns a pointer to the given + * decoder state. + * @retval OPUS_BAD_ARG The index of the requested stream was out of range. + * @hideinitializer + */ +#define OPUS_MULTISTREAM_GET_DECODER_STATE(x,y) OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST, __opus_check_int(x), __opus_check_decstate_ptr(y) + +/**@}*/ + +/** @defgroup opus_multistream Opus Multistream API + * @{ + * + * The multistream API allows individual Opus streams to be combined into a + * single packet, enabling support for up to 255 channels. Unlike an + * elementary Opus stream, the encoder and decoder must negotiate the channel + * configuration before the decoder can successfully interpret the data in the + * packets produced by the encoder. Some basic information, such as packet + * duration, can be computed without any special negotiation. + * + * The format for multistream Opus packets is defined in + * RFC 7845 + * and is based on the self-delimited Opus framing described in Appendix B of + * RFC 6716. + * Normal Opus packets are just a degenerate case of multistream Opus packets, + * and can be encoded or decoded with the multistream API by setting + * streams to 1 when initializing the encoder or + * decoder. + * + * Multistream Opus streams can contain up to 255 elementary Opus streams. + * These may be either "uncoupled" or "coupled", indicating that the decoder + * is configured to decode them to either 1 or 2 channels, respectively. + * The streams are ordered so that all coupled streams appear at the + * beginning. + * + * A mapping table defines which decoded channel i + * should be used for each input/output (I/O) channel j. This table is + * typically provided as an unsigned char array. + * Let i = mapping[j] be the index for I/O channel j. + * If i < 2*coupled_streams, then I/O channel j is + * encoded as the left channel of stream (i/2) if i + * is even, or as the right channel of stream (i/2) if + * i is odd. Otherwise, I/O channel j is encoded as + * mono in stream (i - coupled_streams), unless it has the special + * value 255, in which case it is omitted from the encoding entirely (the + * decoder will reproduce it as silence). Each value i must either + * be the special value 255 or be less than streams + coupled_streams. + * + * The output channels specified by the encoder + * should use the + * Vorbis + * channel ordering. A decoder may wish to apply an additional permutation + * to the mapping the encoder used to achieve a different output channel + * order (e.g. for outputting in WAV order). + * + * Each multistream packet contains an Opus packet for each stream, and all of + * the Opus packets in a single multistream packet must have the same + * duration. Therefore the duration of a multistream packet can be extracted + * from the TOC sequence of the first stream, which is located at the + * beginning of the packet, just like an elementary Opus stream: + * + * @code + * int nb_samples; + * int nb_frames; + * nb_frames = opus_packet_get_nb_frames(data, len); + * if (nb_frames < 1) + * return nb_frames; + * nb_samples = opus_packet_get_samples_per_frame(data, 48000) * nb_frames; + * @endcode + * + * The general encoding and decoding process proceeds exactly the same as in + * the normal @ref opus_encoder and @ref opus_decoder APIs. + * See their documentation for an overview of how to use the corresponding + * multistream functions. + */ + +/** Opus multistream encoder state. + * This contains the complete state of a multistream Opus encoder. + * It is position independent and can be freely copied. + * @see opus_multistream_encoder_create + * @see opus_multistream_encoder_init + */ +typedef struct OpusMSEncoder OpusMSEncoder; + +/** Opus multistream decoder state. + * This contains the complete state of a multistream Opus decoder. + * It is position independent and can be freely copied. + * @see opus_multistream_decoder_create + * @see opus_multistream_decoder_init + */ +typedef struct OpusMSDecoder OpusMSDecoder; + +/**\name Multistream encoder functions */ +/**@{*/ + +/** Gets the size of an OpusMSEncoder structure. + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_encoder_get_size( + int streams, + int coupled_streams +); + +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_surround_encoder_get_size( + int channels, + int mapping_family +); + + +/** Allocates and initializes a multistream encoder state. + * Call opus_multistream_encoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param[in] mapping const unsigned char[channels]: Mapping from + * encoded channels to input channels, as described in + * @ref opus_multistream. As an extra constraint, the + * multistream encoder does not allow encoding coupled + * streams for which one channel is unused since this + * is never a good idea. + * @param application int: The target encoder application. + * This must be one of the following: + *
    + *
    #OPUS_APPLICATION_VOIP
    + *
    Process signal for improved speech intelligibility.
    + *
    #OPUS_APPLICATION_AUDIO
    + *
    Favor faithfulness to the original input.
    + *
    #OPUS_APPLICATION_RESTRICTED_LOWDELAY
    + *
    Configure the minimum possible coding delay by disabling certain modes + * of operation.
    + *
    + * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_encoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int application, + int *error +) OPUS_ARG_NONNULL(5); + +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_surround_encoder_create( + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + unsigned char *mapping, + int application, + int *error +) OPUS_ARG_NONNULL(4) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6); + +/** Initialize a previously allocated multistream encoder state. + * The memory pointed to by \a st must be at least the size returned by + * opus_multistream_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_multistream_encoder_create + * @see opus_multistream_encoder_get_size + * @param st OpusMSEncoder*: Multistream encoder state to initialize. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param[in] mapping const unsigned char[channels]: Mapping from + * encoded channels to input channels, as described in + * @ref opus_multistream. As an extra constraint, the + * multistream encoder does not allow encoding coupled + * streams for which one channel is unused since this + * is never a good idea. + * @param application int: The target encoder application. + * This must be one of the following: + *
    + *
    #OPUS_APPLICATION_VOIP
    + *
    Process signal for improved speech intelligibility.
    + *
    #OPUS_APPLICATION_AUDIO
    + *
    Favor faithfulness to the original input.
    + *
    #OPUS_APPLICATION_RESTRICTED_LOWDELAY
    + *
    Configure the minimum possible coding delay by disabling certain modes + * of operation.
    + *
    + * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_multistream_encoder_init( + OpusMSEncoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +OPUS_EXPORT int opus_multistream_surround_encoder_init( + OpusMSEncoder *st, + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + unsigned char *mapping, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6) OPUS_ARG_NONNULL(7); + +/** Encodes a multistream Opus frame. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param[in] pcm const opus_int16*: The input signal as interleaved + * samples. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode( + OpusMSEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Encodes a multistream Opus frame from floating point input. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param[in] pcm const float*: The input signal as interleaved + * samples with a normal range of + * +/-1.0. + * Samples with a range beyond +/-1.0 + * are supported but will be clipped by + * decoders using the integer API and + * should only be used if it is known + * that the far end supports extended + * dynamic range. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode_float( + OpusMSEncoder *st, + const float *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Frees an OpusMSEncoder allocated by + * opus_multistream_encoder_create(). + * @param st OpusMSEncoder*: Multistream encoder state to be freed. + */ +OPUS_EXPORT void opus_multistream_encoder_destroy(OpusMSEncoder *st); + +/** Perform a CTL function on a multistream Opus encoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_encoderctls, or @ref opus_multistream_ctls. + * @see opus_genericctls + * @see opus_encoderctls + * @see opus_multistream_ctls + */ +OPUS_EXPORT int opus_multistream_encoder_ctl(OpusMSEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/**@}*/ + +/**\name Multistream decoder functions */ +/**@{*/ + +/** Gets the size of an OpusMSDecoder structure. + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_decoder_get_size( + int streams, + int coupled_streams +); + +/** Allocates and initializes a multistream decoder state. + * Call opus_multistream_decoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] mapping const unsigned char[channels]: Mapping from + * coded channels to output channels, as described in + * @ref opus_multistream. + * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSDecoder *opus_multistream_decoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int *error +) OPUS_ARG_NONNULL(5); + +/** Intialize a previously allocated decoder state object. + * The memory pointed to by \a st must be at least the size returned by + * opus_multistream_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_multistream_decoder_create + * @see opus_multistream_deocder_get_size + * @param st OpusMSEncoder*: Multistream encoder state to initialize. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] mapping const unsigned char[channels]: Mapping from + * coded channels to output channels, as described in + * @ref opus_multistream. + * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_multistream_decoder_init( + OpusMSDecoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +/** Decode a multistream Opus packet. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode( + OpusMSDecoder *st, + const unsigned char *data, + opus_int32 len, + opus_int16 *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Decode a multistream Opus packet with floating point output. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode_float( + OpusMSDecoder *st, + const unsigned char *data, + opus_int32 len, + float *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on a multistream Opus decoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_decoderctls, or @ref opus_multistream_ctls. + * @see opus_genericctls + * @see opus_decoderctls + * @see opus_multistream_ctls + */ +OPUS_EXPORT int opus_multistream_decoder_ctl(OpusMSDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/** Frees an OpusMSDecoder allocated by + * opus_multistream_decoder_create(). + * @param st OpusMSDecoder: Multistream decoder state to be freed. + */ +OPUS_EXPORT void opus_multistream_decoder_destroy(OpusMSDecoder *st); + +/**@}*/ + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_MULTISTREAM_H */ diff --git a/port/third_party/opus/include/opus_projection.h b/port/third_party/opus/include/opus_projection.h new file mode 100644 index 00000000..9dabf4e8 --- /dev/null +++ b/port/third_party/opus/include/opus_projection.h @@ -0,0 +1,568 @@ +/* Copyright (c) 2017 Google Inc. + Written by Andrew Allen */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * @file opus_projection.h + * @brief Opus projection reference API + */ + +#ifndef OPUS_PROJECTION_H +#define OPUS_PROJECTION_H + +#include "opus_multistream.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @cond OPUS_INTERNAL_DOC */ + +/** These are the actual encoder and decoder CTL ID numbers. + * They should not be used directly by applications.c + * In general, SETs should be even and GETs should be odd.*/ +/**@{*/ +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST 6001 +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST 6003 +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST 6005 +/**@}*/ + + +/** @endcond */ + +/** @defgroup opus_projection_ctls Projection specific encoder and decoder CTLs + * + * These are convenience macros that are specific to the + * opus_projection_encoder_ctl() and opus_projection_decoder_ctl() + * interface. + * The CTLs from @ref opus_genericctls, @ref opus_encoderctls, + * @ref opus_decoderctls, and @ref opus_multistream_ctls may be applied to a + * projection encoder or decoder as well. + */ +/**@{*/ + +/** Gets the gain (in dB. S7.8-format) of the demixing matrix from the encoder. + * @param[out] x opus_int32 *: Returns the gain (in dB. S7.8-format) + * of the demixing matrix. + * @hideinitializer + */ +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN(x) OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST, __opus_check_int_ptr(x) + + +/** Gets the size in bytes of the demixing matrix from the encoder. + * @param[out] x opus_int32 *: Returns the size in bytes of the + * demixing matrix. + * @hideinitializer + */ +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE(x) OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST, __opus_check_int_ptr(x) + + +/** Copies the demixing matrix to the supplied pointer location. + * @param[out] x unsigned char *: Returns the demixing matrix to the + * supplied pointer location. + * @param y opus_int32: The size in bytes of the reserved memory at the + * pointer location. + * @hideinitializer + */ +#define OPUS_PROJECTION_GET_DEMIXING_MATRIX(x,y) OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST, x, __opus_check_int(y) + + +/**@}*/ + +/** Opus projection encoder state. + * This contains the complete state of a projection Opus encoder. + * It is position independent and can be freely copied. + * @see opus_projection_ambisonics_encoder_create + */ +typedef struct OpusProjectionEncoder OpusProjectionEncoder; + + +/** Opus projection decoder state. + * This contains the complete state of a projection Opus decoder. + * It is position independent and can be freely copied. + * @see opus_projection_decoder_create + * @see opus_projection_decoder_init + */ +typedef struct OpusProjectionDecoder OpusProjectionDecoder; + + +/**\name Projection encoder functions */ +/**@{*/ + +/** Gets the size of an OpusProjectionEncoder structure. + * @param channels int: The total number of input channels to encode. + * This must be no more than 255. + * @param mapping_family int: The mapping family to use for selecting + * the appropriate projection. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_projection_ambisonics_encoder_get_size( + int channels, + int mapping_family +); + + +/** Allocates and initializes a projection encoder state. + * Call opus_projection_encoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param mapping_family int: The mapping family to use for selecting + * the appropriate projection. + * @param[out] streams int *: The total number of streams that will + * be encoded from the input. + * @param[out] coupled_streams int *: Number of coupled (2 channel) + * streams that will be encoded from the input. + * @param application int: The target encoder application. + * This must be one of the following: + *
    + *
    #OPUS_APPLICATION_VOIP
    + *
    Process signal for improved speech intelligibility.
    + *
    #OPUS_APPLICATION_AUDIO
    + *
    Favor faithfulness to the original input.
    + *
    #OPUS_APPLICATION_RESTRICTED_LOWDELAY
    + *
    Configure the minimum possible coding delay by disabling certain modes + * of operation.
    + *
    + * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusProjectionEncoder *opus_projection_ambisonics_encoder_create( + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + int application, + int *error +) OPUS_ARG_NONNULL(4) OPUS_ARG_NONNULL(5); + + +/** Initialize a previously allocated projection encoder state. + * The memory pointed to by \a st must be at least the size returned by + * opus_projection_ambisonics_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_projection_ambisonics_encoder_create + * @see opus_projection_ambisonics_encoder_get_size + * @param st OpusProjectionEncoder*: Projection encoder state to initialize. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param application int: The target encoder application. + * This must be one of the following: + *
    + *
    #OPUS_APPLICATION_VOIP
    + *
    Process signal for improved speech intelligibility.
    + *
    #OPUS_APPLICATION_AUDIO
    + *
    Favor faithfulness to the original input.
    + *
    #OPUS_APPLICATION_RESTRICTED_LOWDELAY
    + *
    Configure the minimum possible coding delay by disabling certain modes + * of operation.
    + *
    + * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_projection_ambisonics_encoder_init( + OpusProjectionEncoder *st, + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6); + + +/** Encodes a projection Opus frame. + * @param st OpusProjectionEncoder*: Projection encoder state. + * @param[in] pcm const opus_int16*: The input signal as interleaved + * samples. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_encode( + OpusProjectionEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + + +/** Encodes a projection Opus frame from floating point input. + * @param st OpusProjectionEncoder*: Projection encoder state. + * @param[in] pcm const float*: The input signal as interleaved + * samples with a normal range of + * +/-1.0. + * Samples with a range beyond +/-1.0 + * are supported but will be clipped by + * decoders using the integer API and + * should only be used if it is known + * that the far end supports extended + * dynamic range. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_encode_float( + OpusProjectionEncoder *st, + const float *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + + +/** Frees an OpusProjectionEncoder allocated by + * opus_projection_ambisonics_encoder_create(). + * @param st OpusProjectionEncoder*: Projection encoder state to be freed. + */ +OPUS_EXPORT void opus_projection_encoder_destroy(OpusProjectionEncoder *st); + + +/** Perform a CTL function on a projection Opus encoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusProjectionEncoder*: Projection encoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_encoderctls, @ref opus_multistream_ctls, or + * @ref opus_projection_ctls + * @see opus_genericctls + * @see opus_encoderctls + * @see opus_multistream_ctls + * @see opus_projection_ctls + */ +OPUS_EXPORT int opus_projection_encoder_ctl(OpusProjectionEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); + + +/**@}*/ + +/**\name Projection decoder functions */ +/**@{*/ + +/** Gets the size of an OpusProjectionDecoder structure. + * @param channels int: The total number of output channels. + * This must be no more than 255. + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_projection_decoder_get_size( + int channels, + int streams, + int coupled_streams +); + + +/** Allocates and initializes a projection decoder state. + * Call opus_projection_decoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] demixing_matrix const unsigned char[demixing_matrix_size]: Demixing matrix + * that mapping from coded channels to output channels, + * as described in @ref opus_projection and + * @ref opus_projection_ctls. + * @param demixing_matrix_size opus_int32: The size in bytes of the + * demixing matrix, as + * described in @ref + * opus_projection_ctls. + * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusProjectionDecoder *opus_projection_decoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + unsigned char *demixing_matrix, + opus_int32 demixing_matrix_size, + int *error +) OPUS_ARG_NONNULL(5); + + +/** Intialize a previously allocated projection decoder state object. + * The memory pointed to by \a st must be at least the size returned by + * opus_projection_decoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_projection_decoder_create + * @see opus_projection_deocder_get_size + * @param st OpusProjectionDecoder*: Projection encoder state to initialize. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] demixing_matrix const unsigned char[demixing_matrix_size]: Demixing matrix + * that mapping from coded channels to output channels, + * as described in @ref opus_projection and + * @ref opus_projection_ctls. + * @param demixing_matrix_size opus_int32: The size in bytes of the + * demixing matrix, as + * described in @ref + * opus_projection_ctls. + * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_projection_decoder_init( + OpusProjectionDecoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + unsigned char *demixing_matrix, + opus_int32 demixing_matrix_size +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + + +/** Decode a projection Opus packet. + * @param st OpusProjectionDecoder*: Projection decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_decode( + OpusProjectionDecoder *st, + const unsigned char *data, + opus_int32 len, + opus_int16 *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + + +/** Decode a projection Opus packet with floating point output. + * @param st OpusProjectionDecoder*: Projection decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_decode_float( + OpusProjectionDecoder *st, + const unsigned char *data, + opus_int32 len, + float *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + + +/** Perform a CTL function on a projection Opus decoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusProjectionDecoder*: Projection decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_decoderctls, @ref opus_multistream_ctls, or + * @ref opus_projection_ctls. + * @see opus_genericctls + * @see opus_decoderctls + * @see opus_multistream_ctls + * @see opus_projection_ctls + */ +OPUS_EXPORT int opus_projection_decoder_ctl(OpusProjectionDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); + + +/** Frees an OpusProjectionDecoder allocated by + * opus_projection_decoder_create(). + * @param st OpusProjectionDecoder: Projection decoder state to be freed. + */ +OPUS_EXPORT void opus_projection_decoder_destroy(OpusProjectionDecoder *st); + + +/**@}*/ + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_PROJECTION_H */ diff --git a/port/third_party/opus/include/opus_types.h b/port/third_party/opus/include/opus_types.h new file mode 100644 index 00000000..7cf67558 --- /dev/null +++ b/port/third_party/opus/include/opus_types.h @@ -0,0 +1,166 @@ +/* (C) COPYRIGHT 1994-2002 Xiph.Org Foundation */ +/* Modified by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +/* opus_types.h based on ogg_types.h from libogg */ + +/** + @file opus_types.h + @brief Opus reference implementation types +*/ +#ifndef OPUS_TYPES_H +#define OPUS_TYPES_H + +#define opus_int int /* used for counters etc; at least 16 bits */ +#define opus_int64 long long +#define opus_int8 signed char + +#define opus_uint unsigned int /* used for counters etc; at least 16 bits */ +#define opus_uint64 unsigned long long +#define opus_uint8 unsigned char + +/* Use the real stdint.h if it's there (taken from Paul Hsieh's pstdint.h) */ +#if (defined(__STDC__) && __STDC__ && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_)) || defined (HAVE_STDINT_H)) +#include +# undef opus_int64 +# undef opus_int8 +# undef opus_uint64 +# undef opus_uint8 + typedef int8_t opus_int8; + typedef uint8_t opus_uint8; + typedef int16_t opus_int16; + typedef uint16_t opus_uint16; + typedef int32_t opus_int32; + typedef uint32_t opus_uint32; + typedef int64_t opus_int64; + typedef uint64_t opus_uint64; +#elif defined(_WIN32) + +# if defined(__CYGWIN__) +# include <_G_config.h> + typedef _G_int32_t opus_int32; + typedef _G_uint32_t opus_uint32; + typedef _G_int16 opus_int16; + typedef _G_uint16 opus_uint16; +# elif defined(__MINGW32__) + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; +# elif defined(__MWERKS__) + typedef int opus_int32; + typedef unsigned int opus_uint32; + typedef short opus_int16; + typedef unsigned short opus_uint16; +# else + /* MSVC/Borland */ + typedef __int32 opus_int32; + typedef unsigned __int32 opus_uint32; + typedef __int16 opus_int16; + typedef unsigned __int16 opus_uint16; +# endif + +#elif defined(__MACOS__) + +# include + typedef SInt16 opus_int16; + typedef UInt16 opus_uint16; + typedef SInt32 opus_int32; + typedef UInt32 opus_uint32; + +#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */ + +# include + typedef int16_t opus_int16; + typedef u_int16_t opus_uint16; + typedef int32_t opus_int32; + typedef u_int32_t opus_uint32; + +#elif defined(__BEOS__) + + /* Be */ +# include + typedef int16 opus_int16; + typedef u_int16 opus_uint16; + typedef int32_t opus_int32; + typedef u_int32_t opus_uint32; + +#elif defined (__EMX__) + + /* OS/2 GCC */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined (DJGPP) + + /* DJGPP */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined(R5900) + + /* PS2 EE */ + typedef int opus_int32; + typedef unsigned opus_uint32; + typedef short opus_int16; + typedef unsigned short opus_uint16; + +#elif defined(__SYMBIAN32__) + + /* Symbian GCC */ + typedef signed short opus_int16; + typedef unsigned short opus_uint16; + typedef signed int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X) + + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef long opus_int32; + typedef unsigned long opus_uint32; + +#elif defined(CONFIG_TI_C6X) + + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#else + + /* Give up, take a reasonable guess */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#endif + +#endif /* OPUS_TYPES_H */ diff --git a/port/third_party/opus/silk/A2NLSF.c b/port/third_party/opus/silk/A2NLSF.c new file mode 100644 index 00000000..b487686f --- /dev/null +++ b/port/third_party/opus/silk/A2NLSF.c @@ -0,0 +1,267 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +/* Conversion between prediction filter coefficients and NLSFs */ +/* Requires the order to be an even number */ +/* A piecewise linear approximation maps LSF <-> cos(LSF) */ +/* Therefore the result is not accurate NLSFs, but the two */ +/* functions are accurate inverses of each other */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "tables.h" + +/* Number of binary divisions, when not in low complexity mode */ +#define BIN_DIV_STEPS_A2NLSF_FIX 3 /* must be no higher than 16 - log2( LSF_COS_TAB_SZ_FIX ) */ +#define MAX_ITERATIONS_A2NLSF_FIX 16 + +/* Helper function for A2NLSF(..) */ +/* Transforms polynomials from cos(n*f) to cos(f)^n */ +static OPUS_INLINE void silk_A2NLSF_trans_poly( + opus_int32 *p, /* I/O Polynomial */ + const opus_int dd /* I Polynomial order (= filter order / 2 ) */ +) +{ + opus_int k, n; + + for( k = 2; k <= dd; k++ ) { + for( n = dd; n > k; n-- ) { + p[ n - 2 ] -= p[ n ]; + } + p[ k - 2 ] -= silk_LSHIFT( p[ k ], 1 ); + } +} +/* Helper function for A2NLSF(..) */ +/* Polynomial evaluation */ +static OPUS_INLINE opus_int32 silk_A2NLSF_eval_poly( /* return the polynomial evaluation, in Q16 */ + opus_int32 *p, /* I Polynomial, Q16 */ + const opus_int32 x, /* I Evaluation point, Q12 */ + const opus_int dd /* I Order */ +) +{ + opus_int n; + opus_int32 x_Q16, y32; + + y32 = p[ dd ]; /* Q16 */ + x_Q16 = silk_LSHIFT( x, 4 ); + + if ( opus_likely( 8 == dd ) ) + { + y32 = silk_SMLAWW( p[ 7 ], y32, x_Q16 ); + y32 = silk_SMLAWW( p[ 6 ], y32, x_Q16 ); + y32 = silk_SMLAWW( p[ 5 ], y32, x_Q16 ); + y32 = silk_SMLAWW( p[ 4 ], y32, x_Q16 ); + y32 = silk_SMLAWW( p[ 3 ], y32, x_Q16 ); + y32 = silk_SMLAWW( p[ 2 ], y32, x_Q16 ); + y32 = silk_SMLAWW( p[ 1 ], y32, x_Q16 ); + y32 = silk_SMLAWW( p[ 0 ], y32, x_Q16 ); + } + else + { + for( n = dd - 1; n >= 0; n-- ) { + y32 = silk_SMLAWW( p[ n ], y32, x_Q16 ); /* Q16 */ + } + } + return y32; +} + +static OPUS_INLINE void silk_A2NLSF_init( + const opus_int32 *a_Q16, + opus_int32 *P, + opus_int32 *Q, + const opus_int dd +) +{ + opus_int k; + + /* Convert filter coefs to even and odd polynomials */ + P[dd] = silk_LSHIFT( 1, 16 ); + Q[dd] = silk_LSHIFT( 1, 16 ); + for( k = 0; k < dd; k++ ) { + P[ k ] = -a_Q16[ dd - k - 1 ] - a_Q16[ dd + k ]; /* Q16 */ + Q[ k ] = -a_Q16[ dd - k - 1 ] + a_Q16[ dd + k ]; /* Q16 */ + } + + /* Divide out zeros as we have that for even filter orders, */ + /* z = 1 is always a root in Q, and */ + /* z = -1 is always a root in P */ + for( k = dd; k > 0; k-- ) { + P[ k - 1 ] -= P[ k ]; + Q[ k - 1 ] += Q[ k ]; + } + + /* Transform polynomials from cos(n*f) to cos(f)^n */ + silk_A2NLSF_trans_poly( P, dd ); + silk_A2NLSF_trans_poly( Q, dd ); +} + +/* Compute Normalized Line Spectral Frequencies (NLSFs) from whitening filter coefficients */ +/* If not all roots are found, the a_Q16 coefficients are bandwidth expanded until convergence. */ +void silk_A2NLSF( + opus_int16 *NLSF, /* O Normalized Line Spectral Frequencies in Q15 (0..2^15-1) [d] */ + opus_int32 *a_Q16, /* I/O Monic whitening filter coefficients in Q16 [d] */ + const opus_int d /* I Filter order (must be even) */ +) +{ + opus_int i, k, m, dd, root_ix, ffrac; + opus_int32 xlo, xhi, xmid; + opus_int32 ylo, yhi, ymid, thr; + opus_int32 nom, den; + opus_int32 P[ SILK_MAX_ORDER_LPC / 2 + 1 ]; + opus_int32 Q[ SILK_MAX_ORDER_LPC / 2 + 1 ]; + opus_int32 *PQ[ 2 ]; + opus_int32 *p; + + /* Store pointers to array */ + PQ[ 0 ] = P; + PQ[ 1 ] = Q; + + dd = silk_RSHIFT( d, 1 ); + + silk_A2NLSF_init( a_Q16, P, Q, dd ); + + /* Find roots, alternating between P and Q */ + p = P; /* Pointer to polynomial */ + + xlo = silk_LSFCosTab_FIX_Q12[ 0 ]; /* Q12*/ + ylo = silk_A2NLSF_eval_poly( p, xlo, dd ); + + if( ylo < 0 ) { + /* Set the first NLSF to zero and move on to the next */ + NLSF[ 0 ] = 0; + p = Q; /* Pointer to polynomial */ + ylo = silk_A2NLSF_eval_poly( p, xlo, dd ); + root_ix = 1; /* Index of current root */ + } else { + root_ix = 0; /* Index of current root */ + } + k = 1; /* Loop counter */ + i = 0; /* Counter for bandwidth expansions applied */ + thr = 0; + while( 1 ) { + /* Evaluate polynomial */ + xhi = silk_LSFCosTab_FIX_Q12[ k ]; /* Q12 */ + yhi = silk_A2NLSF_eval_poly( p, xhi, dd ); + + /* Detect zero crossing */ + if( ( ylo <= 0 && yhi >= thr ) || ( ylo >= 0 && yhi <= -thr ) ) { + if( yhi == 0 ) { + /* If the root lies exactly at the end of the current */ + /* interval, look for the next root in the next interval */ + thr = 1; + } else { + thr = 0; + } + /* Binary division */ + ffrac = -256; + for( m = 0; m < BIN_DIV_STEPS_A2NLSF_FIX; m++ ) { + /* Evaluate polynomial */ + xmid = silk_RSHIFT_ROUND( xlo + xhi, 1 ); + ymid = silk_A2NLSF_eval_poly( p, xmid, dd ); + + /* Detect zero crossing */ + if( ( ylo <= 0 && ymid >= 0 ) || ( ylo >= 0 && ymid <= 0 ) ) { + /* Reduce frequency */ + xhi = xmid; + yhi = ymid; + } else { + /* Increase frequency */ + xlo = xmid; + ylo = ymid; + ffrac = silk_ADD_RSHIFT( ffrac, 128, m ); + } + } + + /* Interpolate */ + if( silk_abs( ylo ) < 65536 ) { + /* Avoid dividing by zero */ + den = ylo - yhi; + nom = silk_LSHIFT( ylo, 8 - BIN_DIV_STEPS_A2NLSF_FIX ) + silk_RSHIFT( den, 1 ); + if( den != 0 ) { + ffrac += silk_DIV32( nom, den ); + } + } else { + /* No risk of dividing by zero because abs(ylo - yhi) >= abs(ylo) >= 65536 */ + ffrac += silk_DIV32( ylo, silk_RSHIFT( ylo - yhi, 8 - BIN_DIV_STEPS_A2NLSF_FIX ) ); + } + NLSF[ root_ix ] = (opus_int16)silk_min_32( silk_LSHIFT( (opus_int32)k, 8 ) + ffrac, silk_int16_MAX ); + + silk_assert( NLSF[ root_ix ] >= 0 ); + + root_ix++; /* Next root */ + if( root_ix >= d ) { + /* Found all roots */ + break; + } + /* Alternate pointer to polynomial */ + p = PQ[ root_ix & 1 ]; + + /* Evaluate polynomial */ + xlo = silk_LSFCosTab_FIX_Q12[ k - 1 ]; /* Q12*/ + ylo = silk_LSHIFT( 1 - ( root_ix & 2 ), 12 ); + } else { + /* Increment loop counter */ + k++; + xlo = xhi; + ylo = yhi; + thr = 0; + + if( k > LSF_COS_TAB_SZ_FIX ) { + i++; + if( i > MAX_ITERATIONS_A2NLSF_FIX ) { + /* Set NLSFs to white spectrum and exit */ + NLSF[ 0 ] = (opus_int16)silk_DIV32_16( 1 << 15, d + 1 ); + for( k = 1; k < d; k++ ) { + NLSF[ k ] = (opus_int16)silk_ADD16( NLSF[ k-1 ], NLSF[ 0 ] ); + } + return; + } + + /* Error: Apply progressively more bandwidth expansion and run again */ + silk_bwexpander_32( a_Q16, d, 65536 - silk_LSHIFT( 1, i ) ); + + silk_A2NLSF_init( a_Q16, P, Q, dd ); + p = P; /* Pointer to polynomial */ + xlo = silk_LSFCosTab_FIX_Q12[ 0 ]; /* Q12*/ + ylo = silk_A2NLSF_eval_poly( p, xlo, dd ); + if( ylo < 0 ) { + /* Set the first NLSF to zero and move on to the next */ + NLSF[ 0 ] = 0; + p = Q; /* Pointer to polynomial */ + ylo = silk_A2NLSF_eval_poly( p, xlo, dd ); + root_ix = 1; /* Index of current root */ + } else { + root_ix = 0; /* Index of current root */ + } + k = 1; /* Reset loop counter */ + } + } + } +} diff --git a/port/third_party/opus/silk/API.h b/port/third_party/opus/silk/API.h new file mode 100644 index 00000000..878965c7 --- /dev/null +++ b/port/third_party/opus/silk/API.h @@ -0,0 +1,156 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_API_H +#define SILK_API_H + +#include "control.h" +#include "typedef.h" +#include "errors.h" +#include "entenc.h" +#include "entdec.h" + +#ifdef ENABLE_DEEP_PLC +#include "lpcnet_private.h" +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define SILK_MAX_FRAMES_PER_PACKET 3 + +/* Struct for TOC (Table of Contents) */ +typedef struct { + opus_int VADFlag; /* Voice activity for packet */ + opus_int VADFlags[ SILK_MAX_FRAMES_PER_PACKET ]; /* Voice activity for each frame in packet */ + opus_int inbandFECFlag; /* Flag indicating if packet contains in-band FEC */ +} silk_TOC_struct; + +/****************************************/ +/* Encoder functions */ +/****************************************/ + +/***********************************************/ +/* Get size in bytes of the Silk encoder state */ +/***********************************************/ +opus_int silk_Get_Encoder_Size( /* O Returns error code */ + opus_int *encSizeBytes /* O Number of bytes in SILK encoder state */ +); + +/*************************/ +/* Init or reset encoder */ +/*************************/ +opus_int silk_InitEncoder( /* O Returns error code */ + void *encState, /* I/O State */ + int arch, /* I Run-time architecture */ + silk_EncControlStruct *encStatus /* O Encoder Status */ +); + +/**************************/ +/* Encode frame with Silk */ +/**************************/ +/* Note: if prefillFlag is set, the input must contain 10 ms of audio, irrespective of what */ +/* encControl->payloadSize_ms is set to */ +opus_int silk_Encode( /* O Returns error code */ + void *encState, /* I/O State */ + silk_EncControlStruct *encControl, /* I Control status */ + const opus_int16 *samplesIn, /* I Speech sample input vector */ + opus_int nSamplesIn, /* I Number of samples in input vector */ + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + opus_int32 *nBytesOut, /* I/O Number of bytes in payload (input: Max bytes) */ + const opus_int prefillFlag, /* I Flag to indicate prefilling buffers no coding */ + int activity /* I Decision of Opus voice activity detector */ +); + +/****************************************/ +/* Decoder functions */ +/****************************************/ + + +/***********************************************/ +/* Load OSCE models from external data pointer */ +/***********************************************/ +opus_int silk_LoadOSCEModels( + void *decState, /* O I/O State */ + const unsigned char *data, /* I pointer to binary blob */ + int len /* I length of binary blob data */ +); + +/***********************************************/ +/* Get size in bytes of the Silk decoder state */ +/***********************************************/ +opus_int silk_Get_Decoder_Size( /* O Returns error code */ + opus_int *decSizeBytes /* O Number of bytes in SILK decoder state */ +); + +/*************************/ +/* Init and Reset decoder */ +/*************************/ +opus_int silk_ResetDecoder( /* O Returns error code */ + void *decState /* I/O State */ +); + +opus_int silk_InitDecoder( /* O Returns error code */ + void *decState /* I/O State */ +); + +/******************/ +/* Decode a frame */ +/******************/ +opus_int silk_Decode( /* O Returns error code */ + void* decState, /* I/O State */ + silk_DecControlStruct* decControl, /* I/O Control Structure */ + opus_int lostFlag, /* I 0: no loss, 1 loss, 2 decode fec */ + opus_int newPacketFlag, /* I Indicates first decoder call for this packet */ + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int16 *samplesOut, /* O Decoded output speech vector */ + opus_int32 *nSamplesOut, /* O Number of samples decoded */ +#ifdef ENABLE_DEEP_PLC + LPCNetPLCState *lpcnet, +#endif + int arch /* I Run-time architecture */ +); + +#if 0 +/**************************************/ +/* Get table of contents for a packet */ +/**************************************/ +opus_int silk_get_TOC( + const opus_uint8 *payload, /* I Payload data */ + const opus_int nBytesIn, /* I Number of input bytes */ + const opus_int nFramesPerPayload, /* I Number of SILK frames per payload */ + silk_TOC_struct *Silk_TOC /* O Type of content */ +); +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/port/third_party/opus/silk/CNG.c b/port/third_party/opus/silk/CNG.c new file mode 100644 index 00000000..2a910099 --- /dev/null +++ b/port/third_party/opus/silk/CNG.c @@ -0,0 +1,188 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" + +/* Generates excitation for CNG LPC synthesis */ +static OPUS_INLINE void silk_CNG_exc( + opus_int32 exc_Q14[], /* O CNG excitation signal Q10 */ + opus_int32 exc_buf_Q14[], /* I Random samples buffer Q10 */ + opus_int length, /* I Length */ + opus_int32 *rand_seed /* I/O Seed to random index generator */ +) +{ + opus_int32 seed; + opus_int i, idx, exc_mask; + + exc_mask = CNG_BUF_MASK_MAX; + while( exc_mask > length ) { + exc_mask = silk_RSHIFT( exc_mask, 1 ); + } + + seed = *rand_seed; + for( i = 0; i < length; i++ ) { + seed = silk_RAND( seed ); + idx = (opus_int)( silk_RSHIFT( seed, 24 ) & exc_mask ); + silk_assert( idx >= 0 ); + silk_assert( idx <= CNG_BUF_MASK_MAX ); + exc_Q14[ i ] = exc_buf_Q14[ idx ]; + } + *rand_seed = seed; +} + +void silk_CNG_Reset( + silk_decoder_state *psDec /* I/O Decoder state */ +) +{ + opus_int i, NLSF_step_Q15, NLSF_acc_Q15; + + NLSF_step_Q15 = silk_DIV32_16( silk_int16_MAX, psDec->LPC_order + 1 ); + NLSF_acc_Q15 = 0; + for( i = 0; i < psDec->LPC_order; i++ ) { + NLSF_acc_Q15 += NLSF_step_Q15; + psDec->sCNG.CNG_smth_NLSF_Q15[ i ] = NLSF_acc_Q15; + } + psDec->sCNG.CNG_smth_Gain_Q16 = 0; + psDec->sCNG.rand_seed = 3176576; +} + +/* Updates CNG estimate, and applies the CNG when packet was lost */ +void silk_CNG( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl, /* I/O Decoder control */ + opus_int16 frame[], /* I/O Signal */ + opus_int length /* I Length of residual */ +) +{ + opus_int i, subfr; + opus_int32 LPC_pred_Q10, max_Gain_Q16, gain_Q16, gain_Q10; + opus_int16 A_Q12[ MAX_LPC_ORDER ]; + silk_CNG_struct *psCNG = &psDec->sCNG; + SAVE_STACK; + + if( psDec->fs_kHz != psCNG->fs_kHz ) { + /* Reset state */ + silk_CNG_Reset( psDec ); + + psCNG->fs_kHz = psDec->fs_kHz; + } + if( psDec->lossCnt == 0 && psDec->prevSignalType == TYPE_NO_VOICE_ACTIVITY ) { + /* Update CNG parameters */ + + /* Smoothing of LSF's */ + for( i = 0; i < psDec->LPC_order; i++ ) { + psCNG->CNG_smth_NLSF_Q15[ i ] += silk_SMULWB( (opus_int32)psDec->prevNLSF_Q15[ i ] - (opus_int32)psCNG->CNG_smth_NLSF_Q15[ i ], CNG_NLSF_SMTH_Q16 ); + } + /* Find the subframe with the highest gain */ + max_Gain_Q16 = 0; + subfr = 0; + for( i = 0; i < psDec->nb_subfr; i++ ) { + if( psDecCtrl->Gains_Q16[ i ] > max_Gain_Q16 ) { + max_Gain_Q16 = psDecCtrl->Gains_Q16[ i ]; + subfr = i; + } + } + /* Update CNG excitation buffer with excitation from this subframe */ + silk_memmove( &psCNG->CNG_exc_buf_Q14[ psDec->subfr_length ], psCNG->CNG_exc_buf_Q14, ( psDec->nb_subfr - 1 ) * psDec->subfr_length * sizeof( opus_int32 ) ); + silk_memcpy( psCNG->CNG_exc_buf_Q14, &psDec->exc_Q14[ subfr * psDec->subfr_length ], psDec->subfr_length * sizeof( opus_int32 ) ); + + /* Smooth gains */ + for( i = 0; i < psDec->nb_subfr; i++ ) { + psCNG->CNG_smth_Gain_Q16 += silk_SMULWB( psDecCtrl->Gains_Q16[ i ] - psCNG->CNG_smth_Gain_Q16, CNG_GAIN_SMTH_Q16 ); + /* If the smoothed gain is 3 dB greater than this subframe's gain, use this subframe's gain to adapt faster. */ + if( silk_SMULWW( psCNG->CNG_smth_Gain_Q16, CNG_GAIN_SMTH_THRESHOLD_Q16 ) > psDecCtrl->Gains_Q16[ i ] ) { + psCNG->CNG_smth_Gain_Q16 = psDecCtrl->Gains_Q16[ i ]; + } + } + } + + /* Add CNG when packet is lost or during DTX */ + if( psDec->lossCnt ) { + VARDECL( opus_int32, CNG_sig_Q14 ); + ALLOC( CNG_sig_Q14, length + MAX_LPC_ORDER, opus_int32 ); + + /* Generate CNG excitation */ + gain_Q16 = silk_SMULWW( psDec->sPLC.randScale_Q14, psDec->sPLC.prevGain_Q16[1] ); + if( gain_Q16 >= (1 << 21) || psCNG->CNG_smth_Gain_Q16 > (1 << 23) ) { + gain_Q16 = silk_SMULTT( gain_Q16, gain_Q16 ); + gain_Q16 = silk_SUB_LSHIFT32(silk_SMULTT( psCNG->CNG_smth_Gain_Q16, psCNG->CNG_smth_Gain_Q16 ), gain_Q16, 5 ); + gain_Q16 = silk_LSHIFT32( silk_SQRT_APPROX( gain_Q16 ), 16 ); + } else { + gain_Q16 = silk_SMULWW( gain_Q16, gain_Q16 ); + gain_Q16 = silk_SUB_LSHIFT32(silk_SMULWW( psCNG->CNG_smth_Gain_Q16, psCNG->CNG_smth_Gain_Q16 ), gain_Q16, 5 ); + gain_Q16 = silk_LSHIFT32( silk_SQRT_APPROX( gain_Q16 ), 8 ); + } + gain_Q10 = silk_RSHIFT( gain_Q16, 6 ); + + silk_CNG_exc( CNG_sig_Q14 + MAX_LPC_ORDER, psCNG->CNG_exc_buf_Q14, length, &psCNG->rand_seed ); + + /* Convert CNG NLSF to filter representation */ + silk_NLSF2A( A_Q12, psCNG->CNG_smth_NLSF_Q15, psDec->LPC_order, psDec->arch ); + + /* Generate CNG signal, by synthesis filtering */ + silk_memcpy( CNG_sig_Q14, psCNG->CNG_synth_state, MAX_LPC_ORDER * sizeof( opus_int32 ) ); + celt_assert( psDec->LPC_order == 10 || psDec->LPC_order == 16 ); + for( i = 0; i < length; i++ ) { + /* Avoids introducing a bias because silk_SMLAWB() always rounds to -inf */ + LPC_pred_Q10 = silk_RSHIFT( psDec->LPC_order, 1 ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 1 ], A_Q12[ 0 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 2 ], A_Q12[ 1 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 3 ], A_Q12[ 2 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 4 ], A_Q12[ 3 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 5 ], A_Q12[ 4 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 6 ], A_Q12[ 5 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 7 ], A_Q12[ 6 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 8 ], A_Q12[ 7 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 9 ], A_Q12[ 8 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 10 ], A_Q12[ 9 ] ); + if( psDec->LPC_order == 16 ) { + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 11 ], A_Q12[ 10 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 12 ], A_Q12[ 11 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 13 ], A_Q12[ 12 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 14 ], A_Q12[ 13 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 15 ], A_Q12[ 14 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, CNG_sig_Q14[ MAX_LPC_ORDER + i - 16 ], A_Q12[ 15 ] ); + } + + /* Update states */ + CNG_sig_Q14[ MAX_LPC_ORDER + i ] = silk_ADD_SAT32( CNG_sig_Q14[ MAX_LPC_ORDER + i ], silk_LSHIFT_SAT32( LPC_pred_Q10, 4 ) ); + + /* Scale with Gain and add to input signal */ + frame[ i ] = (opus_int16)silk_ADD_SAT16( frame[ i ], silk_SAT16( silk_RSHIFT_ROUND( silk_SMULWW( CNG_sig_Q14[ MAX_LPC_ORDER + i ], gain_Q10 ), 8 ) ) ); + + } + silk_memcpy( psCNG->CNG_synth_state, &CNG_sig_Q14[ length ], MAX_LPC_ORDER * sizeof( opus_int32 ) ); + } else { + silk_memset( psCNG->CNG_synth_state, 0, psDec->LPC_order * sizeof( opus_int32 ) ); + } + RESTORE_STACK; +} diff --git a/port/third_party/opus/silk/HP_variable_cutoff.c b/port/third_party/opus/silk/HP_variable_cutoff.c new file mode 100644 index 00000000..bbe10f04 --- /dev/null +++ b/port/third_party/opus/silk/HP_variable_cutoff.c @@ -0,0 +1,77 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#ifdef FIXED_POINT +#include "main_FIX.h" +#else +#include "main_FLP.h" +#endif +#include "tuning_parameters.h" + +/* High-pass filter with cutoff frequency adaptation based on pitch lag statistics */ +void silk_HP_variable_cutoff( + silk_encoder_state_Fxx state_Fxx[] /* I/O Encoder states */ +) +{ + opus_int quality_Q15; + opus_int32 pitch_freq_Hz_Q16, pitch_freq_log_Q7, delta_freq_Q7; + silk_encoder_state *psEncC1 = &state_Fxx[ 0 ].sCmn; + + /* Adaptive cutoff frequency: estimate low end of pitch frequency range */ + if( psEncC1->prevSignalType == TYPE_VOICED ) { + /* difference, in log domain */ + pitch_freq_Hz_Q16 = silk_DIV32_16( silk_LSHIFT( silk_MUL( psEncC1->fs_kHz, 1000 ), 16 ), psEncC1->prevLag ); + pitch_freq_log_Q7 = silk_lin2log( pitch_freq_Hz_Q16 ) - ( 16 << 7 ); + + /* adjustment based on quality */ + quality_Q15 = psEncC1->input_quality_bands_Q15[ 0 ]; + pitch_freq_log_Q7 = silk_SMLAWB( pitch_freq_log_Q7, silk_SMULWB( silk_LSHIFT( -quality_Q15, 2 ), quality_Q15 ), + pitch_freq_log_Q7 - ( silk_lin2log( SILK_FIX_CONST( VARIABLE_HP_MIN_CUTOFF_HZ, 16 ) ) - ( 16 << 7 ) ) ); + + /* delta_freq = pitch_freq_log - psEnc->variable_HP_smth1; */ + delta_freq_Q7 = pitch_freq_log_Q7 - silk_RSHIFT( psEncC1->variable_HP_smth1_Q15, 8 ); + if( delta_freq_Q7 < 0 ) { + /* less smoothing for decreasing pitch frequency, to track something close to the minimum */ + delta_freq_Q7 = silk_MUL( delta_freq_Q7, 3 ); + } + + /* limit delta, to reduce impact of outliers in pitch estimation */ + delta_freq_Q7 = silk_LIMIT_32( delta_freq_Q7, -SILK_FIX_CONST( VARIABLE_HP_MAX_DELTA_FREQ, 7 ), SILK_FIX_CONST( VARIABLE_HP_MAX_DELTA_FREQ, 7 ) ); + + /* update smoother */ + psEncC1->variable_HP_smth1_Q15 = silk_SMLAWB( psEncC1->variable_HP_smth1_Q15, + silk_SMULBB( psEncC1->speech_activity_Q8, delta_freq_Q7 ), SILK_FIX_CONST( VARIABLE_HP_SMTH_COEF1, 16 ) ); + + /* limit frequency range */ + psEncC1->variable_HP_smth1_Q15 = silk_LIMIT_32( psEncC1->variable_HP_smth1_Q15, + silk_LSHIFT( silk_lin2log( VARIABLE_HP_MIN_CUTOFF_HZ ), 8 ), + silk_LSHIFT( silk_lin2log( VARIABLE_HP_MAX_CUTOFF_HZ ), 8 ) ); + } +} diff --git a/port/third_party/opus/silk/Inlines.h b/port/third_party/opus/silk/Inlines.h new file mode 100644 index 00000000..ec986cdf --- /dev/null +++ b/port/third_party/opus/silk/Inlines.h @@ -0,0 +1,188 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +/*! \file silk_Inlines.h + * \brief silk_Inlines.h defines OPUS_INLINE signal processing functions. + */ + +#ifndef SILK_FIX_INLINES_H +#define SILK_FIX_INLINES_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* count leading zeros of opus_int64 */ +static OPUS_INLINE opus_int32 silk_CLZ64( opus_int64 in ) +{ + opus_int32 in_upper; + + in_upper = (opus_int32)silk_RSHIFT64(in, 32); + if (in_upper == 0) { + /* Search in the lower 32 bits */ + return 32 + silk_CLZ32( (opus_int32) in ); + } else { + /* Search in the upper 32 bits */ + return silk_CLZ32( in_upper ); + } +} + +/* get number of leading zeros and fractional part (the bits right after the leading one */ +static OPUS_INLINE void silk_CLZ_FRAC( + opus_int32 in, /* I input */ + opus_int32 *lz, /* O number of leading zeros */ + opus_int32 *frac_Q7 /* O the 7 bits right after the leading one */ +) +{ + opus_int32 lzeros = silk_CLZ32(in); + + * lz = lzeros; + * frac_Q7 = silk_ROR32(in, 24 - lzeros) & 0x7f; +} + +/* Approximation of square root */ +/* Accuracy: < +/- 10% for output values > 15 */ +/* < +/- 2.5% for output values > 120 */ +static OPUS_INLINE opus_int32 silk_SQRT_APPROX( opus_int32 x ) +{ + opus_int32 y, lz, frac_Q7; + + if( x <= 0 ) { + return 0; + } + + silk_CLZ_FRAC(x, &lz, &frac_Q7); + + if( lz & 1 ) { + y = 32768; + } else { + y = 46214; /* 46214 = sqrt(2) * 32768 */ + } + + /* get scaling right */ + y >>= silk_RSHIFT(lz, 1); + + /* increment using fractional part of input */ + y = silk_SMLAWB(y, y, silk_SMULBB(213, frac_Q7)); + + return y; +} + +/* Divide two int32 values and return result as int32 in a given Q-domain */ +static OPUS_INLINE opus_int32 silk_DIV32_varQ( /* O returns a good approximation of "(a32 << Qres) / b32" */ + const opus_int32 a32, /* I numerator (Q0) */ + const opus_int32 b32, /* I denominator (Q0) */ + const opus_int Qres /* I Q-domain of result (>= 0) */ +) +{ + opus_int a_headrm, b_headrm, lshift; + opus_int32 b32_inv, a32_nrm, b32_nrm, result; + + silk_assert( b32 != 0 ); + silk_assert( Qres >= 0 ); + + /* Compute number of bits head room and normalize inputs */ + a_headrm = silk_CLZ32( silk_abs(a32) ) - 1; + a32_nrm = silk_LSHIFT(a32, a_headrm); /* Q: a_headrm */ + b_headrm = silk_CLZ32( silk_abs(b32) ) - 1; + b32_nrm = silk_LSHIFT(b32, b_headrm); /* Q: b_headrm */ + + /* Inverse of b32, with 14 bits of precision */ + b32_inv = silk_DIV32_16( silk_int32_MAX >> 2, silk_RSHIFT(b32_nrm, 16) ); /* Q: 29 + 16 - b_headrm */ + + /* First approximation */ + result = silk_SMULWB(a32_nrm, b32_inv); /* Q: 29 + a_headrm - b_headrm */ + + /* Compute residual by subtracting product of denominator and first approximation */ + /* It's OK to overflow because the final value of a32_nrm should always be small */ + a32_nrm = silk_SUB32_ovflw(a32_nrm, silk_LSHIFT_ovflw( silk_SMMUL(b32_nrm, result), 3 )); /* Q: a_headrm */ + + /* Refinement */ + result = silk_SMLAWB(result, a32_nrm, b32_inv); /* Q: 29 + a_headrm - b_headrm */ + + /* Convert to Qres domain */ + lshift = 29 + a_headrm - b_headrm - Qres; + if( lshift < 0 ) { + return silk_LSHIFT_SAT32(result, -lshift); + } else { + if( lshift < 32){ + return silk_RSHIFT(result, lshift); + } else { + /* Avoid undefined result */ + return 0; + } + } +} + +/* Invert int32 value and return result as int32 in a given Q-domain */ +static OPUS_INLINE opus_int32 silk_INVERSE32_varQ( /* O returns a good approximation of "(1 << Qres) / b32" */ + const opus_int32 b32, /* I denominator (Q0) */ + const opus_int Qres /* I Q-domain of result (> 0) */ +) +{ + opus_int b_headrm, lshift; + opus_int32 b32_inv, b32_nrm, err_Q32, result; + + silk_assert( b32 != 0 ); + silk_assert( Qres > 0 ); + + /* Compute number of bits head room and normalize input */ + b_headrm = silk_CLZ32( silk_abs(b32) ) - 1; + b32_nrm = silk_LSHIFT(b32, b_headrm); /* Q: b_headrm */ + + /* Inverse of b32, with 14 bits of precision */ + b32_inv = silk_DIV32_16( silk_int32_MAX >> 2, silk_RSHIFT(b32_nrm, 16) ); /* Q: 29 + 16 - b_headrm */ + + /* First approximation */ + result = silk_LSHIFT(b32_inv, 16); /* Q: 61 - b_headrm */ + + /* Compute residual by subtracting product of denominator and first approximation from one */ + err_Q32 = silk_LSHIFT( ((opus_int32)1<<29) - silk_SMULWB(b32_nrm, b32_inv), 3 ); /* Q32 */ + + /* Refinement */ + result = silk_SMLAWW(result, err_Q32, b32_inv); /* Q: 61 - b_headrm */ + + /* Convert to Qres domain */ + lshift = 61 - b_headrm - Qres; + if( lshift <= 0 ) { + return silk_LSHIFT_SAT32(result, -lshift); + } else { + if( lshift < 32){ + return silk_RSHIFT(result, lshift); + }else{ + /* Avoid undefined result */ + return 0; + } + } +} + +#ifdef __cplusplus +} +#endif + +#endif /* SILK_FIX_INLINES_H */ diff --git a/port/third_party/opus/silk/LPC_analysis_filter.c b/port/third_party/opus/silk/LPC_analysis_filter.c new file mode 100644 index 00000000..d34b5eb7 --- /dev/null +++ b/port/third_party/opus/silk/LPC_analysis_filter.c @@ -0,0 +1,111 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "celt_lpc.h" + +/*******************************************/ +/* LPC analysis filter */ +/* NB! State is kept internally and the */ +/* filter always starts with zero state */ +/* first d output samples are set to zero */ +/*******************************************/ + +/* OPT: Using celt_fir() for this function should be faster, but it may cause + integer overflows in intermediate values (not final results), which the + current implementation silences by casting to unsigned. Enabling + this should be safe in pretty much all cases, even though it is not technically + C89-compliant. */ +#define USE_CELT_FIR 0 + +void silk_LPC_analysis_filter( + opus_int16 *out, /* O Output signal */ + const opus_int16 *in, /* I Input signal */ + const opus_int16 *B, /* I MA prediction coefficients, Q12 [order] */ + const opus_int32 len, /* I Signal length */ + const opus_int32 d, /* I Filter order */ + int arch /* I Run-time architecture */ +) +{ + opus_int j; +#if defined(FIXED_POINT) && USE_CELT_FIR + opus_int16 num[SILK_MAX_ORDER_LPC]; +#else + int ix; + opus_int32 out32_Q12, out32; + const opus_int16 *in_ptr; +#endif + + celt_assert( d >= 6 ); + celt_assert( (d & 1) == 0 ); + celt_assert( d <= len ); + +#if defined(FIXED_POINT) && USE_CELT_FIR + celt_assert( d <= SILK_MAX_ORDER_LPC ); + for ( j = 0; j < d; j++ ) { + num[ j ] = -B[ j ]; + } + celt_fir( in + d, num, out + d, len - d, d, arch ); + for ( j = 0; j < d; j++ ) { + out[ j ] = 0; + } +#else + (void)arch; + for( ix = d; ix < len; ix++ ) { + in_ptr = &in[ ix - 1 ]; + + out32_Q12 = silk_SMULBB( in_ptr[ 0 ], B[ 0 ] ); + /* Allowing wrap around so that two wraps can cancel each other. The rare + cases where the result wraps around can only be triggered by invalid streams*/ + out32_Q12 = silk_SMLABB_ovflw( out32_Q12, in_ptr[ -1 ], B[ 1 ] ); + out32_Q12 = silk_SMLABB_ovflw( out32_Q12, in_ptr[ -2 ], B[ 2 ] ); + out32_Q12 = silk_SMLABB_ovflw( out32_Q12, in_ptr[ -3 ], B[ 3 ] ); + out32_Q12 = silk_SMLABB_ovflw( out32_Q12, in_ptr[ -4 ], B[ 4 ] ); + out32_Q12 = silk_SMLABB_ovflw( out32_Q12, in_ptr[ -5 ], B[ 5 ] ); + for( j = 6; j < d; j += 2 ) { + out32_Q12 = silk_SMLABB_ovflw( out32_Q12, in_ptr[ -j ], B[ j ] ); + out32_Q12 = silk_SMLABB_ovflw( out32_Q12, in_ptr[ -j - 1 ], B[ j + 1 ] ); + } + + /* Subtract prediction */ + out32_Q12 = silk_SUB32_ovflw( silk_LSHIFT( (opus_int32)in_ptr[ 1 ], 12 ), out32_Q12 ); + + /* Scale to Q0 */ + out32 = silk_RSHIFT_ROUND( out32_Q12, 12 ); + + /* Saturate output */ + out[ ix ] = (opus_int16)silk_SAT16( out32 ); + } + + /* Set first d output samples to zero */ + silk_memset( out, 0, d * sizeof( opus_int16 ) ); +#endif +} diff --git a/port/third_party/opus/silk/LPC_fit.c b/port/third_party/opus/silk/LPC_fit.c new file mode 100644 index 00000000..c0690a1f --- /dev/null +++ b/port/third_party/opus/silk/LPC_fit.c @@ -0,0 +1,82 @@ +/*********************************************************************** +Copyright (c) 2013, Koen Vos. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" + +/* Convert int32 coefficients to int16 coefs and make sure there's no wrap-around. + This logic is reused in _celt_lpc(). Any bug fixes should also be applied there. */ +void silk_LPC_fit( + opus_int16 *a_QOUT, /* O Output signal */ + opus_int32 *a_QIN, /* I/O Input signal */ + const opus_int QOUT, /* I Input Q domain */ + const opus_int QIN, /* I Input Q domain */ + const opus_int d /* I Filter order */ +) +{ + opus_int i, k, idx = 0; + opus_int32 maxabs, absval, chirp_Q16; + + /* Limit the maximum absolute value of the prediction coefficients, so that they'll fit in int16 */ + for( i = 0; i < 10; i++ ) { + /* Find maximum absolute value and its index */ + maxabs = 0; + for( k = 0; k < d; k++ ) { + absval = silk_abs( a_QIN[k] ); + if( absval > maxabs ) { + maxabs = absval; + idx = k; + } + } + maxabs = silk_RSHIFT_ROUND( maxabs, QIN - QOUT ); + + if( maxabs > silk_int16_MAX ) { + /* Reduce magnitude of prediction coefficients */ + maxabs = silk_min( maxabs, 163838 ); /* ( silk_int32_MAX >> 14 ) + silk_int16_MAX = 163838 */ + chirp_Q16 = SILK_FIX_CONST( 0.999, 16 ) - silk_DIV32( silk_LSHIFT( maxabs - silk_int16_MAX, 14 ), + silk_RSHIFT32( silk_MUL( maxabs, idx + 1), 2 ) ); + silk_bwexpander_32( a_QIN, d, chirp_Q16 ); + } else { + break; + } + } + + if( i == 10 ) { + /* Reached the last iteration, clip the coefficients */ + for( k = 0; k < d; k++ ) { + a_QOUT[ k ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( a_QIN[ k ], QIN - QOUT ) ); + a_QIN[ k ] = silk_LSHIFT( (opus_int32)a_QOUT[ k ], QIN - QOUT ); + } + } else { + for( k = 0; k < d; k++ ) { + a_QOUT[ k ] = (opus_int16)silk_RSHIFT_ROUND( a_QIN[ k ], QIN - QOUT ); + } + } +} diff --git a/port/third_party/opus/silk/LPC_inv_pred_gain.c b/port/third_party/opus/silk/LPC_inv_pred_gain.c new file mode 100644 index 00000000..a3746a6e --- /dev/null +++ b/port/third_party/opus/silk/LPC_inv_pred_gain.c @@ -0,0 +1,141 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "define.h" + +#define QA 24 +#define A_LIMIT SILK_FIX_CONST( 0.99975, QA ) + +#define MUL32_FRAC_Q(a32, b32, Q) ((opus_int32)(silk_RSHIFT_ROUND64(silk_SMULL(a32, b32), Q))) + +/* Compute inverse of LPC prediction gain, and */ +/* test if LPC coefficients are stable (all poles within unit circle) */ +static opus_int32 LPC_inverse_pred_gain_QA_c( /* O Returns inverse prediction gain in energy domain, Q30 */ + opus_int32 A_QA[ SILK_MAX_ORDER_LPC ], /* I Prediction coefficients */ + const opus_int order /* I Prediction order */ +) +{ + opus_int k, n, mult2Q; + opus_int32 invGain_Q30, rc_Q31, rc_mult1_Q30, rc_mult2, tmp1, tmp2; + + invGain_Q30 = SILK_FIX_CONST( 1, 30 ); + for( k = order - 1; k > 0; k-- ) { + /* Check for stability */ + if( ( A_QA[ k ] > A_LIMIT ) || ( A_QA[ k ] < -A_LIMIT ) ) { + return 0; + } + + /* Set RC equal to negated AR coef */ + rc_Q31 = -silk_LSHIFT( A_QA[ k ], 31 - QA ); + + /* rc_mult1_Q30 range: [ 1 : 2^30 ] */ + rc_mult1_Q30 = silk_SUB32( SILK_FIX_CONST( 1, 30 ), silk_SMMUL( rc_Q31, rc_Q31 ) ); + silk_assert( rc_mult1_Q30 > ( 1 << 15 ) ); /* reduce A_LIMIT if fails */ + silk_assert( rc_mult1_Q30 <= ( 1 << 30 ) ); + + /* Update inverse gain */ + /* invGain_Q30 range: [ 0 : 2^30 ] */ + invGain_Q30 = silk_LSHIFT( silk_SMMUL( invGain_Q30, rc_mult1_Q30 ), 2 ); + silk_assert( invGain_Q30 >= 0 ); + silk_assert( invGain_Q30 <= ( 1 << 30 ) ); + if( invGain_Q30 < SILK_FIX_CONST( 1.0f / MAX_PREDICTION_POWER_GAIN, 30 ) ) { + return 0; + } + + /* rc_mult2 range: [ 2^30 : silk_int32_MAX ] */ + mult2Q = 32 - silk_CLZ32( silk_abs( rc_mult1_Q30 ) ); + rc_mult2 = silk_INVERSE32_varQ( rc_mult1_Q30, mult2Q + 30 ); + + /* Update AR coefficient */ + for( n = 0; n < (k + 1) >> 1; n++ ) { + opus_int64 tmp64; + tmp1 = A_QA[ n ]; + tmp2 = A_QA[ k - n - 1 ]; + tmp64 = silk_RSHIFT_ROUND64( silk_SMULL( silk_SUB_SAT32(tmp1, + MUL32_FRAC_Q( tmp2, rc_Q31, 31 ) ), rc_mult2 ), mult2Q); + if( tmp64 > silk_int32_MAX || tmp64 < silk_int32_MIN ) { + return 0; + } + A_QA[ n ] = ( opus_int32 )tmp64; + tmp64 = silk_RSHIFT_ROUND64( silk_SMULL( silk_SUB_SAT32(tmp2, + MUL32_FRAC_Q( tmp1, rc_Q31, 31 ) ), rc_mult2), mult2Q); + if( tmp64 > silk_int32_MAX || tmp64 < silk_int32_MIN ) { + return 0; + } + A_QA[ k - n - 1 ] = ( opus_int32 )tmp64; + } + } + + /* Check for stability */ + if( ( A_QA[ k ] > A_LIMIT ) || ( A_QA[ k ] < -A_LIMIT ) ) { + return 0; + } + + /* Set RC equal to negated AR coef */ + rc_Q31 = -silk_LSHIFT( A_QA[ 0 ], 31 - QA ); + + /* Range: [ 1 : 2^30 ] */ + rc_mult1_Q30 = silk_SUB32( SILK_FIX_CONST( 1, 30 ), silk_SMMUL( rc_Q31, rc_Q31 ) ); + + /* Update inverse gain */ + /* Range: [ 0 : 2^30 ] */ + invGain_Q30 = silk_LSHIFT( silk_SMMUL( invGain_Q30, rc_mult1_Q30 ), 2 ); + silk_assert( invGain_Q30 >= 0 ); + silk_assert( invGain_Q30 <= ( 1 << 30 ) ); + if( invGain_Q30 < SILK_FIX_CONST( 1.0f / MAX_PREDICTION_POWER_GAIN, 30 ) ) { + return 0; + } + + return invGain_Q30; +} + +/* For input in Q12 domain */ +opus_int32 silk_LPC_inverse_pred_gain_c( /* O Returns inverse prediction gain in energy domain, Q30 */ + const opus_int16 *A_Q12, /* I Prediction coefficients, Q12 [order] */ + const opus_int order /* I Prediction order */ +) +{ + opus_int k; + opus_int32 Atmp_QA[ SILK_MAX_ORDER_LPC ]; + opus_int32 DC_resp = 0; + + /* Increase Q domain of the AR coefficients */ + for( k = 0; k < order; k++ ) { + DC_resp += (opus_int32)A_Q12[ k ]; + Atmp_QA[ k ] = silk_LSHIFT32( (opus_int32)A_Q12[ k ], QA - 12 ); + } + /* If the DC is unstable, we don't even need to do the full calculations */ + if( DC_resp >= 4096 ) { + return 0; + } + return LPC_inverse_pred_gain_QA_c( Atmp_QA, order ); +} diff --git a/port/third_party/opus/silk/LP_variable_cutoff.c b/port/third_party/opus/silk/LP_variable_cutoff.c new file mode 100644 index 00000000..79112ad3 --- /dev/null +++ b/port/third_party/opus/silk/LP_variable_cutoff.c @@ -0,0 +1,135 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/* + Elliptic/Cauer filters designed with 0.1 dB passband ripple, + 80 dB minimum stopband attenuation, and + [0.95 : 0.15 : 0.35] normalized cut off frequencies. +*/ + +#include "main.h" + +/* Helper function, interpolates the filter taps */ +static OPUS_INLINE void silk_LP_interpolate_filter_taps( + opus_int32 B_Q28[ TRANSITION_NB ], + opus_int32 A_Q28[ TRANSITION_NA ], + const opus_int ind, + const opus_int32 fac_Q16 +) +{ + opus_int nb, na; + + if( ind < TRANSITION_INT_NUM - 1 ) { + if( fac_Q16 > 0 ) { + if( fac_Q16 < 32768 ) { /* fac_Q16 is in range of a 16-bit int */ + /* Piece-wise linear interpolation of B and A */ + for( nb = 0; nb < TRANSITION_NB; nb++ ) { + B_Q28[ nb ] = silk_SMLAWB( + silk_Transition_LP_B_Q28[ ind ][ nb ], + silk_Transition_LP_B_Q28[ ind + 1 ][ nb ] - + silk_Transition_LP_B_Q28[ ind ][ nb ], + fac_Q16 ); + } + for( na = 0; na < TRANSITION_NA; na++ ) { + A_Q28[ na ] = silk_SMLAWB( + silk_Transition_LP_A_Q28[ ind ][ na ], + silk_Transition_LP_A_Q28[ ind + 1 ][ na ] - + silk_Transition_LP_A_Q28[ ind ][ na ], + fac_Q16 ); + } + } else { /* ( fac_Q16 - ( 1 << 16 ) ) is in range of a 16-bit int */ + silk_assert( fac_Q16 - ( 1 << 16 ) == silk_SAT16( fac_Q16 - ( 1 << 16 ) ) ); + /* Piece-wise linear interpolation of B and A */ + for( nb = 0; nb < TRANSITION_NB; nb++ ) { + B_Q28[ nb ] = silk_SMLAWB( + silk_Transition_LP_B_Q28[ ind + 1 ][ nb ], + silk_Transition_LP_B_Q28[ ind + 1 ][ nb ] - + silk_Transition_LP_B_Q28[ ind ][ nb ], + fac_Q16 - ( (opus_int32)1 << 16 ) ); + } + for( na = 0; na < TRANSITION_NA; na++ ) { + A_Q28[ na ] = silk_SMLAWB( + silk_Transition_LP_A_Q28[ ind + 1 ][ na ], + silk_Transition_LP_A_Q28[ ind + 1 ][ na ] - + silk_Transition_LP_A_Q28[ ind ][ na ], + fac_Q16 - ( (opus_int32)1 << 16 ) ); + } + } + } else { + silk_memcpy( B_Q28, silk_Transition_LP_B_Q28[ ind ], TRANSITION_NB * sizeof( opus_int32 ) ); + silk_memcpy( A_Q28, silk_Transition_LP_A_Q28[ ind ], TRANSITION_NA * sizeof( opus_int32 ) ); + } + } else { + silk_memcpy( B_Q28, silk_Transition_LP_B_Q28[ TRANSITION_INT_NUM - 1 ], TRANSITION_NB * sizeof( opus_int32 ) ); + silk_memcpy( A_Q28, silk_Transition_LP_A_Q28[ TRANSITION_INT_NUM - 1 ], TRANSITION_NA * sizeof( opus_int32 ) ); + } +} + +/* Low-pass filter with variable cutoff frequency based on */ +/* piece-wise linear interpolation between elliptic filters */ +/* Start by setting psEncC->mode <> 0; */ +/* Deactivate by setting psEncC->mode = 0; */ +void silk_LP_variable_cutoff( + silk_LP_state *psLP, /* I/O LP filter state */ + opus_int16 *frame, /* I/O Low-pass filtered output signal */ + const opus_int frame_length /* I Frame length */ +) +{ + opus_int32 B_Q28[ TRANSITION_NB ], A_Q28[ TRANSITION_NA ], fac_Q16 = 0; + opus_int ind = 0; + + silk_assert( psLP->transition_frame_no >= 0 && psLP->transition_frame_no <= TRANSITION_FRAMES ); + + /* Run filter if needed */ + if( psLP->mode != 0 ) { + /* Calculate index and interpolation factor for interpolation */ +#if( TRANSITION_INT_STEPS == 64 ) + fac_Q16 = silk_LSHIFT( TRANSITION_FRAMES - psLP->transition_frame_no, 16 - 6 ); +#else + fac_Q16 = silk_DIV32_16( silk_LSHIFT( TRANSITION_FRAMES - psLP->transition_frame_no, 16 ), TRANSITION_FRAMES ); +#endif + ind = silk_RSHIFT( fac_Q16, 16 ); + fac_Q16 -= silk_LSHIFT( ind, 16 ); + + silk_assert( ind >= 0 ); + silk_assert( ind < TRANSITION_INT_NUM ); + + /* Interpolate filter coefficients */ + silk_LP_interpolate_filter_taps( B_Q28, A_Q28, ind, fac_Q16 ); + + /* Update transition frame number for next frame */ + psLP->transition_frame_no = silk_LIMIT( psLP->transition_frame_no + psLP->mode, 0, TRANSITION_FRAMES ); + + /* ARMA low-pass filtering */ + silk_assert( TRANSITION_NB == 3 && TRANSITION_NA == 2 ); + silk_biquad_alt_stride1( frame, B_Q28, A_Q28, psLP->In_LP_State, frame, frame_length); + } +} diff --git a/port/third_party/opus/silk/MacroCount.h b/port/third_party/opus/silk/MacroCount.h new file mode 100644 index 00000000..dab2f57a --- /dev/null +++ b/port/third_party/opus/silk/MacroCount.h @@ -0,0 +1,710 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SIGPROCFIX_API_MACROCOUNT_H +#define SIGPROCFIX_API_MACROCOUNT_H + +#ifdef silk_MACRO_COUNT +#include +#define varDefine opus_int64 ops_count = 0; + +extern opus_int64 ops_count; + +static OPUS_INLINE opus_int64 silk_SaveCount(){ + return(ops_count); +} + +static OPUS_INLINE opus_int64 silk_SaveResetCount(){ + opus_int64 ret; + + ret = ops_count; + ops_count = 0; + return(ret); +} + +static OPUS_INLINE silk_PrintCount(){ + printf("ops_count = %d \n ", (opus_int32)ops_count); +} + +#undef silk_MUL +static OPUS_INLINE opus_int32 silk_MUL(opus_int32 a32, opus_int32 b32){ + opus_int32 ret; + ops_count += 4; + ret = a32 * b32; + return ret; +} + +#undef silk_MUL_uint +static OPUS_INLINE opus_uint32 silk_MUL_uint(opus_uint32 a32, opus_uint32 b32){ + opus_uint32 ret; + ops_count += 4; + ret = a32 * b32; + return ret; +} +#undef silk_MLA +static OPUS_INLINE opus_int32 silk_MLA(opus_int32 a32, opus_int32 b32, opus_int32 c32){ + opus_int32 ret; + ops_count += 4; + ret = a32 + b32 * c32; + return ret; +} + +#undef silk_MLA_uint +static OPUS_INLINE opus_int32 silk_MLA_uint(opus_uint32 a32, opus_uint32 b32, opus_uint32 c32){ + opus_uint32 ret; + ops_count += 4; + ret = a32 + b32 * c32; + return ret; +} + +#undef silk_SMULWB +static OPUS_INLINE opus_int32 silk_SMULWB(opus_int32 a32, opus_int32 b32){ + opus_int32 ret; + ops_count += 5; + ret = (a32 >> 16) * (opus_int32)((opus_int16)b32) + (((a32 & 0x0000FFFF) * (opus_int32)((opus_int16)b32)) >> 16); + return ret; +} +#undef silk_SMLAWB +static OPUS_INLINE opus_int32 silk_SMLAWB(opus_int32 a32, opus_int32 b32, opus_int32 c32){ + opus_int32 ret; + ops_count += 5; + ret = ((a32) + ((((b32) >> 16) * (opus_int32)((opus_int16)(c32))) + ((((b32) & 0x0000FFFF) * (opus_int32)((opus_int16)(c32))) >> 16))); + return ret; +} + +#undef silk_SMULWT +static OPUS_INLINE opus_int32 silk_SMULWT(opus_int32 a32, opus_int32 b32){ + opus_int32 ret; + ops_count += 4; + ret = (a32 >> 16) * (b32 >> 16) + (((a32 & 0x0000FFFF) * (b32 >> 16)) >> 16); + return ret; +} +#undef silk_SMLAWT +static OPUS_INLINE opus_int32 silk_SMLAWT(opus_int32 a32, opus_int32 b32, opus_int32 c32){ + opus_int32 ret; + ops_count += 4; + ret = a32 + ((b32 >> 16) * (c32 >> 16)) + (((b32 & 0x0000FFFF) * ((c32 >> 16)) >> 16)); + return ret; +} + +#undef silk_SMULBB +static OPUS_INLINE opus_int32 silk_SMULBB(opus_int32 a32, opus_int32 b32){ + opus_int32 ret; + ops_count += 1; + ret = (opus_int32)((opus_int16)a32) * (opus_int32)((opus_int16)b32); + return ret; +} +#undef silk_SMLABB +static OPUS_INLINE opus_int32 silk_SMLABB(opus_int32 a32, opus_int32 b32, opus_int32 c32){ + opus_int32 ret; + ops_count += 1; + ret = a32 + (opus_int32)((opus_int16)b32) * (opus_int32)((opus_int16)c32); + return ret; +} + +#undef silk_SMULBT +static OPUS_INLINE opus_int32 silk_SMULBT(opus_int32 a32, opus_int32 b32 ){ + opus_int32 ret; + ops_count += 4; + ret = ((opus_int32)((opus_int16)a32)) * (b32 >> 16); + return ret; +} + +#undef silk_SMLABT +static OPUS_INLINE opus_int32 silk_SMLABT(opus_int32 a32, opus_int32 b32, opus_int32 c32){ + opus_int32 ret; + ops_count += 1; + ret = a32 + ((opus_int32)((opus_int16)b32)) * (c32 >> 16); + return ret; +} + +#undef silk_SMULTT +static OPUS_INLINE opus_int32 silk_SMULTT(opus_int32 a32, opus_int32 b32){ + opus_int32 ret; + ops_count += 1; + ret = (a32 >> 16) * (b32 >> 16); + return ret; +} + +#undef silk_SMLATT +static OPUS_INLINE opus_int32 silk_SMLATT(opus_int32 a32, opus_int32 b32, opus_int32 c32){ + opus_int32 ret; + ops_count += 1; + ret = a32 + (b32 >> 16) * (c32 >> 16); + return ret; +} + + +/* multiply-accumulate macros that allow overflow in the addition (ie, no asserts in debug mode)*/ +#undef silk_MLA_ovflw +#define silk_MLA_ovflw silk_MLA + +#undef silk_SMLABB_ovflw +#define silk_SMLABB_ovflw silk_SMLABB + +#undef silk_SMLABT_ovflw +#define silk_SMLABT_ovflw silk_SMLABT + +#undef silk_SMLATT_ovflw +#define silk_SMLATT_ovflw silk_SMLATT + +#undef silk_SMLAWB_ovflw +#define silk_SMLAWB_ovflw silk_SMLAWB + +#undef silk_SMLAWT_ovflw +#define silk_SMLAWT_ovflw silk_SMLAWT + +#undef silk_SMULL +static OPUS_INLINE opus_int64 silk_SMULL(opus_int32 a32, opus_int32 b32){ + opus_int64 ret; + ops_count += 8; + ret = ((opus_int64)(a32) * /*(opus_int64)*/(b32)); + return ret; +} + +#undef silk_SMLAL +static OPUS_INLINE opus_int64 silk_SMLAL(opus_int64 a64, opus_int32 b32, opus_int32 c32){ + opus_int64 ret; + ops_count += 8; + ret = a64 + ((opus_int64)(b32) * /*(opus_int64)*/(c32)); + return ret; +} +#undef silk_SMLALBB +static OPUS_INLINE opus_int64 silk_SMLALBB(opus_int64 a64, opus_int16 b16, opus_int16 c16){ + opus_int64 ret; + ops_count += 4; + ret = a64 + ((opus_int64)(b16) * /*(opus_int64)*/(c16)); + return ret; +} + +#undef SigProcFIX_CLZ16 +static OPUS_INLINE opus_int32 SigProcFIX_CLZ16(opus_int16 in16) +{ + opus_int32 out32 = 0; + ops_count += 10; + if( in16 == 0 ) { + return 16; + } + /* test nibbles */ + if( in16 & 0xFF00 ) { + if( in16 & 0xF000 ) { + in16 >>= 12; + } else { + out32 += 4; + in16 >>= 8; + } + } else { + if( in16 & 0xFFF0 ) { + out32 += 8; + in16 >>= 4; + } else { + out32 += 12; + } + } + /* test bits and return */ + if( in16 & 0xC ) { + if( in16 & 0x8 ) + return out32 + 0; + else + return out32 + 1; + } else { + if( in16 & 0xE ) + return out32 + 2; + else + return out32 + 3; + } +} + +#undef SigProcFIX_CLZ32 +static OPUS_INLINE opus_int32 SigProcFIX_CLZ32(opus_int32 in32) +{ + /* test highest 16 bits and convert to opus_int16 */ + ops_count += 2; + if( in32 & 0xFFFF0000 ) { + return SigProcFIX_CLZ16((opus_int16)(in32 >> 16)); + } else { + return SigProcFIX_CLZ16((opus_int16)in32) + 16; + } +} + +#undef silk_DIV32 +static OPUS_INLINE opus_int32 silk_DIV32(opus_int32 a32, opus_int32 b32){ + ops_count += 64; + return a32 / b32; +} + +#undef silk_DIV32_16 +static OPUS_INLINE opus_int32 silk_DIV32_16(opus_int32 a32, opus_int32 b32){ + ops_count += 32; + return a32 / b32; +} + +#undef silk_SAT8 +static OPUS_INLINE opus_int8 silk_SAT8(opus_int64 a){ + opus_int8 tmp; + ops_count += 1; + tmp = (opus_int8)((a) > silk_int8_MAX ? silk_int8_MAX : \ + ((a) < silk_int8_MIN ? silk_int8_MIN : (a))); + return(tmp); +} + +#undef silk_SAT16 +static OPUS_INLINE opus_int16 silk_SAT16(opus_int64 a){ + opus_int16 tmp; + ops_count += 1; + tmp = (opus_int16)((a) > silk_int16_MAX ? silk_int16_MAX : \ + ((a) < silk_int16_MIN ? silk_int16_MIN : (a))); + return(tmp); +} +#undef silk_SAT32 +static OPUS_INLINE opus_int32 silk_SAT32(opus_int64 a){ + opus_int32 tmp; + ops_count += 1; + tmp = (opus_int32)((a) > silk_int32_MAX ? silk_int32_MAX : \ + ((a) < silk_int32_MIN ? silk_int32_MIN : (a))); + return(tmp); +} +#undef silk_POS_SAT32 +static OPUS_INLINE opus_int32 silk_POS_SAT32(opus_int64 a){ + opus_int32 tmp; + ops_count += 1; + tmp = (opus_int32)((a) > silk_int32_MAX ? silk_int32_MAX : (a)); + return(tmp); +} + +#undef silk_ADD_POS_SAT8 +static OPUS_INLINE opus_int8 silk_ADD_POS_SAT8(opus_int64 a, opus_int64 b){ + opus_int8 tmp; + ops_count += 1; + tmp = (opus_int8)((((a)+(b)) & 0x80) ? silk_int8_MAX : ((a)+(b))); + return(tmp); +} +#undef silk_ADD_POS_SAT16 +static OPUS_INLINE opus_int16 silk_ADD_POS_SAT16(opus_int64 a, opus_int64 b){ + opus_int16 tmp; + ops_count += 1; + tmp = (opus_int16)((((a)+(b)) & 0x8000) ? silk_int16_MAX : ((a)+(b))); + return(tmp); +} + +#undef silk_ADD_POS_SAT32 +static OPUS_INLINE opus_int32 silk_ADD_POS_SAT32(opus_int64 a, opus_int64 b){ + opus_int32 tmp; + ops_count += 1; + tmp = (opus_int32)((((a)+(b)) & 0x80000000) ? silk_int32_MAX : ((a)+(b))); + return(tmp); +} + +#undef silk_LSHIFT8 +static OPUS_INLINE opus_int8 silk_LSHIFT8(opus_int8 a, opus_int32 shift){ + opus_int8 ret; + ops_count += 1; + ret = a << shift; + return ret; +} +#undef silk_LSHIFT16 +static OPUS_INLINE opus_int16 silk_LSHIFT16(opus_int16 a, opus_int32 shift){ + opus_int16 ret; + ops_count += 1; + ret = a << shift; + return ret; +} +#undef silk_LSHIFT32 +static OPUS_INLINE opus_int32 silk_LSHIFT32(opus_int32 a, opus_int32 shift){ + opus_int32 ret; + ops_count += 1; + ret = a << shift; + return ret; +} +#undef silk_LSHIFT64 +static OPUS_INLINE opus_int64 silk_LSHIFT64(opus_int64 a, opus_int shift){ + ops_count += 1; + return a << shift; +} + +#undef silk_LSHIFT_ovflw +static OPUS_INLINE opus_int32 silk_LSHIFT_ovflw(opus_int32 a, opus_int32 shift){ + ops_count += 1; + return a << shift; +} + +#undef silk_LSHIFT_uint +static OPUS_INLINE opus_uint32 silk_LSHIFT_uint(opus_uint32 a, opus_int32 shift){ + opus_uint32 ret; + ops_count += 1; + ret = a << shift; + return ret; +} + +#undef silk_RSHIFT8 +static OPUS_INLINE opus_int8 silk_RSHIFT8(opus_int8 a, opus_int32 shift){ + ops_count += 1; + return a >> shift; +} +#undef silk_RSHIFT16 +static OPUS_INLINE opus_int16 silk_RSHIFT16(opus_int16 a, opus_int32 shift){ + ops_count += 1; + return a >> shift; +} +#undef silk_RSHIFT32 +static OPUS_INLINE opus_int32 silk_RSHIFT32(opus_int32 a, opus_int32 shift){ + ops_count += 1; + return a >> shift; +} +#undef silk_RSHIFT64 +static OPUS_INLINE opus_int64 silk_RSHIFT64(opus_int64 a, opus_int64 shift){ + ops_count += 1; + return a >> shift; +} + +#undef silk_RSHIFT_uint +static OPUS_INLINE opus_uint32 silk_RSHIFT_uint(opus_uint32 a, opus_int32 shift){ + ops_count += 1; + return a >> shift; +} + +#undef silk_ADD_LSHIFT +static OPUS_INLINE opus_int32 silk_ADD_LSHIFT(opus_int32 a, opus_int32 b, opus_int32 shift){ + opus_int32 ret; + ops_count += 1; + ret = a + (b << shift); + return ret; /* shift >= 0*/ +} +#undef silk_ADD_LSHIFT32 +static OPUS_INLINE opus_int32 silk_ADD_LSHIFT32(opus_int32 a, opus_int32 b, opus_int32 shift){ + opus_int32 ret; + ops_count += 1; + ret = a + (b << shift); + return ret; /* shift >= 0*/ +} +#undef silk_ADD_LSHIFT_uint +static OPUS_INLINE opus_uint32 silk_ADD_LSHIFT_uint(opus_uint32 a, opus_uint32 b, opus_int32 shift){ + opus_uint32 ret; + ops_count += 1; + ret = a + (b << shift); + return ret; /* shift >= 0*/ +} +#undef silk_ADD_RSHIFT +static OPUS_INLINE opus_int32 silk_ADD_RSHIFT(opus_int32 a, opus_int32 b, opus_int32 shift){ + opus_int32 ret; + ops_count += 1; + ret = a + (b >> shift); + return ret; /* shift > 0*/ +} +#undef silk_ADD_RSHIFT32 +static OPUS_INLINE opus_int32 silk_ADD_RSHIFT32(opus_int32 a, opus_int32 b, opus_int32 shift){ + opus_int32 ret; + ops_count += 1; + ret = a + (b >> shift); + return ret; /* shift > 0*/ +} +#undef silk_ADD_RSHIFT_uint +static OPUS_INLINE opus_uint32 silk_ADD_RSHIFT_uint(opus_uint32 a, opus_uint32 b, opus_int32 shift){ + opus_uint32 ret; + ops_count += 1; + ret = a + (b >> shift); + return ret; /* shift > 0*/ +} +#undef silk_SUB_LSHIFT32 +static OPUS_INLINE opus_int32 silk_SUB_LSHIFT32(opus_int32 a, opus_int32 b, opus_int32 shift){ + opus_int32 ret; + ops_count += 1; + ret = a - (b << shift); + return ret; /* shift >= 0*/ +} +#undef silk_SUB_RSHIFT32 +static OPUS_INLINE opus_int32 silk_SUB_RSHIFT32(opus_int32 a, opus_int32 b, opus_int32 shift){ + opus_int32 ret; + ops_count += 1; + ret = a - (b >> shift); + return ret; /* shift > 0*/ +} + +#undef silk_RSHIFT_ROUND +static OPUS_INLINE opus_int32 silk_RSHIFT_ROUND(opus_int32 a, opus_int32 shift){ + opus_int32 ret; + ops_count += 3; + ret = shift == 1 ? (a >> 1) + (a & 1) : ((a >> (shift - 1)) + 1) >> 1; + return ret; +} + +#undef silk_RSHIFT_ROUND64 +static OPUS_INLINE opus_int64 silk_RSHIFT_ROUND64(opus_int64 a, opus_int32 shift){ + opus_int64 ret; + ops_count += 6; + ret = shift == 1 ? (a >> 1) + (a & 1) : ((a >> (shift - 1)) + 1) >> 1; + return ret; +} + +#undef silk_abs_int64 +static OPUS_INLINE opus_int64 silk_abs_int64(opus_int64 a){ + ops_count += 1; + return (((a) > 0) ? (a) : -(a)); /* Be careful, silk_abs returns wrong when input equals to silk_intXX_MIN*/ +} + +#undef silk_abs_int32 +static OPUS_INLINE opus_int32 silk_abs_int32(opus_int32 a){ + ops_count += 1; + return silk_abs(a); +} + + +#undef silk_min +static silk_min(a, b){ + ops_count += 1; + return (((a) < (b)) ? (a) : (b)); +} +#undef silk_max +static silk_max(a, b){ + ops_count += 1; + return (((a) > (b)) ? (a) : (b)); +} +#undef silk_sign +static silk_sign(a){ + ops_count += 1; + return ((a) > 0 ? 1 : ( (a) < 0 ? -1 : 0 )); +} + +#undef silk_ADD16 +static OPUS_INLINE opus_int16 silk_ADD16(opus_int16 a, opus_int16 b){ + opus_int16 ret; + ops_count += 1; + ret = a + b; + return ret; +} + +#undef silk_ADD32 +static OPUS_INLINE opus_int32 silk_ADD32(opus_int32 a, opus_int32 b){ + opus_int32 ret; + ops_count += 1; + ret = a + b; + return ret; +} + +#undef silk_ADD64 +static OPUS_INLINE opus_int64 silk_ADD64(opus_int64 a, opus_int64 b){ + opus_int64 ret; + ops_count += 2; + ret = a + b; + return ret; +} + +#undef silk_SUB16 +static OPUS_INLINE opus_int16 silk_SUB16(opus_int16 a, opus_int16 b){ + opus_int16 ret; + ops_count += 1; + ret = a - b; + return ret; +} + +#undef silk_SUB32 +static OPUS_INLINE opus_int32 silk_SUB32(opus_int32 a, opus_int32 b){ + opus_int32 ret; + ops_count += 1; + ret = a - b; + return ret; +} + +#undef silk_SUB64 +static OPUS_INLINE opus_int64 silk_SUB64(opus_int64 a, opus_int64 b){ + opus_int64 ret; + ops_count += 2; + ret = a - b; + return ret; +} + +#undef silk_ADD_SAT16 +static OPUS_INLINE opus_int16 silk_ADD_SAT16( opus_int16 a16, opus_int16 b16 ) { + opus_int16 res; + /* Nb will be counted in AKP_add32 and silk_SAT16*/ + res = (opus_int16)silk_SAT16( silk_ADD32( (opus_int32)(a16), (b16) ) ); + return res; +} + +#undef silk_ADD_SAT32 +static OPUS_INLINE opus_int32 silk_ADD_SAT32(opus_int32 a32, opus_int32 b32){ + opus_int32 res; + ops_count += 1; + res = ((((a32) + (b32)) & 0x80000000) == 0 ? \ + ((((a32) & (b32)) & 0x80000000) != 0 ? silk_int32_MIN : (a32)+(b32)) : \ + ((((a32) | (b32)) & 0x80000000) == 0 ? silk_int32_MAX : (a32)+(b32)) ); + return res; +} + +#undef silk_ADD_SAT64 +static OPUS_INLINE opus_int64 silk_ADD_SAT64( opus_int64 a64, opus_int64 b64 ) { + opus_int64 res; + ops_count += 1; + res = ((((a64) + (b64)) & 0x8000000000000000LL) == 0 ? \ + ((((a64) & (b64)) & 0x8000000000000000LL) != 0 ? silk_int64_MIN : (a64)+(b64)) : \ + ((((a64) | (b64)) & 0x8000000000000000LL) == 0 ? silk_int64_MAX : (a64)+(b64)) ); + return res; +} + +#undef silk_SUB_SAT16 +static OPUS_INLINE opus_int16 silk_SUB_SAT16( opus_int16 a16, opus_int16 b16 ) { + opus_int16 res; + silk_assert(0); + /* Nb will be counted in sub-macros*/ + res = (opus_int16)silk_SAT16( silk_SUB32( (opus_int32)(a16), (b16) ) ); + return res; +} + +#undef silk_SUB_SAT32 +static OPUS_INLINE opus_int32 silk_SUB_SAT32( opus_int32 a32, opus_int32 b32 ) { + opus_int32 res; + ops_count += 1; + res = ((((a32)-(b32)) & 0x80000000) == 0 ? \ + (( (a32) & ((b32)^0x80000000) & 0x80000000) ? silk_int32_MIN : (a32)-(b32)) : \ + ((((a32)^0x80000000) & (b32) & 0x80000000) ? silk_int32_MAX : (a32)-(b32)) ); + return res; +} + +#undef silk_SUB_SAT64 +static OPUS_INLINE opus_int64 silk_SUB_SAT64( opus_int64 a64, opus_int64 b64 ) { + opus_int64 res; + ops_count += 1; + res = ((((a64)-(b64)) & 0x8000000000000000LL) == 0 ? \ + (( (a64) & ((b64)^0x8000000000000000LL) & 0x8000000000000000LL) ? silk_int64_MIN : (a64)-(b64)) : \ + ((((a64)^0x8000000000000000LL) & (b64) & 0x8000000000000000LL) ? silk_int64_MAX : (a64)-(b64)) ); + + return res; +} + +#undef silk_SMULWW +static OPUS_INLINE opus_int32 silk_SMULWW(opus_int32 a32, opus_int32 b32){ + opus_int32 ret; + /* Nb will be counted in sub-macros*/ + ret = silk_MLA(silk_SMULWB((a32), (b32)), (a32), silk_RSHIFT_ROUND((b32), 16)); + return ret; +} + +#undef silk_SMLAWW +static OPUS_INLINE opus_int32 silk_SMLAWW(opus_int32 a32, opus_int32 b32, opus_int32 c32){ + opus_int32 ret; + /* Nb will be counted in sub-macros*/ + ret = silk_MLA(silk_SMLAWB((a32), (b32), (c32)), (b32), silk_RSHIFT_ROUND((c32), 16)); + return ret; +} + +#undef silk_min_int +static OPUS_INLINE opus_int silk_min_int(opus_int a, opus_int b) +{ + ops_count += 1; + return (((a) < (b)) ? (a) : (b)); +} + +#undef silk_min_16 +static OPUS_INLINE opus_int16 silk_min_16(opus_int16 a, opus_int16 b) +{ + ops_count += 1; + return (((a) < (b)) ? (a) : (b)); +} +#undef silk_min_32 +static OPUS_INLINE opus_int32 silk_min_32(opus_int32 a, opus_int32 b) +{ + ops_count += 1; + return (((a) < (b)) ? (a) : (b)); +} +#undef silk_min_64 +static OPUS_INLINE opus_int64 silk_min_64(opus_int64 a, opus_int64 b) +{ + ops_count += 1; + return (((a) < (b)) ? (a) : (b)); +} + +/* silk_min() versions with typecast in the function call */ +#undef silk_max_int +static OPUS_INLINE opus_int silk_max_int(opus_int a, opus_int b) +{ + ops_count += 1; + return (((a) > (b)) ? (a) : (b)); +} +#undef silk_max_16 +static OPUS_INLINE opus_int16 silk_max_16(opus_int16 a, opus_int16 b) +{ + ops_count += 1; + return (((a) > (b)) ? (a) : (b)); +} +#undef silk_max_32 +static OPUS_INLINE opus_int32 silk_max_32(opus_int32 a, opus_int32 b) +{ + ops_count += 1; + return (((a) > (b)) ? (a) : (b)); +} + +#undef silk_max_64 +static OPUS_INLINE opus_int64 silk_max_64(opus_int64 a, opus_int64 b) +{ + ops_count += 1; + return (((a) > (b)) ? (a) : (b)); +} + + +#undef silk_LIMIT_int +static OPUS_INLINE opus_int silk_LIMIT_int(opus_int a, opus_int limit1, opus_int limit2) +{ + opus_int ret; + ops_count += 6; + + ret = ((limit1) > (limit2) ? ((a) > (limit1) ? (limit1) : ((a) < (limit2) ? (limit2) : (a))) \ + : ((a) > (limit2) ? (limit2) : ((a) < (limit1) ? (limit1) : (a)))); + + return(ret); +} + +#undef silk_LIMIT_16 +static OPUS_INLINE opus_int16 silk_LIMIT_16(opus_int16 a, opus_int16 limit1, opus_int16 limit2) +{ + opus_int16 ret; + ops_count += 6; + + ret = ((limit1) > (limit2) ? ((a) > (limit1) ? (limit1) : ((a) < (limit2) ? (limit2) : (a))) \ + : ((a) > (limit2) ? (limit2) : ((a) < (limit1) ? (limit1) : (a)))); + +return(ret); +} + + +#undef silk_LIMIT_32 +static OPUS_INLINE opus_int32 silk_LIMIT_32(opus_int32 a, opus_int32 limit1, opus_int32 limit2) +{ + opus_int32 ret; + ops_count += 6; + + ret = ((limit1) > (limit2) ? ((a) > (limit1) ? (limit1) : ((a) < (limit2) ? (limit2) : (a))) \ + : ((a) > (limit2) ? (limit2) : ((a) < (limit1) ? (limit1) : (a)))); + return(ret); +} + +#else +#define varDefine +#define silk_SaveCount() + +#endif +#endif + diff --git a/port/third_party/opus/silk/MacroDebug.h b/port/third_party/opus/silk/MacroDebug.h new file mode 100644 index 00000000..3110da9a --- /dev/null +++ b/port/third_party/opus/silk/MacroDebug.h @@ -0,0 +1,945 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Copyright (C) 2012 Xiph.Org Foundation +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef MACRO_DEBUG_H +#define MACRO_DEBUG_H + +/* Redefine macro functions with extensive assertion in DEBUG mode. + As functions can't be undefined, this file can't work with SigProcFIX_MacroCount.h */ + +#if ( defined (FIXED_DEBUG) || ( 0 && defined (_DEBUG) ) ) && !defined (silk_MACRO_COUNT) + +#undef silk_ADD16 +#define silk_ADD16(a,b) silk_ADD16_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int16 silk_ADD16_(opus_int16 a, opus_int16 b, char *file, int line){ + opus_int16 ret; + + ret = a + b; + if ( ret != silk_ADD_SAT16( a, b ) ) + { + fprintf (stderr, "silk_ADD16(%d, %d) in %s: line %d\n", a, b, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_ADD32 +#define silk_ADD32(a,b) silk_ADD32_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_ADD32_(opus_int32 a, opus_int32 b, char *file, int line){ + opus_int32 ret; + + ret = (opus_int32)((opus_uint32)a + (opus_uint32)b); + if ( ret != silk_ADD_SAT32( a, b ) ) + { + fprintf (stderr, "silk_ADD32(%d, %d) in %s: line %d\n", a, b, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_ADD64 +#define silk_ADD64(a,b) silk_ADD64_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int64 silk_ADD64_(opus_int64 a, opus_int64 b, char *file, int line){ + opus_int64 ret; + + ret = a + b; + if ( ret != silk_ADD_SAT64( a, b ) ) + { + fprintf (stderr, "silk_ADD64(%lld, %lld) in %s: line %d\n", (long long)a, (long long)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_SUB16 +#define silk_SUB16(a,b) silk_SUB16_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int16 silk_SUB16_(opus_int16 a, opus_int16 b, char *file, int line){ + opus_int16 ret; + + ret = a - b; + if ( ret != silk_SUB_SAT16( a, b ) ) + { + fprintf (stderr, "silk_SUB16(%d, %d) in %s: line %d\n", a, b, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_SUB32 +#define silk_SUB32(a,b) silk_SUB32_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SUB32_(opus_int32 a, opus_int32 b, char *file, int line){ + opus_int64 ret; + + ret = a - (opus_int64)b; + if ( ret != silk_SUB_SAT32( a, b ) ) + { + fprintf (stderr, "silk_SUB32(%d, %d) in %s: line %d\n", a, b, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_SUB64 +#define silk_SUB64(a,b) silk_SUB64_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int64 silk_SUB64_(opus_int64 a, opus_int64 b, char *file, int line){ + opus_int64 ret; + + ret = a - b; + if ( ret != silk_SUB_SAT64( a, b ) ) + { + fprintf (stderr, "silk_SUB64(%lld, %lld) in %s: line %d\n", (long long)a, (long long)b, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_ADD_SAT16 +#define silk_ADD_SAT16(a,b) silk_ADD_SAT16_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int16 silk_ADD_SAT16_( opus_int16 a16, opus_int16 b16, char *file, int line) { + opus_int16 res; + res = (opus_int16)silk_SAT16( silk_ADD32( (opus_int32)(a16), (b16) ) ); + if ( res != silk_SAT16( (opus_int32)a16 + (opus_int32)b16 ) ) + { + fprintf (stderr, "silk_ADD_SAT16(%d, %d) in %s: line %d\n", a16, b16, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return res; +} + +#undef silk_ADD_SAT32 +#define silk_ADD_SAT32(a,b) silk_ADD_SAT32_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_ADD_SAT32_(opus_int32 a32, opus_int32 b32, char *file, int line){ + opus_int32 res; + res = ((((opus_uint32)(a32) + (opus_uint32)(b32)) & 0x80000000) == 0 ? \ + ((((a32) & (b32)) & 0x80000000) != 0 ? silk_int32_MIN : (a32)+(b32)) : \ + ((((a32) | (b32)) & 0x80000000) == 0 ? silk_int32_MAX : (a32)+(b32)) ); + if ( res != silk_SAT32( (opus_int64)a32 + (opus_int64)b32 ) ) + { + fprintf (stderr, "silk_ADD_SAT32(%d, %d) in %s: line %d\n", a32, b32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return res; +} + +#undef silk_ADD_SAT64 +#define silk_ADD_SAT64(a,b) silk_ADD_SAT64_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int64 silk_ADD_SAT64_( opus_int64 a64, opus_int64 b64, char *file, int line) { + opus_int64 res; + int fail = 0; + res = ((((a64) + (b64)) & 0x8000000000000000LL) == 0 ? \ + ((((a64) & (b64)) & 0x8000000000000000LL) != 0 ? silk_int64_MIN : (a64)+(b64)) : \ + ((((a64) | (b64)) & 0x8000000000000000LL) == 0 ? silk_int64_MAX : (a64)+(b64)) ); + if( res != a64 + b64 ) { + /* Check that we saturated to the correct extreme value */ + if ( !(( res == silk_int64_MAX && ( ( a64 >> 1 ) + ( b64 >> 1 ) > ( silk_int64_MAX >> 3 ) ) ) || + ( res == silk_int64_MIN && ( ( a64 >> 1 ) + ( b64 >> 1 ) < ( silk_int64_MIN >> 3 ) ) ) ) ) + { + fail = 1; + } + } else { + /* Saturation not necessary */ + fail = res != a64 + b64; + } + if ( fail ) + { + fprintf (stderr, "silk_ADD_SAT64(%lld, %lld) in %s: line %d\n", (long long)a64, (long long)b64, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return res; +} + +#undef silk_SUB_SAT16 +#define silk_SUB_SAT16(a,b) silk_SUB_SAT16_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int16 silk_SUB_SAT16_( opus_int16 a16, opus_int16 b16, char *file, int line ) { + opus_int16 res; + res = (opus_int16)silk_SAT16( silk_SUB32( (opus_int32)(a16), (b16) ) ); + if ( res != silk_SAT16( (opus_int32)a16 - (opus_int32)b16 ) ) + { + fprintf (stderr, "silk_SUB_SAT16(%d, %d) in %s: line %d\n", a16, b16, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return res; +} + +#undef silk_SUB_SAT32 +#define silk_SUB_SAT32(a,b) silk_SUB_SAT32_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SUB_SAT32_( opus_int32 a32, opus_int32 b32, char *file, int line ) { + opus_int32 res; + res = ((((opus_uint32)(a32)-(opus_uint32)(b32)) & 0x80000000) == 0 ? \ + (( (a32) & ((b32)^0x80000000) & 0x80000000) ? silk_int32_MIN : (a32)-(b32)) : \ + ((((a32)^0x80000000) & (b32) & 0x80000000) ? silk_int32_MAX : (a32)-(b32)) ); + if ( res != silk_SAT32( (opus_int64)a32 - (opus_int64)b32 ) ) + { + fprintf (stderr, "silk_SUB_SAT32(%d, %d) in %s: line %d\n", a32, b32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return res; +} + +#undef silk_SUB_SAT64 +#define silk_SUB_SAT64(a,b) silk_SUB_SAT64_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int64 silk_SUB_SAT64_( opus_int64 a64, opus_int64 b64, char *file, int line ) { + opus_int64 res; + int fail = 0; + res = ((((a64)-(b64)) & 0x8000000000000000LL) == 0 ? \ + (( (a64) & ((b64)^0x8000000000000000LL) & 0x8000000000000000LL) ? silk_int64_MIN : (a64)-(b64)) : \ + ((((a64)^0x8000000000000000LL) & (b64) & 0x8000000000000000LL) ? silk_int64_MAX : (a64)-(b64)) ); + if( res != a64 - b64 ) { + /* Check that we saturated to the correct extreme value */ + if( !(( res == silk_int64_MAX && ( ( a64 >> 1 ) + ( b64 >> 1 ) > ( silk_int64_MAX >> 3 ) ) ) || + ( res == silk_int64_MIN && ( ( a64 >> 1 ) + ( b64 >> 1 ) < ( silk_int64_MIN >> 3 ) ) ) )) + { + fail = 1; + } + } else { + /* Saturation not necessary */ + fail = res != a64 - b64; + } + if ( fail ) + { + fprintf (stderr, "silk_SUB_SAT64(%lld, %lld) in %s: line %d\n", (long long)a64, (long long)b64, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return res; +} + +#undef silk_MUL +#define silk_MUL(a,b) silk_MUL_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_MUL_(opus_int32 a32, opus_int32 b32, char *file, int line){ + opus_int32 ret; + opus_int64 ret64; + ret = (opus_int32)((opus_uint32)a32 * (opus_uint32)b32); + ret64 = (opus_int64)a32 * (opus_int64)b32; + if ( (opus_int64)ret != ret64 ) + { + fprintf (stderr, "silk_MUL(%d, %d) in %s: line %d\n", a32, b32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_MUL_uint +#define silk_MUL_uint(a,b) silk_MUL_uint_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_uint32 silk_MUL_uint_(opus_uint32 a32, opus_uint32 b32, char *file, int line){ + opus_uint32 ret; + ret = a32 * b32; + if ( (opus_uint64)ret != (opus_uint64)a32 * (opus_uint64)b32 ) + { + fprintf (stderr, "silk_MUL_uint(%u, %u) in %s: line %d\n", a32, b32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_MLA +#define silk_MLA(a,b,c) silk_MLA_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_MLA_(opus_int32 a32, opus_int32 b32, opus_int32 c32, char *file, int line){ + opus_int32 ret; + ret = a32 + b32 * c32; + if ( (opus_int64)ret != (opus_int64)a32 + (opus_int64)b32 * (opus_int64)c32 ) + { + fprintf (stderr, "silk_MLA(%d, %d, %d) in %s: line %d\n", a32, b32, c32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_MLA_uint +#define silk_MLA_uint(a,b,c) silk_MLA_uint_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_MLA_uint_(opus_uint32 a32, opus_uint32 b32, opus_uint32 c32, char *file, int line){ + opus_uint32 ret; + ret = a32 + b32 * c32; + if ( (opus_int64)ret != (opus_int64)a32 + (opus_int64)b32 * (opus_int64)c32 ) + { + fprintf (stderr, "silk_MLA_uint(%d, %d, %d) in %s: line %d\n", a32, b32, c32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_SMULWB +#define silk_SMULWB(a,b) silk_SMULWB_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SMULWB_(opus_int32 a32, opus_int32 b32, char *file, int line){ + opus_int32 ret; + ret = (a32 >> 16) * (opus_int32)((opus_int16)b32) + (((a32 & 0x0000FFFF) * (opus_int32)((opus_int16)b32)) >> 16); + if ( (opus_int64)ret != ((opus_int64)a32 * (opus_int16)b32) >> 16 ) + { + fprintf (stderr, "silk_SMULWB(%d, %d) in %s: line %d\n", a32, b32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_SMLAWB +#define silk_SMLAWB(a,b,c) silk_SMLAWB_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SMLAWB_(opus_int32 a32, opus_int32 b32, opus_int32 c32, char *file, int line){ + opus_int32 ret; + ret = silk_ADD32_ovflw( a32, silk_SMULWB( b32, c32 ) ); + if ( ret != silk_ADD_SAT32( a32, silk_SMULWB( b32, c32 ) ) ) + { + fprintf (stderr, "silk_SMLAWB(%d, %d, %d) in %s: line %d\n", a32, b32, c32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_SMULWT +#define silk_SMULWT(a,b) silk_SMULWT_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SMULWT_(opus_int32 a32, opus_int32 b32, char *file, int line){ + opus_int32 ret; + ret = (a32 >> 16) * (b32 >> 16) + (((a32 & 0x0000FFFF) * (b32 >> 16)) >> 16); + if ( (opus_int64)ret != ((opus_int64)a32 * (b32 >> 16)) >> 16 ) + { + fprintf (stderr, "silk_SMULWT(%d, %d) in %s: line %d\n", a32, b32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_SMLAWT +#define silk_SMLAWT(a,b,c) silk_SMLAWT_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SMLAWT_(opus_int32 a32, opus_int32 b32, opus_int32 c32, char *file, int line){ + opus_int32 ret; + ret = a32 + ((b32 >> 16) * (c32 >> 16)) + (((b32 & 0x0000FFFF) * ((c32 >> 16)) >> 16)); + if ( (opus_int64)ret != (opus_int64)a32 + (((opus_int64)b32 * (c32 >> 16)) >> 16) ) + { + fprintf (stderr, "silk_SMLAWT(%d, %d, %d) in %s: line %d\n", a32, b32, c32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_SMULL +#define silk_SMULL(a,b) silk_SMULL_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int64 silk_SMULL_(opus_int64 a64, opus_int64 b64, char *file, int line){ + opus_int64 ret64; + int fail = 0; + ret64 = a64 * b64; + if( b64 != 0 ) { + fail = a64 != (ret64 / b64); + } else if( a64 != 0 ) { + fail = b64 != (ret64 / a64); + } + if ( fail ) + { + fprintf (stderr, "silk_SMULL(%lld, %lld) in %s: line %d\n", (long long)a64, (long long)b64, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret64; +} + +/* no checking needed for silk_SMULBB */ +#undef silk_SMLABB +#define silk_SMLABB(a,b,c) silk_SMLABB_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SMLABB_(opus_int32 a32, opus_int32 b32, opus_int32 c32, char *file, int line){ + opus_int32 ret; + ret = a32 + (opus_int32)((opus_int16)b32) * (opus_int32)((opus_int16)c32); + if ( (opus_int64)ret != (opus_int64)a32 + (opus_int64)b32 * (opus_int16)c32 ) + { + fprintf (stderr, "silk_SMLABB(%d, %d, %d) in %s: line %d\n", a32, b32, c32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +/* no checking needed for silk_SMULBT */ +#undef silk_SMLABT +#define silk_SMLABT(a,b,c) silk_SMLABT_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SMLABT_(opus_int32 a32, opus_int32 b32, opus_int32 c32, char *file, int line){ + opus_int32 ret; + ret = a32 + ((opus_int32)((opus_int16)b32)) * (c32 >> 16); + if ( (opus_int64)ret != (opus_int64)a32 + (opus_int64)b32 * (c32 >> 16) ) + { + fprintf (stderr, "silk_SMLABT(%d, %d, %d) in %s: line %d\n", a32, b32, c32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +/* no checking needed for silk_SMULTT */ +#undef silk_SMLATT +#define silk_SMLATT(a,b,c) silk_SMLATT_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SMLATT_(opus_int32 a32, opus_int32 b32, opus_int32 c32, char *file, int line){ + opus_int32 ret; + ret = a32 + (b32 >> 16) * (c32 >> 16); + if ( (opus_int64)ret != (opus_int64)a32 + (b32 >> 16) * (c32 >> 16) ) + { + fprintf (stderr, "silk_SMLATT(%d, %d, %d) in %s: line %d\n", a32, b32, c32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_SMULWW +#define silk_SMULWW(a,b) silk_SMULWW_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SMULWW_(opus_int32 a32, opus_int32 b32, char *file, int line){ + opus_int32 ret, tmp1, tmp2; + opus_int64 ret64; + int fail = 0; + + ret = silk_SMULWB( a32, b32 ); + tmp1 = silk_RSHIFT_ROUND( b32, 16 ); + tmp2 = silk_MUL( a32, tmp1 ); + + fail |= (opus_int64)tmp2 != (opus_int64) a32 * (opus_int64) tmp1; + + tmp1 = ret; + ret = silk_ADD32( tmp1, tmp2 ); + fail |= silk_ADD32( tmp1, tmp2 ) != silk_ADD_SAT32( tmp1, tmp2 ); + + ret64 = silk_RSHIFT64( silk_SMULL( a32, b32 ), 16 ); + fail |= (opus_int64)ret != ret64; + + if ( fail ) + { + fprintf (stderr, "silk_SMULWW(%d, %d) in %s: line %d\n", a32, b32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + + return ret; +} + +#undef silk_SMLAWW +#define silk_SMLAWW(a,b,c) silk_SMLAWW_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SMLAWW_(opus_int32 a32, opus_int32 b32, opus_int32 c32, char *file, int line){ + opus_int32 ret, tmp; + + tmp = silk_SMULWW( b32, c32 ); + ret = silk_ADD32( a32, tmp ); + if ( ret != silk_ADD_SAT32( a32, tmp ) ) + { + fprintf (stderr, "silk_SMLAWW(%d, %d, %d) in %s: line %d\n", a32, b32, c32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +/* no checking needed for silk_SMULL + no checking needed for silk_SMLAL + no checking needed for silk_SMLALBB + no checking needed for SigProcFIX_CLZ16 + no checking needed for SigProcFIX_CLZ32*/ + +#undef silk_DIV32 +#define silk_DIV32(a,b) silk_DIV32_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_DIV32_(opus_int32 a32, opus_int32 b32, char *file, int line){ + if ( b32 == 0 ) + { + fprintf (stderr, "silk_DIV32(%d, %d) in %s: line %d\n", a32, b32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return a32 / b32; +} + +#undef silk_DIV32_16 +#define silk_DIV32_16(a,b) silk_DIV32_16_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_DIV32_16_(opus_int32 a32, opus_int32 b32, char *file, int line){ + int fail = 0; + fail |= b32 == 0; + fail |= b32 > silk_int16_MAX; + fail |= b32 < silk_int16_MIN; + if ( fail ) + { + fprintf (stderr, "silk_DIV32_16(%d, %d) in %s: line %d\n", a32, b32, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return a32 / b32; +} + +/* no checking needed for silk_SAT8 + no checking needed for silk_SAT16 + no checking needed for silk_SAT32 + no checking needed for silk_POS_SAT32 + no checking needed for silk_ADD_POS_SAT8 + no checking needed for silk_ADD_POS_SAT16 + no checking needed for silk_ADD_POS_SAT32 */ + +#undef silk_LSHIFT8 +#define silk_LSHIFT8(a,b) silk_LSHIFT8_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int8 silk_LSHIFT8_(opus_int8 a, opus_int32 shift, char *file, int line){ + opus_int8 ret; + int fail = 0; + ret = (opus_int8)((opus_uint8)a << shift); + fail |= shift < 0; + fail |= shift >= 8; + fail |= (opus_int64)ret != (opus_int64)(((opus_uint64)a) << shift); + if ( fail ) + { + fprintf (stderr, "silk_LSHIFT8(%d, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_LSHIFT16 +#define silk_LSHIFT16(a,b) silk_LSHIFT16_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int16 silk_LSHIFT16_(opus_int16 a, opus_int32 shift, char *file, int line){ + opus_int16 ret; + int fail = 0; + ret = (opus_int16)((opus_uint16)a << shift); + fail |= shift < 0; + fail |= shift >= 16; + fail |= (opus_int64)ret != (opus_int64)(((opus_uint64)a) << shift); + if ( fail ) + { + fprintf (stderr, "silk_LSHIFT16(%d, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_LSHIFT32 +#define silk_LSHIFT32(a,b) silk_LSHIFT32_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_LSHIFT32_(opus_int32 a, opus_int32 shift, char *file, int line){ + opus_int32 ret; + int fail = 0; + ret = (opus_int32)((opus_uint32)a << shift); + fail |= shift < 0; + fail |= shift >= 32; + fail |= (opus_int64)ret != (opus_int64)(((opus_uint64)a) << shift); + if ( fail ) + { + fprintf (stderr, "silk_LSHIFT32(%d, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_LSHIFT64 +#define silk_LSHIFT64(a,b) silk_LSHIFT64_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int64 silk_LSHIFT64_(opus_int64 a, opus_int shift, char *file, int line){ + opus_int64 ret; + int fail = 0; + ret = (opus_int64)((opus_uint64)a << shift); + fail |= shift < 0; + fail |= shift >= 64; + fail |= (ret>>shift) != ((opus_int64)a); + if ( fail ) + { + fprintf (stderr, "silk_LSHIFT64(%lld, %d) in %s: line %d\n", (long long)a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_LSHIFT_ovflw +#define silk_LSHIFT_ovflw(a,b) silk_LSHIFT_ovflw_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_LSHIFT_ovflw_(opus_int32 a, opus_int32 shift, char *file, int line){ + if ( (shift < 0) || (shift >= 32) ) /* no check for overflow */ + { + fprintf (stderr, "silk_LSHIFT_ovflw(%d, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return a << shift; +} + +#undef silk_LSHIFT_uint +#define silk_LSHIFT_uint(a,b) silk_LSHIFT_uint_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_uint32 silk_LSHIFT_uint_(opus_uint32 a, opus_int32 shift, char *file, int line){ + opus_uint32 ret; + ret = a << shift; + if ( (shift < 0) || ((opus_int64)ret != ((opus_int64)a) << shift)) + { + fprintf (stderr, "silk_LSHIFT_uint(%u, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_RSHIFT8 +#define silk_RSHITF8(a,b) silk_RSHIFT8_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int8 silk_RSHIFT8_(opus_int8 a, opus_int32 shift, char *file, int line){ + if ( (shift < 0) || (shift>=8) ) + { + fprintf (stderr, "silk_RSHITF8(%d, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return a >> shift; +} + +#undef silk_RSHIFT16 +#define silk_RSHITF16(a,b) silk_RSHIFT16_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int16 silk_RSHIFT16_(opus_int16 a, opus_int32 shift, char *file, int line){ + if ( (shift < 0) || (shift>=16) ) + { + fprintf (stderr, "silk_RSHITF16(%d, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return a >> shift; +} + +#undef silk_RSHIFT32 +#define silk_RSHIFT32(a,b) silk_RSHIFT32_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_RSHIFT32_(opus_int32 a, opus_int32 shift, char *file, int line){ + if ( (shift < 0) || (shift>=32) ) + { + fprintf (stderr, "silk_RSHITF32(%d, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return a >> shift; +} + +#undef silk_RSHIFT64 +#define silk_RSHIFT64(a,b) silk_RSHIFT64_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int64 silk_RSHIFT64_(opus_int64 a, opus_int64 shift, char *file, int line){ + if ( (shift < 0) || (shift>=64) ) + { + fprintf (stderr, "silk_RSHITF64(%lld, %lld) in %s: line %d\n", (long long)a, (long long)shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return a >> shift; +} + +#undef silk_RSHIFT_uint +#define silk_RSHIFT_uint(a,b) silk_RSHIFT_uint_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_uint32 silk_RSHIFT_uint_(opus_uint32 a, opus_int32 shift, char *file, int line){ + if ( (shift < 0) || (shift>32) ) + { + fprintf (stderr, "silk_RSHIFT_uint(%u, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return a >> shift; +} + +#undef silk_ADD_LSHIFT +#define silk_ADD_LSHIFT(a,b,c) silk_ADD_LSHIFT_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE int silk_ADD_LSHIFT_(int a, int b, int shift, char *file, int line){ + opus_int16 ret; + ret = a + (opus_int16)((opus_uint16)b << shift); + if ( (shift < 0) || (shift>15) || ((opus_int64)ret != (opus_int64)a + (opus_int64)(((opus_uint64)b) << shift)) ) + { + fprintf (stderr, "silk_ADD_LSHIFT(%d, %d, %d) in %s: line %d\n", a, b, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; /* shift >= 0 */ +} + +#undef silk_ADD_LSHIFT32 +#define silk_ADD_LSHIFT32(a,b,c) silk_ADD_LSHIFT32_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_ADD_LSHIFT32_(opus_int32 a, opus_int32 b, opus_int32 shift, char *file, int line){ + opus_int32 ret; + ret = silk_ADD32_ovflw(a, (opus_int32)((opus_uint32)b << shift)); + if ( (shift < 0) || (shift>31) || ((opus_int64)ret != (opus_int64)a + (opus_int64)(((opus_uint64)b) << shift)) ) + { + fprintf (stderr, "silk_ADD_LSHIFT32(%d, %d, %d) in %s: line %d\n", a, b, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; /* shift >= 0 */ +} + +#undef silk_ADD_LSHIFT_uint +#define silk_ADD_LSHIFT_uint(a,b,c) silk_ADD_LSHIFT_uint_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_uint32 silk_ADD_LSHIFT_uint_(opus_uint32 a, opus_uint32 b, opus_int32 shift, char *file, int line){ + opus_uint32 ret; + ret = a + (b << shift); + if ( (shift < 0) || (shift>32) || ((opus_int64)ret != (opus_int64)a + (((opus_int64)b) << shift)) ) + { + fprintf (stderr, "silk_ADD_LSHIFT_uint(%u, %u, %d) in %s: line %d\n", a, b, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; /* shift >= 0 */ +} + +#undef silk_ADD_RSHIFT +#define silk_ADD_RSHIFT(a,b,c) silk_ADD_RSHIFT_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE int silk_ADD_RSHIFT_(int a, int b, int shift, char *file, int line){ + opus_int16 ret; + ret = a + (b >> shift); + if ( (shift < 0) || (shift>15) || ((opus_int64)ret != (opus_int64)a + (((opus_int64)b) >> shift)) ) + { + fprintf (stderr, "silk_ADD_RSHIFT(%d, %d, %d) in %s: line %d\n", a, b, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; /* shift > 0 */ +} + +#undef silk_ADD_RSHIFT32 +#define silk_ADD_RSHIFT32(a,b,c) silk_ADD_RSHIFT32_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_ADD_RSHIFT32_(opus_int32 a, opus_int32 b, opus_int32 shift, char *file, int line){ + opus_int32 ret; + ret = silk_ADD32_ovflw(a, (b >> shift)); + if ( (shift < 0) || (shift>31) || ((opus_int64)ret != (opus_int64)a + (((opus_int64)b) >> shift)) ) + { + fprintf (stderr, "silk_ADD_RSHIFT32(%d, %d, %d) in %s: line %d\n", a, b, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; /* shift > 0 */ +} + +#undef silk_ADD_RSHIFT_uint +#define silk_ADD_RSHIFT_uint(a,b,c) silk_ADD_RSHIFT_uint_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_uint32 silk_ADD_RSHIFT_uint_(opus_uint32 a, opus_uint32 b, opus_int32 shift, char *file, int line){ + opus_uint32 ret; + ret = a + (b >> shift); + if ( (shift < 0) || (shift>32) || ((opus_int64)ret != (opus_int64)a + (((opus_int64)b) >> shift)) ) + { + fprintf (stderr, "silk_ADD_RSHIFT_uint(%u, %u, %d) in %s: line %d\n", a, b, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; /* shift > 0 */ +} + +#undef silk_SUB_LSHIFT32 +#define silk_SUB_LSHIFT32(a,b,c) silk_SUB_LSHIFT32_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SUB_LSHIFT32_(opus_int32 a, opus_int32 b, opus_int32 shift, char *file, int line){ + opus_int32 ret; + ret = silk_SUB32_ovflw(a, (opus_int32)((opus_uint32)b << shift)); + if ( (shift < 0) || (shift>31) || ((opus_int64)ret != (opus_int64)a - (opus_int64)(((opus_uint64)b) << shift)) ) + { + fprintf (stderr, "silk_SUB_LSHIFT32(%d, %d, %d) in %s: line %d\n", a, b, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; /* shift >= 0 */ +} + +#undef silk_SUB_RSHIFT32 +#define silk_SUB_RSHIFT32(a,b,c) silk_SUB_RSHIFT32_((a), (b), (c), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_SUB_RSHIFT32_(opus_int32 a, opus_int32 b, opus_int32 shift, char *file, int line){ + opus_int32 ret; + ret = silk_SUB32_ovflw(a, (b >> shift)); + if ( (shift < 0) || (shift>31) || ((opus_int64)ret != (opus_int64)a - (((opus_int64)b) >> shift)) ) + { + fprintf (stderr, "silk_SUB_RSHIFT32(%d, %d, %d) in %s: line %d\n", a, b, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; /* shift > 0 */ +} + +#undef silk_RSHIFT_ROUND +#define silk_RSHIFT_ROUND(a,b) silk_RSHIFT_ROUND_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_RSHIFT_ROUND_(opus_int32 a, opus_int32 shift, char *file, int line){ + opus_int32 ret; + ret = shift == 1 ? (a >> 1) + (a & 1) : ((a >> (shift - 1)) + 1) >> 1; + /* the macro definition can't handle a shift of zero */ + if ( (shift <= 0) || (shift>31) || ((opus_int64)ret != ((opus_int64)a + ((opus_int64)1 << (shift - 1))) >> shift) ) + { + fprintf (stderr, "silk_RSHIFT_ROUND(%d, %d) in %s: line %d\n", a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return ret; +} + +#undef silk_RSHIFT_ROUND64 +#define silk_RSHIFT_ROUND64(a,b) silk_RSHIFT_ROUND64_((a), (b), __FILE__, __LINE__) +static OPUS_INLINE opus_int64 silk_RSHIFT_ROUND64_(opus_int64 a, opus_int32 shift, char *file, int line){ + opus_int64 ret; + /* the macro definition can't handle a shift of zero */ + if ( (shift <= 0) || (shift>=64) ) + { + fprintf (stderr, "silk_RSHIFT_ROUND64(%lld, %d) in %s: line %d\n", (long long)a, shift, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + ret = shift == 1 ? (a >> 1) + (a & 1) : ((a >> (shift - 1)) + 1) >> 1; + return ret; +} + +/* silk_abs is used on floats also, so doesn't work... */ +/*#undef silk_abs +static OPUS_INLINE opus_int32 silk_abs(opus_int32 a){ + silk_assert(a != 0x80000000); + return (((a) > 0) ? (a) : -(a)); // Be careful, silk_abs returns wrong when input equals to silk_intXX_MIN +}*/ + +#undef silk_abs_int64 +#define silk_abs_int64(a) silk_abs_int64_((a), __FILE__, __LINE__) +static OPUS_INLINE opus_int64 silk_abs_int64_(opus_int64 a, char *file, int line){ + if ( a == silk_int64_MIN ) + { + fprintf (stderr, "silk_abs_int64(%lld) in %s: line %d\n", (long long)a, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return (((a) > 0) ? (a) : -(a)); /* Be careful, silk_abs returns wrong when input equals to silk_intXX_MIN */ +} + +#undef silk_abs_int32 +#define silk_abs_int32(a) silk_abs_int32_((a), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_abs_int32_(opus_int32 a, char *file, int line){ + if ( a == silk_int32_MIN ) + { + fprintf (stderr, "silk_abs_int32(%d) in %s: line %d\n", a, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return silk_abs(a); +} + +#undef silk_CHECK_FIT8 +#define silk_CHECK_FIT8(a) silk_CHECK_FIT8_((a), __FILE__, __LINE__) +static OPUS_INLINE opus_int8 silk_CHECK_FIT8_( opus_int64 a, char *file, int line ){ + opus_int8 ret; + ret = (opus_int8)a; + if ( (opus_int64)ret != a ) + { + fprintf (stderr, "silk_CHECK_FIT8(%lld) in %s: line %d\n", (long long)a, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return( ret ); +} + +#undef silk_CHECK_FIT16 +#define silk_CHECK_FIT16(a) silk_CHECK_FIT16_((a), __FILE__, __LINE__) +static OPUS_INLINE opus_int16 silk_CHECK_FIT16_( opus_int64 a, char *file, int line ){ + opus_int16 ret; + ret = (opus_int16)a; + if ( (opus_int64)ret != a ) + { + fprintf (stderr, "silk_CHECK_FIT16(%lld) in %s: line %d\n", (long long)a, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return( ret ); +} + +#undef silk_CHECK_FIT32 +#define silk_CHECK_FIT32(a) silk_CHECK_FIT32_((a), __FILE__, __LINE__) +static OPUS_INLINE opus_int32 silk_CHECK_FIT32_( opus_int64 a, char *file, int line ){ + opus_int32 ret; + ret = (opus_int32)a; + if ( (opus_int64)ret != a ) + { + fprintf (stderr, "silk_CHECK_FIT32(%lld) in %s: line %d\n", (long long)a, file, line); +#ifdef FIXED_DEBUG_ASSERT + silk_assert( 0 ); +#endif + } + return( ret ); +} + +/* no checking for silk_NSHIFT_MUL_32_32 + no checking for silk_NSHIFT_MUL_16_16 + no checking needed for silk_min + no checking needed for silk_max + no checking needed for silk_sign +*/ + +#endif +#endif /* MACRO_DEBUG_H */ diff --git a/port/third_party/opus/silk/NLSF2A.c b/port/third_party/opus/silk/NLSF2A.c new file mode 100644 index 00000000..d5b77306 --- /dev/null +++ b/port/third_party/opus/silk/NLSF2A.c @@ -0,0 +1,141 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/* conversion between prediction filter coefficients and LSFs */ +/* order should be even */ +/* a piecewise linear approximation maps LSF <-> cos(LSF) */ +/* therefore the result is not accurate LSFs, but the two */ +/* functions are accurate inverses of each other */ + +#include "SigProc_FIX.h" +#include "tables.h" + +#define QA 16 + +/* helper function for NLSF2A(..) */ +static OPUS_INLINE void silk_NLSF2A_find_poly( + opus_int32 *out, /* O intermediate polynomial, QA [dd+1] */ + const opus_int32 *cLSF, /* I vector of interleaved 2*cos(LSFs), QA [d] */ + opus_int dd /* I polynomial order (= 1/2 * filter order) */ +) +{ + opus_int k, n; + opus_int32 ftmp; + + out[0] = silk_LSHIFT( 1, QA ); + out[1] = -cLSF[0]; + for( k = 1; k < dd; k++ ) { + ftmp = cLSF[2*k]; /* QA*/ + out[k+1] = silk_LSHIFT( out[k-1], 1 ) - (opus_int32)silk_RSHIFT_ROUND64( silk_SMULL( ftmp, out[k] ), QA ); + for( n = k; n > 1; n-- ) { + out[n] += out[n-2] - (opus_int32)silk_RSHIFT_ROUND64( silk_SMULL( ftmp, out[n-1] ), QA ); + } + out[1] -= ftmp; + } +} + +/* compute whitening filter coefficients from normalized line spectral frequencies */ +void silk_NLSF2A( + opus_int16 *a_Q12, /* O monic whitening filter coefficients in Q12, [ d ] */ + const opus_int16 *NLSF, /* I normalized line spectral frequencies in Q15, [ d ] */ + const opus_int d, /* I filter order (should be even) */ + int arch /* I Run-time architecture */ +) +{ + /* This ordering was found to maximize quality. It improves numerical accuracy of + silk_NLSF2A_find_poly() compared to "standard" ordering. */ + static const unsigned char ordering16[16] = { + 0, 15, 8, 7, 4, 11, 12, 3, 2, 13, 10, 5, 6, 9, 14, 1 + }; + static const unsigned char ordering10[10] = { + 0, 9, 6, 3, 4, 5, 8, 1, 2, 7 + }; + const unsigned char *ordering; + opus_int k, i, dd; + opus_int32 cos_LSF_QA[ SILK_MAX_ORDER_LPC ]; + opus_int32 P[ SILK_MAX_ORDER_LPC / 2 + 1 ], Q[ SILK_MAX_ORDER_LPC / 2 + 1 ]; + opus_int32 Ptmp, Qtmp, f_int, f_frac, cos_val, delta; + opus_int32 a32_QA1[ SILK_MAX_ORDER_LPC ]; + + silk_assert( LSF_COS_TAB_SZ_FIX == 128 ); + celt_assert( d==10 || d==16 ); + + /* convert LSFs to 2*cos(LSF), using piecewise linear curve from table */ + ordering = d == 16 ? ordering16 : ordering10; + for( k = 0; k < d; k++ ) { + silk_assert( NLSF[k] >= 0 ); + + /* f_int on a scale 0-127 (rounded down) */ + f_int = silk_RSHIFT( NLSF[k], 15 - 7 ); + + /* f_frac, range: 0..255 */ + f_frac = NLSF[k] - silk_LSHIFT( f_int, 15 - 7 ); + + silk_assert(f_int >= 0); + silk_assert(f_int < LSF_COS_TAB_SZ_FIX ); + + /* Read start and end value from table */ + cos_val = silk_LSFCosTab_FIX_Q12[ f_int ]; /* Q12 */ + delta = silk_LSFCosTab_FIX_Q12[ f_int + 1 ] - cos_val; /* Q12, with a range of 0..200 */ + + /* Linear interpolation */ + cos_LSF_QA[ordering[k]] = silk_RSHIFT_ROUND( silk_LSHIFT( cos_val, 8 ) + silk_MUL( delta, f_frac ), 20 - QA ); /* QA */ + } + + dd = silk_RSHIFT( d, 1 ); + + /* generate even and odd polynomials using convolution */ + silk_NLSF2A_find_poly( P, &cos_LSF_QA[ 0 ], dd ); + silk_NLSF2A_find_poly( Q, &cos_LSF_QA[ 1 ], dd ); + + /* convert even and odd polynomials to opus_int32 Q12 filter coefs */ + for( k = 0; k < dd; k++ ) { + Ptmp = P[ k+1 ] + P[ k ]; + Qtmp = Q[ k+1 ] - Q[ k ]; + + /* the Ptmp and Qtmp values at this stage need to fit in int32 */ + a32_QA1[ k ] = -Qtmp - Ptmp; /* QA+1 */ + a32_QA1[ d-k-1 ] = Qtmp - Ptmp; /* QA+1 */ + } + + /* Convert int32 coefficients to Q12 int16 coefs */ + silk_LPC_fit( a_Q12, a32_QA1, 12, QA + 1, d ); + + for( i = 0; silk_LPC_inverse_pred_gain( a_Q12, d, arch ) == 0 && i < MAX_LPC_STABILIZE_ITERATIONS; i++ ) { + /* Prediction coefficients are (too close to) unstable; apply bandwidth expansion */ + /* on the unscaled coefficients, convert to Q12 and measure again */ + silk_bwexpander_32( a32_QA1, d, 65536 - silk_LSHIFT( 2, i ) ); + for( k = 0; k < d; k++ ) { + a_Q12[ k ] = (opus_int16)silk_RSHIFT_ROUND( a32_QA1[ k ], QA + 1 - 12 ); /* QA+1 -> Q12 */ + } + } +} + diff --git a/port/third_party/opus/silk/NLSF_VQ.c b/port/third_party/opus/silk/NLSF_VQ.c new file mode 100644 index 00000000..b83182a7 --- /dev/null +++ b/port/third_party/opus/silk/NLSF_VQ.c @@ -0,0 +1,76 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Compute quantization errors for an LPC_order element input vector for a VQ codebook */ +void silk_NLSF_VQ( + opus_int32 err_Q24[], /* O Quantization errors [K] */ + const opus_int16 in_Q15[], /* I Input vectors to be quantized [LPC_order] */ + const opus_uint8 pCB_Q8[], /* I Codebook vectors [K*LPC_order] */ + const opus_int16 pWght_Q9[], /* I Codebook weights [K*LPC_order] */ + const opus_int K, /* I Number of codebook vectors */ + const opus_int LPC_order /* I Number of LPCs */ +) +{ + opus_int i, m; + opus_int32 diff_Q15, diffw_Q24, sum_error_Q24, pred_Q24; + const opus_int16 *w_Q9_ptr; + const opus_uint8 *cb_Q8_ptr; + + celt_assert( ( LPC_order & 1 ) == 0 ); + + /* Loop over codebook */ + cb_Q8_ptr = pCB_Q8; + w_Q9_ptr = pWght_Q9; + for( i = 0; i < K; i++ ) { + sum_error_Q24 = 0; + pred_Q24 = 0; + for( m = LPC_order-2; m >= 0; m -= 2 ) { + /* Compute weighted absolute predictive quantization error for index m + 1 */ + diff_Q15 = silk_SUB_LSHIFT32( in_Q15[ m + 1 ], (opus_int32)cb_Q8_ptr[ m + 1 ], 7 ); /* range: [ -32767 : 32767 ]*/ + diffw_Q24 = silk_SMULBB( diff_Q15, w_Q9_ptr[ m + 1 ] ); + sum_error_Q24 = silk_ADD32( sum_error_Q24, silk_abs( silk_SUB_RSHIFT32( diffw_Q24, pred_Q24, 1 ) ) ); + pred_Q24 = diffw_Q24; + + /* Compute weighted absolute predictive quantization error for index m */ + diff_Q15 = silk_SUB_LSHIFT32( in_Q15[ m ], (opus_int32)cb_Q8_ptr[ m ], 7 ); /* range: [ -32767 : 32767 ]*/ + diffw_Q24 = silk_SMULBB( diff_Q15, w_Q9_ptr[ m ] ); + sum_error_Q24 = silk_ADD32( sum_error_Q24, silk_abs( silk_SUB_RSHIFT32( diffw_Q24, pred_Q24, 1 ) ) ); + pred_Q24 = diffw_Q24; + + silk_assert( sum_error_Q24 >= 0 ); + } + err_Q24[ i ] = sum_error_Q24; + cb_Q8_ptr += LPC_order; + w_Q9_ptr += LPC_order; + } +} diff --git a/port/third_party/opus/silk/NLSF_VQ_weights_laroia.c b/port/third_party/opus/silk/NLSF_VQ_weights_laroia.c new file mode 100644 index 00000000..9873bcde --- /dev/null +++ b/port/third_party/opus/silk/NLSF_VQ_weights_laroia.c @@ -0,0 +1,80 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "define.h" +#include "SigProc_FIX.h" + +/* +R. Laroia, N. Phamdo and N. Farvardin, "Robust and Efficient Quantization of Speech LSP +Parameters Using Structured Vector Quantization", Proc. IEEE Int. Conf. Acoust., Speech, +Signal Processing, pp. 641-644, 1991. +*/ + +/* Laroia low complexity NLSF weights */ +void silk_NLSF_VQ_weights_laroia( + opus_int16 *pNLSFW_Q_OUT, /* O Pointer to input vector weights [D] */ + const opus_int16 *pNLSF_Q15, /* I Pointer to input vector [D] */ + const opus_int D /* I Input vector dimension (even) */ +) +{ + opus_int k; + opus_int32 tmp1_int, tmp2_int; + + celt_assert( D > 0 ); + celt_assert( ( D & 1 ) == 0 ); + + /* First value */ + tmp1_int = silk_max_int( pNLSF_Q15[ 0 ], 1 ); + tmp1_int = silk_DIV32_16( (opus_int32)1 << ( 15 + NLSF_W_Q ), tmp1_int ); + tmp2_int = silk_max_int( pNLSF_Q15[ 1 ] - pNLSF_Q15[ 0 ], 1 ); + tmp2_int = silk_DIV32_16( (opus_int32)1 << ( 15 + NLSF_W_Q ), tmp2_int ); + pNLSFW_Q_OUT[ 0 ] = (opus_int16)silk_min_int( tmp1_int + tmp2_int, silk_int16_MAX ); + silk_assert( pNLSFW_Q_OUT[ 0 ] > 0 ); + + /* Main loop */ + for( k = 1; k < D - 1; k += 2 ) { + tmp1_int = silk_max_int( pNLSF_Q15[ k + 1 ] - pNLSF_Q15[ k ], 1 ); + tmp1_int = silk_DIV32_16( (opus_int32)1 << ( 15 + NLSF_W_Q ), tmp1_int ); + pNLSFW_Q_OUT[ k ] = (opus_int16)silk_min_int( tmp1_int + tmp2_int, silk_int16_MAX ); + silk_assert( pNLSFW_Q_OUT[ k ] > 0 ); + + tmp2_int = silk_max_int( pNLSF_Q15[ k + 2 ] - pNLSF_Q15[ k + 1 ], 1 ); + tmp2_int = silk_DIV32_16( (opus_int32)1 << ( 15 + NLSF_W_Q ), tmp2_int ); + pNLSFW_Q_OUT[ k + 1 ] = (opus_int16)silk_min_int( tmp1_int + tmp2_int, silk_int16_MAX ); + silk_assert( pNLSFW_Q_OUT[ k + 1 ] > 0 ); + } + + /* Last value */ + tmp1_int = silk_max_int( ( 1 << 15 ) - pNLSF_Q15[ D - 1 ], 1 ); + tmp1_int = silk_DIV32_16( (opus_int32)1 << ( 15 + NLSF_W_Q ), tmp1_int ); + pNLSFW_Q_OUT[ D - 1 ] = (opus_int16)silk_min_int( tmp1_int + tmp2_int, silk_int16_MAX ); + silk_assert( pNLSFW_Q_OUT[ D - 1 ] > 0 ); +} diff --git a/port/third_party/opus/silk/NLSF_decode.c b/port/third_party/opus/silk/NLSF_decode.c new file mode 100644 index 00000000..eeb0ba8c --- /dev/null +++ b/port/third_party/opus/silk/NLSF_decode.c @@ -0,0 +1,93 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Predictive dequantizer for NLSF residuals */ +static OPUS_INLINE void silk_NLSF_residual_dequant( /* O Returns RD value in Q30 */ + opus_int16 x_Q10[], /* O Output [ order ] */ + const opus_int8 indices[], /* I Quantization indices [ order ] */ + const opus_uint8 pred_coef_Q8[], /* I Backward predictor coefs [ order ] */ + const opus_int quant_step_size_Q16, /* I Quantization step size */ + const opus_int16 order /* I Number of input values */ +) +{ + opus_int i, out_Q10, pred_Q10; + + out_Q10 = 0; + for( i = order-1; i >= 0; i-- ) { + pred_Q10 = silk_RSHIFT( silk_SMULBB( out_Q10, (opus_int16)pred_coef_Q8[ i ] ), 8 ); + out_Q10 = silk_LSHIFT( indices[ i ], 10 ); + if( out_Q10 > 0 ) { + out_Q10 = silk_SUB16( out_Q10, SILK_FIX_CONST( NLSF_QUANT_LEVEL_ADJ, 10 ) ); + } else if( out_Q10 < 0 ) { + out_Q10 = silk_ADD16( out_Q10, SILK_FIX_CONST( NLSF_QUANT_LEVEL_ADJ, 10 ) ); + } + out_Q10 = silk_SMLAWB( pred_Q10, (opus_int32)out_Q10, quant_step_size_Q16 ); + x_Q10[ i ] = out_Q10; + } +} + + +/***********************/ +/* NLSF vector decoder */ +/***********************/ +void silk_NLSF_decode( + opus_int16 *pNLSF_Q15, /* O Quantized NLSF vector [ LPC_ORDER ] */ + opus_int8 *NLSFIndices, /* I Codebook path vector [ LPC_ORDER + 1 ] */ + const silk_NLSF_CB_struct *psNLSF_CB /* I Codebook object */ +) +{ + opus_int i; + opus_uint8 pred_Q8[ MAX_LPC_ORDER ]; + opus_int16 ec_ix[ MAX_LPC_ORDER ]; + opus_int16 res_Q10[ MAX_LPC_ORDER ]; + opus_int32 NLSF_Q15_tmp; + const opus_uint8 *pCB_element; + const opus_int16 *pCB_Wght_Q9; + + /* Unpack entropy table indices and predictor for current CB1 index */ + silk_NLSF_unpack( ec_ix, pred_Q8, psNLSF_CB, NLSFIndices[ 0 ] ); + + /* Predictive residual dequantizer */ + silk_NLSF_residual_dequant( res_Q10, &NLSFIndices[ 1 ], pred_Q8, psNLSF_CB->quantStepSize_Q16, psNLSF_CB->order ); + + /* Apply inverse square-rooted weights to first stage and add to output */ + pCB_element = &psNLSF_CB->CB1_NLSF_Q8[ NLSFIndices[ 0 ] * psNLSF_CB->order ]; + pCB_Wght_Q9 = &psNLSF_CB->CB1_Wght_Q9[ NLSFIndices[ 0 ] * psNLSF_CB->order ]; + for( i = 0; i < psNLSF_CB->order; i++ ) { + NLSF_Q15_tmp = silk_ADD_LSHIFT32( silk_DIV32_16( silk_LSHIFT( (opus_int32)res_Q10[ i ], 14 ), pCB_Wght_Q9[ i ] ), (opus_int16)pCB_element[ i ], 7 ); + pNLSF_Q15[ i ] = (opus_int16)silk_LIMIT( NLSF_Q15_tmp, 0, 32767 ); + } + + /* NLSF stabilization */ + silk_NLSF_stabilize( pNLSF_Q15, psNLSF_CB->deltaMin_Q15, psNLSF_CB->order ); +} diff --git a/port/third_party/opus/silk/NLSF_del_dec_quant.c b/port/third_party/opus/silk/NLSF_del_dec_quant.c new file mode 100644 index 00000000..44a16acd --- /dev/null +++ b/port/third_party/opus/silk/NLSF_del_dec_quant.c @@ -0,0 +1,215 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Delayed-decision quantizer for NLSF residuals */ +opus_int32 silk_NLSF_del_dec_quant( /* O Returns RD value in Q25 */ + opus_int8 indices[], /* O Quantization indices [ order ] */ + const opus_int16 x_Q10[], /* I Input [ order ] */ + const opus_int16 w_Q5[], /* I Weights [ order ] */ + const opus_uint8 pred_coef_Q8[], /* I Backward predictor coefs [ order ] */ + const opus_int16 ec_ix[], /* I Indices to entropy coding tables [ order ] */ + const opus_uint8 ec_rates_Q5[], /* I Rates [] */ + const opus_int quant_step_size_Q16, /* I Quantization step size */ + const opus_int16 inv_quant_step_size_Q6, /* I Inverse quantization step size */ + const opus_int32 mu_Q20, /* I R/D tradeoff */ + const opus_int16 order /* I Number of input values */ +) +{ + opus_int i, j, nStates, ind_tmp, ind_min_max, ind_max_min, in_Q10, res_Q10; + opus_int pred_Q10, diff_Q10, rate0_Q5, rate1_Q5; + opus_int16 out0_Q10, out1_Q10; + opus_int32 RD_tmp_Q25, min_Q25, min_max_Q25, max_min_Q25; + opus_int ind_sort[ NLSF_QUANT_DEL_DEC_STATES ]; + opus_int8 ind[ NLSF_QUANT_DEL_DEC_STATES ][ MAX_LPC_ORDER ]; + opus_int16 prev_out_Q10[ 2 * NLSF_QUANT_DEL_DEC_STATES ]; + opus_int32 RD_Q25[ 2 * NLSF_QUANT_DEL_DEC_STATES ]; + opus_int32 RD_min_Q25[ NLSF_QUANT_DEL_DEC_STATES ]; + opus_int32 RD_max_Q25[ NLSF_QUANT_DEL_DEC_STATES ]; + const opus_uint8 *rates_Q5; + + opus_int out0_Q10_table[2 * NLSF_QUANT_MAX_AMPLITUDE_EXT]; + opus_int out1_Q10_table[2 * NLSF_QUANT_MAX_AMPLITUDE_EXT]; + + for (i = -NLSF_QUANT_MAX_AMPLITUDE_EXT; i <= NLSF_QUANT_MAX_AMPLITUDE_EXT-1; i++) + { + out0_Q10 = silk_LSHIFT( i, 10 ); + out1_Q10 = silk_ADD16( out0_Q10, 1024 ); + if( i > 0 ) { + out0_Q10 = silk_SUB16( out0_Q10, SILK_FIX_CONST( NLSF_QUANT_LEVEL_ADJ, 10 ) ); + out1_Q10 = silk_SUB16( out1_Q10, SILK_FIX_CONST( NLSF_QUANT_LEVEL_ADJ, 10 ) ); + } else if( i == 0 ) { + out1_Q10 = silk_SUB16( out1_Q10, SILK_FIX_CONST( NLSF_QUANT_LEVEL_ADJ, 10 ) ); + } else if( i == -1 ) { + out0_Q10 = silk_ADD16( out0_Q10, SILK_FIX_CONST( NLSF_QUANT_LEVEL_ADJ, 10 ) ); + } else { + out0_Q10 = silk_ADD16( out0_Q10, SILK_FIX_CONST( NLSF_QUANT_LEVEL_ADJ, 10 ) ); + out1_Q10 = silk_ADD16( out1_Q10, SILK_FIX_CONST( NLSF_QUANT_LEVEL_ADJ, 10 ) ); + } + out0_Q10_table[ i + NLSF_QUANT_MAX_AMPLITUDE_EXT ] = silk_RSHIFT( silk_SMULBB( out0_Q10, quant_step_size_Q16 ), 16 ); + out1_Q10_table[ i + NLSF_QUANT_MAX_AMPLITUDE_EXT ] = silk_RSHIFT( silk_SMULBB( out1_Q10, quant_step_size_Q16 ), 16 ); + } + + silk_assert( (NLSF_QUANT_DEL_DEC_STATES & (NLSF_QUANT_DEL_DEC_STATES-1)) == 0 ); /* must be power of two */ + + nStates = 1; + RD_Q25[ 0 ] = 0; + prev_out_Q10[ 0 ] = 0; + for( i = order - 1; i >= 0; i-- ) { + rates_Q5 = &ec_rates_Q5[ ec_ix[ i ] ]; + in_Q10 = x_Q10[ i ]; + for( j = 0; j < nStates; j++ ) { + pred_Q10 = silk_RSHIFT( silk_SMULBB( (opus_int16)pred_coef_Q8[ i ], prev_out_Q10[ j ] ), 8 ); + res_Q10 = silk_SUB16( in_Q10, pred_Q10 ); + ind_tmp = silk_RSHIFT( silk_SMULBB( inv_quant_step_size_Q6, res_Q10 ), 16 ); + ind_tmp = silk_LIMIT( ind_tmp, -NLSF_QUANT_MAX_AMPLITUDE_EXT, NLSF_QUANT_MAX_AMPLITUDE_EXT-1 ); + ind[ j ][ i ] = (opus_int8)ind_tmp; + + /* compute outputs for ind_tmp and ind_tmp + 1 */ + out0_Q10 = out0_Q10_table[ ind_tmp + NLSF_QUANT_MAX_AMPLITUDE_EXT ]; + out1_Q10 = out1_Q10_table[ ind_tmp + NLSF_QUANT_MAX_AMPLITUDE_EXT ]; + + out0_Q10 = silk_ADD16( out0_Q10, pred_Q10 ); + out1_Q10 = silk_ADD16( out1_Q10, pred_Q10 ); + prev_out_Q10[ j ] = out0_Q10; + prev_out_Q10[ j + nStates ] = out1_Q10; + + /* compute RD for ind_tmp and ind_tmp + 1 */ + if( ind_tmp + 1 >= NLSF_QUANT_MAX_AMPLITUDE ) { + if( ind_tmp + 1 == NLSF_QUANT_MAX_AMPLITUDE ) { + rate0_Q5 = rates_Q5[ ind_tmp + NLSF_QUANT_MAX_AMPLITUDE ]; + rate1_Q5 = 280; + } else { + rate0_Q5 = silk_SMLABB( 280 - 43 * NLSF_QUANT_MAX_AMPLITUDE, 43, ind_tmp ); + rate1_Q5 = silk_ADD16( rate0_Q5, 43 ); + } + } else if( ind_tmp <= -NLSF_QUANT_MAX_AMPLITUDE ) { + if( ind_tmp == -NLSF_QUANT_MAX_AMPLITUDE ) { + rate0_Q5 = 280; + rate1_Q5 = rates_Q5[ ind_tmp + 1 + NLSF_QUANT_MAX_AMPLITUDE ]; + } else { + rate0_Q5 = silk_SMLABB( 280 - 43 * NLSF_QUANT_MAX_AMPLITUDE, -43, ind_tmp ); + rate1_Q5 = silk_SUB16( rate0_Q5, 43 ); + } + } else { + rate0_Q5 = rates_Q5[ ind_tmp + NLSF_QUANT_MAX_AMPLITUDE ]; + rate1_Q5 = rates_Q5[ ind_tmp + 1 + NLSF_QUANT_MAX_AMPLITUDE ]; + } + RD_tmp_Q25 = RD_Q25[ j ]; + diff_Q10 = silk_SUB16( in_Q10, out0_Q10 ); + RD_Q25[ j ] = silk_SMLABB( silk_MLA( RD_tmp_Q25, silk_SMULBB( diff_Q10, diff_Q10 ), w_Q5[ i ] ), mu_Q20, rate0_Q5 ); + diff_Q10 = silk_SUB16( in_Q10, out1_Q10 ); + RD_Q25[ j + nStates ] = silk_SMLABB( silk_MLA( RD_tmp_Q25, silk_SMULBB( diff_Q10, diff_Q10 ), w_Q5[ i ] ), mu_Q20, rate1_Q5 ); + } + + if( nStates <= NLSF_QUANT_DEL_DEC_STATES/2 ) { + /* double number of states and copy */ + for( j = 0; j < nStates; j++ ) { + ind[ j + nStates ][ i ] = ind[ j ][ i ] + 1; + } + nStates = silk_LSHIFT( nStates, 1 ); + for( j = nStates; j < NLSF_QUANT_DEL_DEC_STATES; j++ ) { + ind[ j ][ i ] = ind[ j - nStates ][ i ]; + } + } else { + /* sort lower and upper half of RD_Q25, pairwise */ + for( j = 0; j < NLSF_QUANT_DEL_DEC_STATES; j++ ) { + if( RD_Q25[ j ] > RD_Q25[ j + NLSF_QUANT_DEL_DEC_STATES ] ) { + RD_max_Q25[ j ] = RD_Q25[ j ]; + RD_min_Q25[ j ] = RD_Q25[ j + NLSF_QUANT_DEL_DEC_STATES ]; + RD_Q25[ j ] = RD_min_Q25[ j ]; + RD_Q25[ j + NLSF_QUANT_DEL_DEC_STATES ] = RD_max_Q25[ j ]; + /* swap prev_out values */ + out0_Q10 = prev_out_Q10[ j ]; + prev_out_Q10[ j ] = prev_out_Q10[ j + NLSF_QUANT_DEL_DEC_STATES ]; + prev_out_Q10[ j + NLSF_QUANT_DEL_DEC_STATES ] = out0_Q10; + ind_sort[ j ] = j + NLSF_QUANT_DEL_DEC_STATES; + } else { + RD_min_Q25[ j ] = RD_Q25[ j ]; + RD_max_Q25[ j ] = RD_Q25[ j + NLSF_QUANT_DEL_DEC_STATES ]; + ind_sort[ j ] = j; + } + } + /* compare the highest RD values of the winning half with the lowest one in the losing half, and copy if necessary */ + /* afterwards ind_sort[] will contain the indices of the NLSF_QUANT_DEL_DEC_STATES winning RD values */ + while( 1 ) { + min_max_Q25 = silk_int32_MAX; + max_min_Q25 = 0; + ind_min_max = 0; + ind_max_min = 0; + for( j = 0; j < NLSF_QUANT_DEL_DEC_STATES; j++ ) { + if( min_max_Q25 > RD_max_Q25[ j ] ) { + min_max_Q25 = RD_max_Q25[ j ]; + ind_min_max = j; + } + if( max_min_Q25 < RD_min_Q25[ j ] ) { + max_min_Q25 = RD_min_Q25[ j ]; + ind_max_min = j; + } + } + if( min_max_Q25 >= max_min_Q25 ) { + break; + } + /* copy ind_min_max to ind_max_min */ + ind_sort[ ind_max_min ] = ind_sort[ ind_min_max ] ^ NLSF_QUANT_DEL_DEC_STATES; + RD_Q25[ ind_max_min ] = RD_Q25[ ind_min_max + NLSF_QUANT_DEL_DEC_STATES ]; + prev_out_Q10[ ind_max_min ] = prev_out_Q10[ ind_min_max + NLSF_QUANT_DEL_DEC_STATES ]; + RD_min_Q25[ ind_max_min ] = 0; + RD_max_Q25[ ind_min_max ] = silk_int32_MAX; + silk_memcpy( ind[ ind_max_min ], ind[ ind_min_max ], MAX_LPC_ORDER * sizeof( opus_int8 ) ); + } + /* increment index if it comes from the upper half */ + for( j = 0; j < NLSF_QUANT_DEL_DEC_STATES; j++ ) { + ind[ j ][ i ] += silk_RSHIFT( ind_sort[ j ], NLSF_QUANT_DEL_DEC_STATES_LOG2 ); + } + } + } + + /* last sample: find winner, copy indices and return RD value */ + ind_tmp = 0; + min_Q25 = silk_int32_MAX; + for( j = 0; j < 2 * NLSF_QUANT_DEL_DEC_STATES; j++ ) { + if( min_Q25 > RD_Q25[ j ] ) { + min_Q25 = RD_Q25[ j ]; + ind_tmp = j; + } + } + for( j = 0; j < order; j++ ) { + indices[ j ] = ind[ ind_tmp & ( NLSF_QUANT_DEL_DEC_STATES - 1 ) ][ j ]; + silk_assert( indices[ j ] >= -NLSF_QUANT_MAX_AMPLITUDE_EXT ); + silk_assert( indices[ j ] <= NLSF_QUANT_MAX_AMPLITUDE_EXT ); + } + indices[ 0 ] += silk_RSHIFT( ind_tmp, NLSF_QUANT_DEL_DEC_STATES_LOG2 ); + silk_assert( indices[ 0 ] <= NLSF_QUANT_MAX_AMPLITUDE_EXT ); + silk_assert( min_Q25 >= 0 ); + return min_Q25; +} diff --git a/port/third_party/opus/silk/NLSF_encode.c b/port/third_party/opus/silk/NLSF_encode.c new file mode 100644 index 00000000..01ac7db7 --- /dev/null +++ b/port/third_party/opus/silk/NLSF_encode.c @@ -0,0 +1,124 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" + +/***********************/ +/* NLSF vector encoder */ +/***********************/ +opus_int32 silk_NLSF_encode( /* O Returns RD value in Q25 */ + opus_int8 *NLSFIndices, /* I Codebook path vector [ LPC_ORDER + 1 ] */ + opus_int16 *pNLSF_Q15, /* I/O (Un)quantized NLSF vector [ LPC_ORDER ] */ + const silk_NLSF_CB_struct *psNLSF_CB, /* I Codebook object */ + const opus_int16 *pW_Q2, /* I NLSF weight vector [ LPC_ORDER ] */ + const opus_int NLSF_mu_Q20, /* I Rate weight for the RD optimization */ + const opus_int nSurvivors, /* I Max survivors after first stage */ + const opus_int signalType /* I Signal type: 0/1/2 */ +) +{ + opus_int i, s, ind1, bestIndex, prob_Q8, bits_q7; + opus_int32 W_tmp_Q9, ret; + VARDECL( opus_int32, err_Q24 ); + VARDECL( opus_int32, RD_Q25 ); + VARDECL( opus_int, tempIndices1 ); + VARDECL( opus_int8, tempIndices2 ); + opus_int16 res_Q10[ MAX_LPC_ORDER ]; + opus_int16 NLSF_tmp_Q15[ MAX_LPC_ORDER ]; + opus_int16 W_adj_Q5[ MAX_LPC_ORDER ]; + opus_uint8 pred_Q8[ MAX_LPC_ORDER ]; + opus_int16 ec_ix[ MAX_LPC_ORDER ]; + const opus_uint8 *pCB_element, *iCDF_ptr; + const opus_int16 *pCB_Wght_Q9; + SAVE_STACK; + + celt_assert( signalType >= 0 && signalType <= 2 ); + silk_assert( NLSF_mu_Q20 <= 32767 && NLSF_mu_Q20 >= 0 ); + + /* NLSF stabilization */ + silk_NLSF_stabilize( pNLSF_Q15, psNLSF_CB->deltaMin_Q15, psNLSF_CB->order ); + + /* First stage: VQ */ + ALLOC( err_Q24, psNLSF_CB->nVectors, opus_int32 ); + silk_NLSF_VQ( err_Q24, pNLSF_Q15, psNLSF_CB->CB1_NLSF_Q8, psNLSF_CB->CB1_Wght_Q9, psNLSF_CB->nVectors, psNLSF_CB->order ); + + /* Sort the quantization errors */ + ALLOC( tempIndices1, nSurvivors, opus_int ); + silk_insertion_sort_increasing( err_Q24, tempIndices1, psNLSF_CB->nVectors, nSurvivors ); + + ALLOC( RD_Q25, nSurvivors, opus_int32 ); + ALLOC( tempIndices2, nSurvivors * MAX_LPC_ORDER, opus_int8 ); + + /* Loop over survivors */ + for( s = 0; s < nSurvivors; s++ ) { + ind1 = tempIndices1[ s ]; + + /* Residual after first stage */ + pCB_element = &psNLSF_CB->CB1_NLSF_Q8[ ind1 * psNLSF_CB->order ]; + pCB_Wght_Q9 = &psNLSF_CB->CB1_Wght_Q9[ ind1 * psNLSF_CB->order ]; + for( i = 0; i < psNLSF_CB->order; i++ ) { + NLSF_tmp_Q15[ i ] = silk_LSHIFT16( (opus_int16)pCB_element[ i ], 7 ); + W_tmp_Q9 = pCB_Wght_Q9[ i ]; + res_Q10[ i ] = (opus_int16)silk_RSHIFT( silk_SMULBB( pNLSF_Q15[ i ] - NLSF_tmp_Q15[ i ], W_tmp_Q9 ), 14 ); + W_adj_Q5[ i ] = silk_DIV32_varQ( (opus_int32)pW_Q2[ i ], silk_SMULBB( W_tmp_Q9, W_tmp_Q9 ), 21 ); + } + + /* Unpack entropy table indices and predictor for current CB1 index */ + silk_NLSF_unpack( ec_ix, pred_Q8, psNLSF_CB, ind1 ); + + /* Trellis quantizer */ + RD_Q25[ s ] = silk_NLSF_del_dec_quant( &tempIndices2[ s * MAX_LPC_ORDER ], res_Q10, W_adj_Q5, pred_Q8, ec_ix, + psNLSF_CB->ec_Rates_Q5, psNLSF_CB->quantStepSize_Q16, psNLSF_CB->invQuantStepSize_Q6, NLSF_mu_Q20, psNLSF_CB->order ); + + /* Add rate for first stage */ + iCDF_ptr = &psNLSF_CB->CB1_iCDF[ ( signalType >> 1 ) * psNLSF_CB->nVectors ]; + if( ind1 == 0 ) { + prob_Q8 = 256 - iCDF_ptr[ ind1 ]; + } else { + prob_Q8 = iCDF_ptr[ ind1 - 1 ] - iCDF_ptr[ ind1 ]; + } + bits_q7 = ( 8 << 7 ) - silk_lin2log( prob_Q8 ); + RD_Q25[ s ] = silk_SMLABB( RD_Q25[ s ], bits_q7, silk_RSHIFT( NLSF_mu_Q20, 2 ) ); + } + + /* Find the lowest rate-distortion error */ + silk_insertion_sort_increasing( RD_Q25, &bestIndex, nSurvivors, 1 ); + + NLSFIndices[ 0 ] = (opus_int8)tempIndices1[ bestIndex ]; + silk_memcpy( &NLSFIndices[ 1 ], &tempIndices2[ bestIndex * MAX_LPC_ORDER ], psNLSF_CB->order * sizeof( opus_int8 ) ); + + /* Decode */ + silk_NLSF_decode( pNLSF_Q15, NLSFIndices, psNLSF_CB ); + + ret = RD_Q25[ 0 ]; + RESTORE_STACK; + return ret; +} diff --git a/port/third_party/opus/silk/NLSF_stabilize.c b/port/third_party/opus/silk/NLSF_stabilize.c new file mode 100644 index 00000000..8f3426b9 --- /dev/null +++ b/port/third_party/opus/silk/NLSF_stabilize.c @@ -0,0 +1,142 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/* NLSF stabilizer: */ +/* */ +/* - Moves NLSFs further apart if they are too close */ +/* - Moves NLSFs away from borders if they are too close */ +/* - High effort to achieve a modification with minimum */ +/* Euclidean distance to input vector */ +/* - Output are sorted NLSF coefficients */ +/* */ + +#include "SigProc_FIX.h" + +/* Constant Definitions */ +#define MAX_LOOPS 20 + +/* NLSF stabilizer, for a single input data vector */ +void silk_NLSF_stabilize( + opus_int16 *NLSF_Q15, /* I/O Unstable/stabilized normalized LSF vector in Q15 [L] */ + const opus_int16 *NDeltaMin_Q15, /* I Min distance vector, NDeltaMin_Q15[L] must be >= 1 [L+1] */ + const opus_int L /* I Number of NLSF parameters in the input vector */ +) +{ + opus_int i, I=0, k, loops; + opus_int16 center_freq_Q15; + opus_int32 diff_Q15, min_diff_Q15, min_center_Q15, max_center_Q15; + + /* This is necessary to ensure an output within range of a opus_int16 */ + silk_assert( NDeltaMin_Q15[L] >= 1 ); + + for( loops = 0; loops < MAX_LOOPS; loops++ ) { + /**************************/ + /* Find smallest distance */ + /**************************/ + /* First element */ + min_diff_Q15 = NLSF_Q15[0] - NDeltaMin_Q15[0]; + I = 0; + /* Middle elements */ + for( i = 1; i <= L-1; i++ ) { + diff_Q15 = NLSF_Q15[i] - ( NLSF_Q15[i-1] + NDeltaMin_Q15[i] ); + if( diff_Q15 < min_diff_Q15 ) { + min_diff_Q15 = diff_Q15; + I = i; + } + } + /* Last element */ + diff_Q15 = ( 1 << 15 ) - ( NLSF_Q15[L-1] + NDeltaMin_Q15[L] ); + if( diff_Q15 < min_diff_Q15 ) { + min_diff_Q15 = diff_Q15; + I = L; + } + + /***************************************************/ + /* Now check if the smallest distance non-negative */ + /***************************************************/ + if( min_diff_Q15 >= 0 ) { + return; + } + + if( I == 0 ) { + /* Move away from lower limit */ + NLSF_Q15[0] = NDeltaMin_Q15[0]; + + } else if( I == L) { + /* Move away from higher limit */ + NLSF_Q15[L-1] = ( 1 << 15 ) - NDeltaMin_Q15[L]; + + } else { + /* Find the lower extreme for the location of the current center frequency */ + min_center_Q15 = 0; + for( k = 0; k < I; k++ ) { + min_center_Q15 += NDeltaMin_Q15[k]; + } + min_center_Q15 += silk_RSHIFT( NDeltaMin_Q15[I], 1 ); + + /* Find the upper extreme for the location of the current center frequency */ + max_center_Q15 = 1 << 15; + for( k = L; k > I; k-- ) { + max_center_Q15 -= NDeltaMin_Q15[k]; + } + max_center_Q15 -= silk_RSHIFT( NDeltaMin_Q15[I], 1 ); + + /* Move apart, sorted by value, keeping the same center frequency */ + center_freq_Q15 = (opus_int16)silk_LIMIT_32( silk_RSHIFT_ROUND( (opus_int32)NLSF_Q15[I-1] + (opus_int32)NLSF_Q15[I], 1 ), + min_center_Q15, max_center_Q15 ); + NLSF_Q15[I-1] = center_freq_Q15 - silk_RSHIFT( NDeltaMin_Q15[I], 1 ); + NLSF_Q15[I] = NLSF_Q15[I-1] + NDeltaMin_Q15[I]; + } + } + + /* Safe and simple fall back method, which is less ideal than the above */ + if( loops == MAX_LOOPS ) + { + /* Insertion sort (fast for already almost sorted arrays): */ + /* Best case: O(n) for an already sorted array */ + /* Worst case: O(n^2) for an inversely sorted array */ + silk_insertion_sort_increasing_all_values_int16( &NLSF_Q15[0], L ); + + /* First NLSF should be no less than NDeltaMin[0] */ + NLSF_Q15[0] = silk_max_int( NLSF_Q15[0], NDeltaMin_Q15[0] ); + + /* Keep delta_min distance between the NLSFs */ + for( i = 1; i < L; i++ ) + NLSF_Q15[i] = silk_max_int( NLSF_Q15[i], silk_ADD_SAT16( NLSF_Q15[i-1], NDeltaMin_Q15[i] ) ); + + /* Last NLSF should be no higher than 1 - NDeltaMin[L] */ + NLSF_Q15[L-1] = silk_min_int( NLSF_Q15[L-1], (1<<15) - NDeltaMin_Q15[L] ); + + /* Keep NDeltaMin distance between the NLSFs */ + for( i = L-2; i >= 0; i-- ) + NLSF_Q15[i] = silk_min_int( NLSF_Q15[i], NLSF_Q15[i+1] - NDeltaMin_Q15[i+1] ); + } +} diff --git a/port/third_party/opus/silk/NLSF_unpack.c b/port/third_party/opus/silk/NLSF_unpack.c new file mode 100644 index 00000000..17bd23f7 --- /dev/null +++ b/port/third_party/opus/silk/NLSF_unpack.c @@ -0,0 +1,55 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Unpack predictor values and indices for entropy coding tables */ +void silk_NLSF_unpack( + opus_int16 ec_ix[], /* O Indices to entropy tables [ LPC_ORDER ] */ + opus_uint8 pred_Q8[], /* O LSF predictor [ LPC_ORDER ] */ + const silk_NLSF_CB_struct *psNLSF_CB, /* I Codebook object */ + const opus_int CB1_index /* I Index of vector in first LSF codebook */ +) +{ + opus_int i; + opus_uint8 entry; + const opus_uint8 *ec_sel_ptr; + + ec_sel_ptr = &psNLSF_CB->ec_sel[ CB1_index * psNLSF_CB->order / 2 ]; + for( i = 0; i < psNLSF_CB->order; i += 2 ) { + entry = *ec_sel_ptr++; + ec_ix [ i ] = silk_SMULBB( silk_RSHIFT( entry, 1 ) & 7, 2 * NLSF_QUANT_MAX_AMPLITUDE + 1 ); + pred_Q8[ i ] = psNLSF_CB->pred_Q8[ i + ( entry & 1 ) * ( psNLSF_CB->order - 1 ) ]; + ec_ix [ i + 1 ] = silk_SMULBB( silk_RSHIFT( entry, 5 ) & 7, 2 * NLSF_QUANT_MAX_AMPLITUDE + 1 ); + pred_Q8[ i + 1 ] = psNLSF_CB->pred_Q8[ i + ( silk_RSHIFT( entry, 4 ) & 1 ) * ( psNLSF_CB->order - 1 ) + 1 ]; + } +} + diff --git a/port/third_party/opus/silk/NSQ.c b/port/third_party/opus/silk/NSQ.c new file mode 100644 index 00000000..1caa829b --- /dev/null +++ b/port/third_party/opus/silk/NSQ.c @@ -0,0 +1,437 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" +#include "NSQ.h" + + +static OPUS_INLINE void silk_nsq_scale_states( + const silk_encoder_state *psEncC, /* I Encoder State */ + silk_nsq_state *NSQ, /* I/O NSQ state */ + const opus_int16 x16[], /* I input */ + opus_int32 x_sc_Q10[], /* O input scaled with 1/Gain */ + const opus_int16 sLTP[], /* I re-whitened LTP state in Q0 */ + opus_int32 sLTP_Q15[], /* O LTP state matching scaled input */ + opus_int subfr, /* I subframe number */ + const opus_int LTP_scale_Q14, /* I */ + const opus_int32 Gains_Q16[ MAX_NB_SUBFR ], /* I */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lag */ + const opus_int signal_type /* I Signal type */ +); + +#if !defined(OPUS_X86_MAY_HAVE_SSE4_1) +static OPUS_INLINE void silk_noise_shape_quantizer( + silk_nsq_state *NSQ, /* I/O NSQ state */ + opus_int signalType, /* I Signal type */ + const opus_int32 x_sc_Q10[], /* I */ + opus_int8 pulses[], /* O */ + opus_int16 xq[], /* O */ + opus_int32 sLTP_Q15[], /* I/O LTP state */ + const opus_int16 a_Q12[], /* I Short term prediction coefs */ + const opus_int16 b_Q14[], /* I Long term prediction coefs */ + const opus_int16 AR_shp_Q13[], /* I Noise shaping AR coefs */ + opus_int lag, /* I Pitch lag */ + opus_int32 HarmShapeFIRPacked_Q14, /* I */ + opus_int Tilt_Q14, /* I Spectral tilt */ + opus_int32 LF_shp_Q14, /* I */ + opus_int32 Gain_Q16, /* I */ + opus_int Lambda_Q10, /* I */ + opus_int offset_Q10, /* I */ + opus_int length, /* I Input length */ + opus_int shapingLPCOrder, /* I Noise shaping AR filter order */ + opus_int predictLPCOrder, /* I Prediction filter order */ + int arch /* I Architecture */ +); +#endif + +void silk_NSQ_c +( + const silk_encoder_state *psEncC, /* I Encoder State */ + silk_nsq_state *NSQ, /* I/O NSQ state */ + SideInfoIndices *psIndices, /* I/O Quantization Indices */ + const opus_int16 x16[], /* I Input */ + opus_int8 pulses[], /* O Quantized pulse signal */ + const opus_int16 *PredCoef_Q12, /* I Short term prediction coefs */ + const opus_int16 LTPCoef_Q14[ LTP_ORDER * MAX_NB_SUBFR ], /* I Long term prediction coefs */ + const opus_int16 AR_Q13[ MAX_NB_SUBFR * MAX_SHAPE_LPC_ORDER ], /* I Noise shaping coefs */ + const opus_int HarmShapeGain_Q14[ MAX_NB_SUBFR ], /* I Long term shaping coefs */ + const opus_int Tilt_Q14[ MAX_NB_SUBFR ], /* I Spectral tilt */ + const opus_int32 LF_shp_Q14[ MAX_NB_SUBFR ], /* I Low frequency shaping coefs */ + const opus_int32 Gains_Q16[ MAX_NB_SUBFR ], /* I Quantization step sizes */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lags */ + const opus_int Lambda_Q10, /* I Rate/distortion tradeoff */ + const opus_int LTP_scale_Q14 /* I LTP state scaling */ +) +{ + opus_int k, lag, start_idx, LSF_interpolation_flag; + const opus_int16 *A_Q12, *B_Q14, *AR_shp_Q13; + opus_int16 *pxq; + VARDECL( opus_int32, sLTP_Q15 ); + VARDECL( opus_int16, sLTP ); + opus_int32 HarmShapeFIRPacked_Q14; + opus_int offset_Q10; + VARDECL( opus_int32, x_sc_Q10 ); + SAVE_STACK; + + NSQ->rand_seed = psIndices->Seed; + + /* Set unvoiced lag to the previous one, overwrite later for voiced */ + lag = NSQ->lagPrev; + + silk_assert( NSQ->prev_gain_Q16 != 0 ); + + offset_Q10 = silk_Quantization_Offsets_Q10[ psIndices->signalType >> 1 ][ psIndices->quantOffsetType ]; + + if( psIndices->NLSFInterpCoef_Q2 == 4 ) { + LSF_interpolation_flag = 0; + } else { + LSF_interpolation_flag = 1; + } + + ALLOC( sLTP_Q15, psEncC->ltp_mem_length + psEncC->frame_length, opus_int32 ); + ALLOC( sLTP, psEncC->ltp_mem_length + psEncC->frame_length, opus_int16 ); + ALLOC( x_sc_Q10, psEncC->subfr_length, opus_int32 ); + /* Set up pointers to start of sub frame */ + NSQ->sLTP_shp_buf_idx = psEncC->ltp_mem_length; + NSQ->sLTP_buf_idx = psEncC->ltp_mem_length; + pxq = &NSQ->xq[ psEncC->ltp_mem_length ]; + for( k = 0; k < psEncC->nb_subfr; k++ ) { + A_Q12 = &PredCoef_Q12[ (( k >> 1 ) | ( 1 - LSF_interpolation_flag )) * MAX_LPC_ORDER ]; + B_Q14 = <PCoef_Q14[ k * LTP_ORDER ]; + AR_shp_Q13 = &AR_Q13[ k * MAX_SHAPE_LPC_ORDER ]; + + /* Noise shape parameters */ + silk_assert( HarmShapeGain_Q14[ k ] >= 0 ); + HarmShapeFIRPacked_Q14 = silk_RSHIFT( HarmShapeGain_Q14[ k ], 2 ); + HarmShapeFIRPacked_Q14 |= silk_LSHIFT( (opus_int32)silk_RSHIFT( HarmShapeGain_Q14[ k ], 1 ), 16 ); + + NSQ->rewhite_flag = 0; + if( psIndices->signalType == TYPE_VOICED ) { + /* Voiced */ + lag = pitchL[ k ]; + + /* Re-whitening */ + if( ( k & ( 3 - silk_LSHIFT( LSF_interpolation_flag, 1 ) ) ) == 0 ) { + /* Rewhiten with new A coefs */ + start_idx = psEncC->ltp_mem_length - lag - psEncC->predictLPCOrder - LTP_ORDER / 2; + celt_assert( start_idx > 0 ); + + silk_LPC_analysis_filter( &sLTP[ start_idx ], &NSQ->xq[ start_idx + k * psEncC->subfr_length ], + A_Q12, psEncC->ltp_mem_length - start_idx, psEncC->predictLPCOrder, psEncC->arch ); + + NSQ->rewhite_flag = 1; + NSQ->sLTP_buf_idx = psEncC->ltp_mem_length; + } + } + + silk_nsq_scale_states( psEncC, NSQ, x16, x_sc_Q10, sLTP, sLTP_Q15, k, LTP_scale_Q14, Gains_Q16, pitchL, psIndices->signalType ); + + silk_noise_shape_quantizer( NSQ, psIndices->signalType, x_sc_Q10, pulses, pxq, sLTP_Q15, A_Q12, B_Q14, + AR_shp_Q13, lag, HarmShapeFIRPacked_Q14, Tilt_Q14[ k ], LF_shp_Q14[ k ], Gains_Q16[ k ], Lambda_Q10, + offset_Q10, psEncC->subfr_length, psEncC->shapingLPCOrder, psEncC->predictLPCOrder, psEncC->arch ); + + x16 += psEncC->subfr_length; + pulses += psEncC->subfr_length; + pxq += psEncC->subfr_length; + } + + /* Update lagPrev for next frame */ + NSQ->lagPrev = pitchL[ psEncC->nb_subfr - 1 ]; + + /* Save quantized speech and noise shaping signals */ + silk_memmove( NSQ->xq, &NSQ->xq[ psEncC->frame_length ], psEncC->ltp_mem_length * sizeof( opus_int16 ) ); + silk_memmove( NSQ->sLTP_shp_Q14, &NSQ->sLTP_shp_Q14[ psEncC->frame_length ], psEncC->ltp_mem_length * sizeof( opus_int32 ) ); + RESTORE_STACK; +} + +/******************************/ +/* silk_noise_shape_quantizer */ +/******************************/ + +#if !defined(OPUS_X86_MAY_HAVE_SSE4_1) +static OPUS_INLINE +#endif +void silk_noise_shape_quantizer( + silk_nsq_state *NSQ, /* I/O NSQ state */ + opus_int signalType, /* I Signal type */ + const opus_int32 x_sc_Q10[], /* I */ + opus_int8 pulses[], /* O */ + opus_int16 xq[], /* O */ + opus_int32 sLTP_Q15[], /* I/O LTP state */ + const opus_int16 a_Q12[], /* I Short term prediction coefs */ + const opus_int16 b_Q14[], /* I Long term prediction coefs */ + const opus_int16 AR_shp_Q13[], /* I Noise shaping AR coefs */ + opus_int lag, /* I Pitch lag */ + opus_int32 HarmShapeFIRPacked_Q14, /* I */ + opus_int Tilt_Q14, /* I Spectral tilt */ + opus_int32 LF_shp_Q14, /* I */ + opus_int32 Gain_Q16, /* I */ + opus_int Lambda_Q10, /* I */ + opus_int offset_Q10, /* I */ + opus_int length, /* I Input length */ + opus_int shapingLPCOrder, /* I Noise shaping AR filter order */ + opus_int predictLPCOrder, /* I Prediction filter order */ + int arch /* I Architecture */ +) +{ + opus_int i; + opus_int32 LTP_pred_Q13, LPC_pred_Q10, n_AR_Q12, n_LTP_Q13; + opus_int32 n_LF_Q12, r_Q10, rr_Q10, q1_Q0, q1_Q10, q2_Q10, rd1_Q20, rd2_Q20; + opus_int32 exc_Q14, LPC_exc_Q14, xq_Q14, Gain_Q10; + opus_int32 tmp1, tmp2, sLF_AR_shp_Q14; + opus_int32 *psLPC_Q14, *shp_lag_ptr, *pred_lag_ptr; +#ifdef silk_short_prediction_create_arch_coef + opus_int32 a_Q12_arch[MAX_LPC_ORDER]; +#endif + + shp_lag_ptr = &NSQ->sLTP_shp_Q14[ NSQ->sLTP_shp_buf_idx - lag + HARM_SHAPE_FIR_TAPS / 2 ]; + pred_lag_ptr = &sLTP_Q15[ NSQ->sLTP_buf_idx - lag + LTP_ORDER / 2 ]; + Gain_Q10 = silk_RSHIFT( Gain_Q16, 6 ); + + /* Set up short term AR state */ + psLPC_Q14 = &NSQ->sLPC_Q14[ NSQ_LPC_BUF_LENGTH - 1 ]; + +#ifdef silk_short_prediction_create_arch_coef + silk_short_prediction_create_arch_coef(a_Q12_arch, a_Q12, predictLPCOrder); +#endif + + for( i = 0; i < length; i++ ) { + /* Generate dither */ + NSQ->rand_seed = silk_RAND( NSQ->rand_seed ); + + /* Short-term prediction */ + LPC_pred_Q10 = silk_noise_shape_quantizer_short_prediction(psLPC_Q14, a_Q12, a_Q12_arch, predictLPCOrder, arch); + + /* Long-term prediction */ + if( signalType == TYPE_VOICED ) { + /* Unrolled loop */ + /* Avoids introducing a bias because silk_SMLAWB() always rounds to -inf */ + LTP_pred_Q13 = 2; + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ 0 ], b_Q14[ 0 ] ); + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ -1 ], b_Q14[ 1 ] ); + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ -2 ], b_Q14[ 2 ] ); + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ -3 ], b_Q14[ 3 ] ); + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ -4 ], b_Q14[ 4 ] ); + pred_lag_ptr++; + } else { + LTP_pred_Q13 = 0; + } + + /* Noise shape feedback */ + celt_assert( ( shapingLPCOrder & 1 ) == 0 ); /* check that order is even */ + n_AR_Q12 = silk_NSQ_noise_shape_feedback_loop(&NSQ->sDiff_shp_Q14, NSQ->sAR2_Q14, AR_shp_Q13, shapingLPCOrder, arch); + + n_AR_Q12 = silk_SMLAWB( n_AR_Q12, NSQ->sLF_AR_shp_Q14, Tilt_Q14 ); + + n_LF_Q12 = silk_SMULWB( NSQ->sLTP_shp_Q14[ NSQ->sLTP_shp_buf_idx - 1 ], LF_shp_Q14 ); + n_LF_Q12 = silk_SMLAWT( n_LF_Q12, NSQ->sLF_AR_shp_Q14, LF_shp_Q14 ); + + celt_assert( lag > 0 || signalType != TYPE_VOICED ); + + /* Combine prediction and noise shaping signals */ + tmp1 = silk_SUB32_ovflw( silk_LSHIFT32( LPC_pred_Q10, 2 ), n_AR_Q12 ); /* Q12 */ + tmp1 = silk_SUB32_ovflw( tmp1, n_LF_Q12 ); /* Q12 */ + if( lag > 0 ) { + /* Symmetric, packed FIR coefficients */ + n_LTP_Q13 = silk_SMULWB( silk_ADD_SAT32( shp_lag_ptr[ 0 ], shp_lag_ptr[ -2 ] ), HarmShapeFIRPacked_Q14 ); + n_LTP_Q13 = silk_SMLAWT( n_LTP_Q13, shp_lag_ptr[ -1 ], HarmShapeFIRPacked_Q14 ); + n_LTP_Q13 = silk_LSHIFT( n_LTP_Q13, 1 ); + shp_lag_ptr++; + + tmp2 = silk_SUB32( LTP_pred_Q13, n_LTP_Q13 ); /* Q13 */ + tmp1 = silk_ADD32_ovflw( tmp2, silk_LSHIFT32( tmp1, 1 ) ); /* Q13 */ + tmp1 = silk_RSHIFT_ROUND( tmp1, 3 ); /* Q10 */ + } else { + tmp1 = silk_RSHIFT_ROUND( tmp1, 2 ); /* Q10 */ + } + + r_Q10 = silk_SUB32( x_sc_Q10[ i ], tmp1 ); /* residual error Q10 */ + + /* Flip sign depending on dither */ + if( NSQ->rand_seed < 0 ) { + r_Q10 = -r_Q10; + } + r_Q10 = silk_LIMIT_32( r_Q10, -(31 << 10), 30 << 10 ); + + /* Find two quantization level candidates and measure their rate-distortion */ + q1_Q10 = silk_SUB32( r_Q10, offset_Q10 ); + q1_Q0 = silk_RSHIFT( q1_Q10, 10 ); + if (Lambda_Q10 > 2048) { + /* For aggressive RDO, the bias becomes more than one pulse. */ + int rdo_offset = Lambda_Q10/2 - 512; + if (q1_Q10 > rdo_offset) { + q1_Q0 = silk_RSHIFT( q1_Q10 - rdo_offset, 10 ); + } else if (q1_Q10 < -rdo_offset) { + q1_Q0 = silk_RSHIFT( q1_Q10 + rdo_offset, 10 ); + } else if (q1_Q10 < 0) { + q1_Q0 = -1; + } else { + q1_Q0 = 0; + } + } + if( q1_Q0 > 0 ) { + q1_Q10 = silk_SUB32( silk_LSHIFT( q1_Q0, 10 ), QUANT_LEVEL_ADJUST_Q10 ); + q1_Q10 = silk_ADD32( q1_Q10, offset_Q10 ); + q2_Q10 = silk_ADD32( q1_Q10, 1024 ); + rd1_Q20 = silk_SMULBB( q1_Q10, Lambda_Q10 ); + rd2_Q20 = silk_SMULBB( q2_Q10, Lambda_Q10 ); + } else if( q1_Q0 == 0 ) { + q1_Q10 = offset_Q10; + q2_Q10 = silk_ADD32( q1_Q10, 1024 - QUANT_LEVEL_ADJUST_Q10 ); + rd1_Q20 = silk_SMULBB( q1_Q10, Lambda_Q10 ); + rd2_Q20 = silk_SMULBB( q2_Q10, Lambda_Q10 ); + } else if( q1_Q0 == -1 ) { + q2_Q10 = offset_Q10; + q1_Q10 = silk_SUB32( q2_Q10, 1024 - QUANT_LEVEL_ADJUST_Q10 ); + rd1_Q20 = silk_SMULBB( -q1_Q10, Lambda_Q10 ); + rd2_Q20 = silk_SMULBB( q2_Q10, Lambda_Q10 ); + } else { /* Q1_Q0 < -1 */ + q1_Q10 = silk_ADD32( silk_LSHIFT( q1_Q0, 10 ), QUANT_LEVEL_ADJUST_Q10 ); + q1_Q10 = silk_ADD32( q1_Q10, offset_Q10 ); + q2_Q10 = silk_ADD32( q1_Q10, 1024 ); + rd1_Q20 = silk_SMULBB( -q1_Q10, Lambda_Q10 ); + rd2_Q20 = silk_SMULBB( -q2_Q10, Lambda_Q10 ); + } + rr_Q10 = silk_SUB32( r_Q10, q1_Q10 ); + rd1_Q20 = silk_SMLABB( rd1_Q20, rr_Q10, rr_Q10 ); + rr_Q10 = silk_SUB32( r_Q10, q2_Q10 ); + rd2_Q20 = silk_SMLABB( rd2_Q20, rr_Q10, rr_Q10 ); + + if( rd2_Q20 < rd1_Q20 ) { + q1_Q10 = q2_Q10; + } + + pulses[ i ] = (opus_int8)silk_RSHIFT_ROUND( q1_Q10, 10 ); + + /* Excitation */ + exc_Q14 = silk_LSHIFT( q1_Q10, 4 ); + if ( NSQ->rand_seed < 0 ) { + exc_Q14 = -exc_Q14; + } + + /* Add predictions */ + LPC_exc_Q14 = silk_ADD_LSHIFT32( exc_Q14, LTP_pred_Q13, 1 ); + xq_Q14 = silk_ADD32_ovflw( LPC_exc_Q14, silk_LSHIFT32( LPC_pred_Q10, 4 ) ); + + /* Scale XQ back to normal level before saving */ + xq[ i ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( silk_SMULWW( xq_Q14, Gain_Q10 ), 8 ) ); + + /* Update states */ + psLPC_Q14++; + *psLPC_Q14 = xq_Q14; + NSQ->sDiff_shp_Q14 = silk_SUB_LSHIFT32( xq_Q14, x_sc_Q10[ i ], 4 ); + sLF_AR_shp_Q14 = silk_SUB32_ovflw( NSQ->sDiff_shp_Q14, silk_LSHIFT32( n_AR_Q12, 2 ) ); + NSQ->sLF_AR_shp_Q14 = sLF_AR_shp_Q14; + + NSQ->sLTP_shp_Q14[ NSQ->sLTP_shp_buf_idx ] = silk_SUB32_ovflw(sLF_AR_shp_Q14, silk_LSHIFT32(n_LF_Q12, 2)); + sLTP_Q15[ NSQ->sLTP_buf_idx ] = silk_LSHIFT( LPC_exc_Q14, 1 ); + NSQ->sLTP_shp_buf_idx++; + NSQ->sLTP_buf_idx++; + + /* Make dither dependent on quantized signal */ + NSQ->rand_seed = silk_ADD32_ovflw( NSQ->rand_seed, pulses[ i ] ); + } + + /* Update LPC synth buffer */ + silk_memcpy( NSQ->sLPC_Q14, &NSQ->sLPC_Q14[ length ], NSQ_LPC_BUF_LENGTH * sizeof( opus_int32 ) ); +} + +static OPUS_INLINE void silk_nsq_scale_states( + const silk_encoder_state *psEncC, /* I Encoder State */ + silk_nsq_state *NSQ, /* I/O NSQ state */ + const opus_int16 x16[], /* I input */ + opus_int32 x_sc_Q10[], /* O input scaled with 1/Gain */ + const opus_int16 sLTP[], /* I re-whitened LTP state in Q0 */ + opus_int32 sLTP_Q15[], /* O LTP state matching scaled input */ + opus_int subfr, /* I subframe number */ + const opus_int LTP_scale_Q14, /* I */ + const opus_int32 Gains_Q16[ MAX_NB_SUBFR ], /* I */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lag */ + const opus_int signal_type /* I Signal type */ +) +{ + opus_int i, lag; + opus_int32 gain_adj_Q16, inv_gain_Q31, inv_gain_Q26; + + lag = pitchL[ subfr ]; + inv_gain_Q31 = silk_INVERSE32_varQ( silk_max( Gains_Q16[ subfr ], 1 ), 47 ); + silk_assert( inv_gain_Q31 != 0 ); + + /* Scale input */ + inv_gain_Q26 = silk_RSHIFT_ROUND( inv_gain_Q31, 5 ); + for( i = 0; i < psEncC->subfr_length; i++ ) { + x_sc_Q10[ i ] = silk_SMULWW( x16[ i ], inv_gain_Q26 ); + } + + /* After rewhitening the LTP state is un-scaled, so scale with inv_gain_Q16 */ + if( NSQ->rewhite_flag ) { + if( subfr == 0 ) { + /* Do LTP downscaling */ + inv_gain_Q31 = silk_LSHIFT( silk_SMULWB( inv_gain_Q31, LTP_scale_Q14 ), 2 ); + } + for( i = NSQ->sLTP_buf_idx - lag - LTP_ORDER / 2; i < NSQ->sLTP_buf_idx; i++ ) { + silk_assert( i < MAX_FRAME_LENGTH ); + sLTP_Q15[ i ] = silk_SMULWB( inv_gain_Q31, sLTP[ i ] ); + } + } + + /* Adjust for changing gain */ + if( Gains_Q16[ subfr ] != NSQ->prev_gain_Q16 ) { + gain_adj_Q16 = silk_DIV32_varQ( NSQ->prev_gain_Q16, Gains_Q16[ subfr ], 16 ); + + /* Scale long-term shaping state */ + for( i = NSQ->sLTP_shp_buf_idx - psEncC->ltp_mem_length; i < NSQ->sLTP_shp_buf_idx; i++ ) { + NSQ->sLTP_shp_Q14[ i ] = silk_SMULWW( gain_adj_Q16, NSQ->sLTP_shp_Q14[ i ] ); + } + + /* Scale long-term prediction state */ + if( signal_type == TYPE_VOICED && NSQ->rewhite_flag == 0 ) { + for( i = NSQ->sLTP_buf_idx - lag - LTP_ORDER / 2; i < NSQ->sLTP_buf_idx; i++ ) { + sLTP_Q15[ i ] = silk_SMULWW( gain_adj_Q16, sLTP_Q15[ i ] ); + } + } + + NSQ->sLF_AR_shp_Q14 = silk_SMULWW( gain_adj_Q16, NSQ->sLF_AR_shp_Q14 ); + NSQ->sDiff_shp_Q14 = silk_SMULWW( gain_adj_Q16, NSQ->sDiff_shp_Q14 ); + + /* Scale short-term prediction and shaping states */ + for( i = 0; i < NSQ_LPC_BUF_LENGTH; i++ ) { + NSQ->sLPC_Q14[ i ] = silk_SMULWW( gain_adj_Q16, NSQ->sLPC_Q14[ i ] ); + } + for( i = 0; i < MAX_SHAPE_LPC_ORDER; i++ ) { + NSQ->sAR2_Q14[ i ] = silk_SMULWW( gain_adj_Q16, NSQ->sAR2_Q14[ i ] ); + } + + /* Save inverse gain */ + NSQ->prev_gain_Q16 = Gains_Q16[ subfr ]; + } +} diff --git a/port/third_party/opus/silk/NSQ.h b/port/third_party/opus/silk/NSQ.h new file mode 100644 index 00000000..971832f6 --- /dev/null +++ b/port/third_party/opus/silk/NSQ.h @@ -0,0 +1,101 @@ +/*********************************************************************** +Copyright (c) 2014 Vidyo. +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ +#ifndef SILK_NSQ_H +#define SILK_NSQ_H + +#include "SigProc_FIX.h" + +#undef silk_short_prediction_create_arch_coef + +static OPUS_INLINE opus_int32 silk_noise_shape_quantizer_short_prediction_c(const opus_int32 *buf32, const opus_int16 *coef16, opus_int order) +{ + opus_int32 out; + silk_assert( order == 10 || order == 16 ); + + /* Avoids introducing a bias because silk_SMLAWB() always rounds to -inf */ + out = silk_RSHIFT( order, 1 ); + out = silk_SMLAWB( out, buf32[ 0 ], coef16[ 0 ] ); + out = silk_SMLAWB( out, buf32[ -1 ], coef16[ 1 ] ); + out = silk_SMLAWB( out, buf32[ -2 ], coef16[ 2 ] ); + out = silk_SMLAWB( out, buf32[ -3 ], coef16[ 3 ] ); + out = silk_SMLAWB( out, buf32[ -4 ], coef16[ 4 ] ); + out = silk_SMLAWB( out, buf32[ -5 ], coef16[ 5 ] ); + out = silk_SMLAWB( out, buf32[ -6 ], coef16[ 6 ] ); + out = silk_SMLAWB( out, buf32[ -7 ], coef16[ 7 ] ); + out = silk_SMLAWB( out, buf32[ -8 ], coef16[ 8 ] ); + out = silk_SMLAWB( out, buf32[ -9 ], coef16[ 9 ] ); + + if( order == 16 ) + { + out = silk_SMLAWB( out, buf32[ -10 ], coef16[ 10 ] ); + out = silk_SMLAWB( out, buf32[ -11 ], coef16[ 11 ] ); + out = silk_SMLAWB( out, buf32[ -12 ], coef16[ 12 ] ); + out = silk_SMLAWB( out, buf32[ -13 ], coef16[ 13 ] ); + out = silk_SMLAWB( out, buf32[ -14 ], coef16[ 14 ] ); + out = silk_SMLAWB( out, buf32[ -15 ], coef16[ 15 ] ); + } + return out; +} + +#define silk_noise_shape_quantizer_short_prediction(in, coef, coefRev, order, arch) ((void)arch,silk_noise_shape_quantizer_short_prediction_c(in, coef, order)) + +static OPUS_INLINE opus_int32 silk_NSQ_noise_shape_feedback_loop_c(const opus_int32 *data0, opus_int32 *data1, const opus_int16 *coef, opus_int order) +{ + opus_int32 out; + opus_int32 tmp1, tmp2; + opus_int j; + + tmp2 = data0[0]; + tmp1 = data1[0]; + data1[0] = tmp2; + + out = silk_RSHIFT(order, 1); + out = silk_SMLAWB(out, tmp2, coef[0]); + + for (j = 2; j < order; j += 2) { + tmp2 = data1[j - 1]; + data1[j - 1] = tmp1; + out = silk_SMLAWB(out, tmp1, coef[j - 1]); + tmp1 = data1[j + 0]; + data1[j + 0] = tmp2; + out = silk_SMLAWB(out, tmp2, coef[j]); + } + data1[order - 1] = tmp1; + out = silk_SMLAWB(out, tmp1, coef[order - 1]); + /* Q11 -> Q12 */ + out = silk_LSHIFT32( out, 1 ); + return out; +} + +#define silk_NSQ_noise_shape_feedback_loop(data0, data1, coef, order, arch) ((void)arch,silk_NSQ_noise_shape_feedback_loop_c(data0, data1, coef, order)) + +#if defined(OPUS_ARM_MAY_HAVE_NEON_INTR) +#include "arm/NSQ_neon.h" +#endif + +#endif /* SILK_NSQ_H */ diff --git a/port/third_party/opus/silk/NSQ_del_dec.c b/port/third_party/opus/silk/NSQ_del_dec.c new file mode 100644 index 00000000..e8dadf15 --- /dev/null +++ b/port/third_party/opus/silk/NSQ_del_dec.c @@ -0,0 +1,733 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" +#include "NSQ.h" + + +typedef struct { + opus_int32 sLPC_Q14[ MAX_SUB_FRAME_LENGTH + NSQ_LPC_BUF_LENGTH ]; + opus_int32 RandState[ DECISION_DELAY ]; + opus_int32 Q_Q10[ DECISION_DELAY ]; + opus_int32 Xq_Q14[ DECISION_DELAY ]; + opus_int32 Pred_Q15[ DECISION_DELAY ]; + opus_int32 Shape_Q14[ DECISION_DELAY ]; + opus_int32 sAR2_Q14[ MAX_SHAPE_LPC_ORDER ]; + opus_int32 LF_AR_Q14; + opus_int32 Diff_Q14; + opus_int32 Seed; + opus_int32 SeedInit; + opus_int32 RD_Q10; +} NSQ_del_dec_struct; + +typedef struct { + opus_int32 Q_Q10; + opus_int32 RD_Q10; + opus_int32 xq_Q14; + opus_int32 LF_AR_Q14; + opus_int32 Diff_Q14; + opus_int32 sLTP_shp_Q14; + opus_int32 LPC_exc_Q14; +} NSQ_sample_struct; + +typedef NSQ_sample_struct NSQ_sample_pair[ 2 ]; + +#if defined(MIPSr1_ASM) +#include "mips/NSQ_del_dec_mipsr1.h" +#endif +static OPUS_INLINE void silk_nsq_del_dec_scale_states( + const silk_encoder_state *psEncC, /* I Encoder State */ + silk_nsq_state *NSQ, /* I/O NSQ state */ + NSQ_del_dec_struct psDelDec[], /* I/O Delayed decision states */ + const opus_int16 x16[], /* I Input */ + opus_int32 x_sc_Q10[], /* O Input scaled with 1/Gain in Q10 */ + const opus_int16 sLTP[], /* I Re-whitened LTP state in Q0 */ + opus_int32 sLTP_Q15[], /* O LTP state matching scaled input */ + opus_int subfr, /* I Subframe number */ + opus_int nStatesDelayedDecision, /* I Number of del dec states */ + const opus_int LTP_scale_Q14, /* I LTP state scaling */ + const opus_int32 Gains_Q16[ MAX_NB_SUBFR ], /* I */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lag */ + const opus_int signal_type, /* I Signal type */ + const opus_int decisionDelay /* I Decision delay */ +); + +/******************************************/ +/* Noise shape quantizer for one subframe */ +/******************************************/ +static OPUS_INLINE void silk_noise_shape_quantizer_del_dec( + silk_nsq_state *NSQ, /* I/O NSQ state */ + NSQ_del_dec_struct psDelDec[], /* I/O Delayed decision states */ + opus_int signalType, /* I Signal type */ + const opus_int32 x_Q10[], /* I */ + opus_int8 pulses[], /* O */ + opus_int16 xq[], /* O */ + opus_int32 sLTP_Q15[], /* I/O LTP filter state */ + opus_int32 delayedGain_Q10[], /* I/O Gain delay buffer */ + const opus_int16 a_Q12[], /* I Short term prediction coefs */ + const opus_int16 b_Q14[], /* I Long term prediction coefs */ + const opus_int16 AR_shp_Q13[], /* I Noise shaping coefs */ + opus_int lag, /* I Pitch lag */ + opus_int32 HarmShapeFIRPacked_Q14, /* I */ + opus_int Tilt_Q14, /* I Spectral tilt */ + opus_int32 LF_shp_Q14, /* I */ + opus_int32 Gain_Q16, /* I */ + opus_int Lambda_Q10, /* I */ + opus_int offset_Q10, /* I */ + opus_int length, /* I Input length */ + opus_int subfr, /* I Subframe number */ + opus_int shapingLPCOrder, /* I Shaping LPC filter order */ + opus_int predictLPCOrder, /* I Prediction filter order */ + opus_int warping_Q16, /* I */ + opus_int nStatesDelayedDecision, /* I Number of states in decision tree */ + opus_int *smpl_buf_idx, /* I/O Index to newest samples in buffers */ + opus_int decisionDelay, /* I */ + int arch /* I */ +); + +void silk_NSQ_del_dec_c( + const silk_encoder_state *psEncC, /* I Encoder State */ + silk_nsq_state *NSQ, /* I/O NSQ state */ + SideInfoIndices *psIndices, /* I/O Quantization Indices */ + const opus_int16 x16[], /* I Input */ + opus_int8 pulses[], /* O Quantized pulse signal */ + const opus_int16 *PredCoef_Q12, /* I Short term prediction coefs */ + const opus_int16 LTPCoef_Q14[ LTP_ORDER * MAX_NB_SUBFR ], /* I Long term prediction coefs */ + const opus_int16 AR_Q13[ MAX_NB_SUBFR * MAX_SHAPE_LPC_ORDER ], /* I Noise shaping coefs */ + const opus_int HarmShapeGain_Q14[ MAX_NB_SUBFR ], /* I Long term shaping coefs */ + const opus_int Tilt_Q14[ MAX_NB_SUBFR ], /* I Spectral tilt */ + const opus_int32 LF_shp_Q14[ MAX_NB_SUBFR ], /* I Low frequency shaping coefs */ + const opus_int32 Gains_Q16[ MAX_NB_SUBFR ], /* I Quantization step sizes */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lags */ + const opus_int Lambda_Q10, /* I Rate/distortion tradeoff */ + const opus_int LTP_scale_Q14 /* I LTP state scaling */ +) +{ + opus_int i, k, lag, start_idx, LSF_interpolation_flag, Winner_ind, subfr; + opus_int last_smple_idx, smpl_buf_idx, decisionDelay; + const opus_int16 *A_Q12, *B_Q14, *AR_shp_Q13; + opus_int16 *pxq; + VARDECL( opus_int32, sLTP_Q15 ); + VARDECL( opus_int16, sLTP ); + opus_int32 HarmShapeFIRPacked_Q14; + opus_int offset_Q10; + opus_int32 RDmin_Q10, Gain_Q10; + VARDECL( opus_int32, x_sc_Q10 ); + VARDECL( opus_int32, delayedGain_Q10 ); + VARDECL( NSQ_del_dec_struct, psDelDec ); + NSQ_del_dec_struct *psDD; + SAVE_STACK; + + /* Set unvoiced lag to the previous one, overwrite later for voiced */ + lag = NSQ->lagPrev; + + silk_assert( NSQ->prev_gain_Q16 != 0 ); + + /* Initialize delayed decision states */ + ALLOC( psDelDec, psEncC->nStatesDelayedDecision, NSQ_del_dec_struct ); + silk_memset( psDelDec, 0, psEncC->nStatesDelayedDecision * sizeof( NSQ_del_dec_struct ) ); + for( k = 0; k < psEncC->nStatesDelayedDecision; k++ ) { + psDD = &psDelDec[ k ]; + psDD->Seed = ( k + psIndices->Seed ) & 3; + psDD->SeedInit = psDD->Seed; + psDD->RD_Q10 = 0; + psDD->LF_AR_Q14 = NSQ->sLF_AR_shp_Q14; + psDD->Diff_Q14 = NSQ->sDiff_shp_Q14; + psDD->Shape_Q14[ 0 ] = NSQ->sLTP_shp_Q14[ psEncC->ltp_mem_length - 1 ]; + silk_memcpy( psDD->sLPC_Q14, NSQ->sLPC_Q14, NSQ_LPC_BUF_LENGTH * sizeof( opus_int32 ) ); + silk_memcpy( psDD->sAR2_Q14, NSQ->sAR2_Q14, sizeof( NSQ->sAR2_Q14 ) ); + } + + offset_Q10 = silk_Quantization_Offsets_Q10[ psIndices->signalType >> 1 ][ psIndices->quantOffsetType ]; + smpl_buf_idx = 0; /* index of oldest samples */ + + decisionDelay = silk_min_int( DECISION_DELAY, psEncC->subfr_length ); + + /* For voiced frames limit the decision delay to lower than the pitch lag */ + if( psIndices->signalType == TYPE_VOICED ) { + for( k = 0; k < psEncC->nb_subfr; k++ ) { + decisionDelay = silk_min_int( decisionDelay, pitchL[ k ] - LTP_ORDER / 2 - 1 ); + } + } else { + if( lag > 0 ) { + decisionDelay = silk_min_int( decisionDelay, lag - LTP_ORDER / 2 - 1 ); + } + } + + if( psIndices->NLSFInterpCoef_Q2 == 4 ) { + LSF_interpolation_flag = 0; + } else { + LSF_interpolation_flag = 1; + } + + ALLOC( sLTP_Q15, psEncC->ltp_mem_length + psEncC->frame_length, opus_int32 ); + ALLOC( sLTP, psEncC->ltp_mem_length + psEncC->frame_length, opus_int16 ); + ALLOC( x_sc_Q10, psEncC->subfr_length, opus_int32 ); + ALLOC( delayedGain_Q10, DECISION_DELAY, opus_int32 ); + /* Set up pointers to start of sub frame */ + pxq = &NSQ->xq[ psEncC->ltp_mem_length ]; + NSQ->sLTP_shp_buf_idx = psEncC->ltp_mem_length; + NSQ->sLTP_buf_idx = psEncC->ltp_mem_length; + subfr = 0; + for( k = 0; k < psEncC->nb_subfr; k++ ) { + A_Q12 = &PredCoef_Q12[ ( ( k >> 1 ) | ( 1 - LSF_interpolation_flag ) ) * MAX_LPC_ORDER ]; + B_Q14 = <PCoef_Q14[ k * LTP_ORDER ]; + AR_shp_Q13 = &AR_Q13[ k * MAX_SHAPE_LPC_ORDER ]; + + /* Noise shape parameters */ + silk_assert( HarmShapeGain_Q14[ k ] >= 0 ); + HarmShapeFIRPacked_Q14 = silk_RSHIFT( HarmShapeGain_Q14[ k ], 2 ); + HarmShapeFIRPacked_Q14 |= silk_LSHIFT( (opus_int32)silk_RSHIFT( HarmShapeGain_Q14[ k ], 1 ), 16 ); + + NSQ->rewhite_flag = 0; + if( psIndices->signalType == TYPE_VOICED ) { + /* Voiced */ + lag = pitchL[ k ]; + + /* Re-whitening */ + if( ( k & ( 3 - silk_LSHIFT( LSF_interpolation_flag, 1 ) ) ) == 0 ) { + if( k == 2 ) { + /* RESET DELAYED DECISIONS */ + /* Find winner */ + RDmin_Q10 = psDelDec[ 0 ].RD_Q10; + Winner_ind = 0; + for( i = 1; i < psEncC->nStatesDelayedDecision; i++ ) { + if( psDelDec[ i ].RD_Q10 < RDmin_Q10 ) { + RDmin_Q10 = psDelDec[ i ].RD_Q10; + Winner_ind = i; + } + } + for( i = 0; i < psEncC->nStatesDelayedDecision; i++ ) { + if( i != Winner_ind ) { + psDelDec[ i ].RD_Q10 += ( silk_int32_MAX >> 4 ); + silk_assert( psDelDec[ i ].RD_Q10 >= 0 ); + } + } + + /* Copy final part of signals from winner state to output and long-term filter states */ + psDD = &psDelDec[ Winner_ind ]; + last_smple_idx = smpl_buf_idx + decisionDelay; + for( i = 0; i < decisionDelay; i++ ) { + last_smple_idx = ( last_smple_idx - 1 ) % DECISION_DELAY; + if( last_smple_idx < 0 ) last_smple_idx += DECISION_DELAY; + pulses[ i - decisionDelay ] = (opus_int8)silk_RSHIFT_ROUND( psDD->Q_Q10[ last_smple_idx ], 10 ); + pxq[ i - decisionDelay ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( + silk_SMULWW( psDD->Xq_Q14[ last_smple_idx ], Gains_Q16[ 1 ] ), 14 ) ); + NSQ->sLTP_shp_Q14[ NSQ->sLTP_shp_buf_idx - decisionDelay + i ] = psDD->Shape_Q14[ last_smple_idx ]; + } + + subfr = 0; + } + + /* Rewhiten with new A coefs */ + start_idx = psEncC->ltp_mem_length - lag - psEncC->predictLPCOrder - LTP_ORDER / 2; + celt_assert( start_idx > 0 ); + + silk_LPC_analysis_filter( &sLTP[ start_idx ], &NSQ->xq[ start_idx + k * psEncC->subfr_length ], + A_Q12, psEncC->ltp_mem_length - start_idx, psEncC->predictLPCOrder, psEncC->arch ); + + NSQ->sLTP_buf_idx = psEncC->ltp_mem_length; + NSQ->rewhite_flag = 1; + } + } + + silk_nsq_del_dec_scale_states( psEncC, NSQ, psDelDec, x16, x_sc_Q10, sLTP, sLTP_Q15, k, + psEncC->nStatesDelayedDecision, LTP_scale_Q14, Gains_Q16, pitchL, psIndices->signalType, decisionDelay ); + + silk_noise_shape_quantizer_del_dec( NSQ, psDelDec, psIndices->signalType, x_sc_Q10, pulses, pxq, sLTP_Q15, + delayedGain_Q10, A_Q12, B_Q14, AR_shp_Q13, lag, HarmShapeFIRPacked_Q14, Tilt_Q14[ k ], LF_shp_Q14[ k ], + Gains_Q16[ k ], Lambda_Q10, offset_Q10, psEncC->subfr_length, subfr++, psEncC->shapingLPCOrder, + psEncC->predictLPCOrder, psEncC->warping_Q16, psEncC->nStatesDelayedDecision, &smpl_buf_idx, decisionDelay, psEncC->arch ); + + x16 += psEncC->subfr_length; + pulses += psEncC->subfr_length; + pxq += psEncC->subfr_length; + } + + /* Find winner */ + RDmin_Q10 = psDelDec[ 0 ].RD_Q10; + Winner_ind = 0; + for( k = 1; k < psEncC->nStatesDelayedDecision; k++ ) { + if( psDelDec[ k ].RD_Q10 < RDmin_Q10 ) { + RDmin_Q10 = psDelDec[ k ].RD_Q10; + Winner_ind = k; + } + } + + /* Copy final part of signals from winner state to output and long-term filter states */ + psDD = &psDelDec[ Winner_ind ]; + psIndices->Seed = psDD->SeedInit; + last_smple_idx = smpl_buf_idx + decisionDelay; + Gain_Q10 = silk_RSHIFT32( Gains_Q16[ psEncC->nb_subfr - 1 ], 6 ); + for( i = 0; i < decisionDelay; i++ ) { + last_smple_idx = ( last_smple_idx - 1 ) % DECISION_DELAY; + if( last_smple_idx < 0 ) last_smple_idx += DECISION_DELAY; + + pulses[ i - decisionDelay ] = (opus_int8)silk_RSHIFT_ROUND( psDD->Q_Q10[ last_smple_idx ], 10 ); + pxq[ i - decisionDelay ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( + silk_SMULWW( psDD->Xq_Q14[ last_smple_idx ], Gain_Q10 ), 8 ) ); + NSQ->sLTP_shp_Q14[ NSQ->sLTP_shp_buf_idx - decisionDelay + i ] = psDD->Shape_Q14[ last_smple_idx ]; + } + silk_memcpy( NSQ->sLPC_Q14, &psDD->sLPC_Q14[ psEncC->subfr_length ], NSQ_LPC_BUF_LENGTH * sizeof( opus_int32 ) ); + silk_memcpy( NSQ->sAR2_Q14, psDD->sAR2_Q14, sizeof( psDD->sAR2_Q14 ) ); + + /* Update states */ + NSQ->sLF_AR_shp_Q14 = psDD->LF_AR_Q14; + NSQ->sDiff_shp_Q14 = psDD->Diff_Q14; + NSQ->lagPrev = pitchL[ psEncC->nb_subfr - 1 ]; + + /* Save quantized speech signal */ + silk_memmove( NSQ->xq, &NSQ->xq[ psEncC->frame_length ], psEncC->ltp_mem_length * sizeof( opus_int16 ) ); + silk_memmove( NSQ->sLTP_shp_Q14, &NSQ->sLTP_shp_Q14[ psEncC->frame_length ], psEncC->ltp_mem_length * sizeof( opus_int32 ) ); + RESTORE_STACK; +} + +/******************************************/ +/* Noise shape quantizer for one subframe */ +/******************************************/ +#ifndef OVERRIDE_silk_noise_shape_quantizer_del_dec +static OPUS_INLINE void silk_noise_shape_quantizer_del_dec( + silk_nsq_state *NSQ, /* I/O NSQ state */ + NSQ_del_dec_struct psDelDec[], /* I/O Delayed decision states */ + opus_int signalType, /* I Signal type */ + const opus_int32 x_Q10[], /* I */ + opus_int8 pulses[], /* O */ + opus_int16 xq[], /* O */ + opus_int32 sLTP_Q15[], /* I/O LTP filter state */ + opus_int32 delayedGain_Q10[], /* I/O Gain delay buffer */ + const opus_int16 a_Q12[], /* I Short term prediction coefs */ + const opus_int16 b_Q14[], /* I Long term prediction coefs */ + const opus_int16 AR_shp_Q13[], /* I Noise shaping coefs */ + opus_int lag, /* I Pitch lag */ + opus_int32 HarmShapeFIRPacked_Q14, /* I */ + opus_int Tilt_Q14, /* I Spectral tilt */ + opus_int32 LF_shp_Q14, /* I */ + opus_int32 Gain_Q16, /* I */ + opus_int Lambda_Q10, /* I */ + opus_int offset_Q10, /* I */ + opus_int length, /* I Input length */ + opus_int subfr, /* I Subframe number */ + opus_int shapingLPCOrder, /* I Shaping LPC filter order */ + opus_int predictLPCOrder, /* I Prediction filter order */ + opus_int warping_Q16, /* I */ + opus_int nStatesDelayedDecision, /* I Number of states in decision tree */ + opus_int *smpl_buf_idx, /* I/O Index to newest samples in buffers */ + opus_int decisionDelay, /* I */ + int arch /* I */ +) +{ + opus_int i, j, k, Winner_ind, RDmin_ind, RDmax_ind, last_smple_idx; + opus_int32 Winner_rand_state; + opus_int32 LTP_pred_Q14, LPC_pred_Q14, n_AR_Q14, n_LTP_Q14; + opus_int32 n_LF_Q14, r_Q10, rr_Q10, rd1_Q10, rd2_Q10, RDmin_Q10, RDmax_Q10; + opus_int32 q1_Q0, q1_Q10, q2_Q10, exc_Q14, LPC_exc_Q14, xq_Q14, Gain_Q10; + opus_int32 tmp1, tmp2, sLF_AR_shp_Q14; + opus_int32 *pred_lag_ptr, *shp_lag_ptr, *psLPC_Q14; +#ifdef silk_short_prediction_create_arch_coef + opus_int32 a_Q12_arch[MAX_LPC_ORDER]; +#endif + + VARDECL( NSQ_sample_pair, psSampleState ); + NSQ_del_dec_struct *psDD; + NSQ_sample_struct *psSS; + SAVE_STACK; + + celt_assert( nStatesDelayedDecision > 0 ); + ALLOC( psSampleState, nStatesDelayedDecision, NSQ_sample_pair ); + + shp_lag_ptr = &NSQ->sLTP_shp_Q14[ NSQ->sLTP_shp_buf_idx - lag + HARM_SHAPE_FIR_TAPS / 2 ]; + pred_lag_ptr = &sLTP_Q15[ NSQ->sLTP_buf_idx - lag + LTP_ORDER / 2 ]; + Gain_Q10 = silk_RSHIFT( Gain_Q16, 6 ); + +#ifdef silk_short_prediction_create_arch_coef + silk_short_prediction_create_arch_coef(a_Q12_arch, a_Q12, predictLPCOrder); +#endif + + for( i = 0; i < length; i++ ) { + /* Perform common calculations used in all states */ + + /* Long-term prediction */ + if( signalType == TYPE_VOICED ) { + /* Unrolled loop */ + /* Avoids introducing a bias because silk_SMLAWB() always rounds to -inf */ + LTP_pred_Q14 = 2; + LTP_pred_Q14 = silk_SMLAWB( LTP_pred_Q14, pred_lag_ptr[ 0 ], b_Q14[ 0 ] ); + LTP_pred_Q14 = silk_SMLAWB( LTP_pred_Q14, pred_lag_ptr[ -1 ], b_Q14[ 1 ] ); + LTP_pred_Q14 = silk_SMLAWB( LTP_pred_Q14, pred_lag_ptr[ -2 ], b_Q14[ 2 ] ); + LTP_pred_Q14 = silk_SMLAWB( LTP_pred_Q14, pred_lag_ptr[ -3 ], b_Q14[ 3 ] ); + LTP_pred_Q14 = silk_SMLAWB( LTP_pred_Q14, pred_lag_ptr[ -4 ], b_Q14[ 4 ] ); + LTP_pred_Q14 = silk_LSHIFT( LTP_pred_Q14, 1 ); /* Q13 -> Q14 */ + pred_lag_ptr++; + } else { + LTP_pred_Q14 = 0; + } + + /* Long-term shaping */ + if( lag > 0 ) { + /* Symmetric, packed FIR coefficients */ + n_LTP_Q14 = silk_SMULWB( silk_ADD_SAT32( shp_lag_ptr[ 0 ], shp_lag_ptr[ -2 ] ), HarmShapeFIRPacked_Q14 ); + n_LTP_Q14 = silk_SMLAWT( n_LTP_Q14, shp_lag_ptr[ -1 ], HarmShapeFIRPacked_Q14 ); + n_LTP_Q14 = silk_SUB_LSHIFT32( LTP_pred_Q14, n_LTP_Q14, 2 ); /* Q12 -> Q14 */ + shp_lag_ptr++; + } else { + n_LTP_Q14 = 0; + } + + for( k = 0; k < nStatesDelayedDecision; k++ ) { + /* Delayed decision state */ + psDD = &psDelDec[ k ]; + + /* Sample state */ + psSS = psSampleState[ k ]; + + /* Generate dither */ + psDD->Seed = silk_RAND( psDD->Seed ); + + /* Pointer used in short term prediction and shaping */ + psLPC_Q14 = &psDD->sLPC_Q14[ NSQ_LPC_BUF_LENGTH - 1 + i ]; + /* Short-term prediction */ + LPC_pred_Q14 = silk_noise_shape_quantizer_short_prediction(psLPC_Q14, a_Q12, a_Q12_arch, predictLPCOrder, arch); + LPC_pred_Q14 = silk_LSHIFT( LPC_pred_Q14, 4 ); /* Q10 -> Q14 */ + + /* Noise shape feedback */ + celt_assert( ( shapingLPCOrder & 1 ) == 0 ); /* check that order is even */ + /* Output of lowpass section */ + tmp2 = silk_SMLAWB( psDD->Diff_Q14, psDD->sAR2_Q14[ 0 ], warping_Q16 ); + /* Output of allpass section */ + tmp1 = silk_SMLAWB( psDD->sAR2_Q14[ 0 ], silk_SUB32_ovflw(psDD->sAR2_Q14[ 1 ], tmp2), warping_Q16 ); + psDD->sAR2_Q14[ 0 ] = tmp2; + n_AR_Q14 = silk_RSHIFT( shapingLPCOrder, 1 ); + n_AR_Q14 = silk_SMLAWB( n_AR_Q14, tmp2, AR_shp_Q13[ 0 ] ); + /* Loop over allpass sections */ + for( j = 2; j < shapingLPCOrder; j += 2 ) { + /* Output of allpass section */ + tmp2 = silk_SMLAWB( psDD->sAR2_Q14[ j - 1 ], silk_SUB32_ovflw(psDD->sAR2_Q14[ j + 0 ], tmp1), warping_Q16 ); + psDD->sAR2_Q14[ j - 1 ] = tmp1; + n_AR_Q14 = silk_SMLAWB( n_AR_Q14, tmp1, AR_shp_Q13[ j - 1 ] ); + /* Output of allpass section */ + tmp1 = silk_SMLAWB( psDD->sAR2_Q14[ j + 0 ], silk_SUB32_ovflw(psDD->sAR2_Q14[ j + 1 ], tmp2), warping_Q16 ); + psDD->sAR2_Q14[ j + 0 ] = tmp2; + n_AR_Q14 = silk_SMLAWB( n_AR_Q14, tmp2, AR_shp_Q13[ j ] ); + } + psDD->sAR2_Q14[ shapingLPCOrder - 1 ] = tmp1; + n_AR_Q14 = silk_SMLAWB( n_AR_Q14, tmp1, AR_shp_Q13[ shapingLPCOrder - 1 ] ); + + n_AR_Q14 = silk_LSHIFT( n_AR_Q14, 1 ); /* Q11 -> Q12 */ + n_AR_Q14 = silk_SMLAWB( n_AR_Q14, psDD->LF_AR_Q14, Tilt_Q14 ); /* Q12 */ + n_AR_Q14 = silk_LSHIFT( n_AR_Q14, 2 ); /* Q12 -> Q14 */ + + n_LF_Q14 = silk_SMULWB( psDD->Shape_Q14[ *smpl_buf_idx ], LF_shp_Q14 ); /* Q12 */ + n_LF_Q14 = silk_SMLAWT( n_LF_Q14, psDD->LF_AR_Q14, LF_shp_Q14 ); /* Q12 */ + n_LF_Q14 = silk_LSHIFT( n_LF_Q14, 2 ); /* Q12 -> Q14 */ + + /* Input minus prediction plus noise feedback */ + /* r = x[ i ] - LTP_pred - LPC_pred + n_AR + n_Tilt + n_LF + n_LTP */ + tmp1 = silk_ADD_SAT32( n_AR_Q14, n_LF_Q14 ); /* Q14 */ + tmp2 = silk_ADD32_ovflw( n_LTP_Q14, LPC_pred_Q14 ); /* Q13 */ + tmp1 = silk_SUB_SAT32( tmp2, tmp1 ); /* Q13 */ + tmp1 = silk_RSHIFT_ROUND( tmp1, 4 ); /* Q10 */ + + r_Q10 = silk_SUB32( x_Q10[ i ], tmp1 ); /* residual error Q10 */ + + /* Flip sign depending on dither */ + if ( psDD->Seed < 0 ) { + r_Q10 = -r_Q10; + } + r_Q10 = silk_LIMIT_32( r_Q10, -(31 << 10), 30 << 10 ); + + /* Find two quantization level candidates and measure their rate-distortion */ + q1_Q10 = silk_SUB32( r_Q10, offset_Q10 ); + q1_Q0 = silk_RSHIFT( q1_Q10, 10 ); + if (Lambda_Q10 > 2048) { + /* For aggressive RDO, the bias becomes more than one pulse. */ + int rdo_offset = Lambda_Q10/2 - 512; + if (q1_Q10 > rdo_offset) { + q1_Q0 = silk_RSHIFT( q1_Q10 - rdo_offset, 10 ); + } else if (q1_Q10 < -rdo_offset) { + q1_Q0 = silk_RSHIFT( q1_Q10 + rdo_offset, 10 ); + } else if (q1_Q10 < 0) { + q1_Q0 = -1; + } else { + q1_Q0 = 0; + } + } + if( q1_Q0 > 0 ) { + q1_Q10 = silk_SUB32( silk_LSHIFT( q1_Q0, 10 ), QUANT_LEVEL_ADJUST_Q10 ); + q1_Q10 = silk_ADD32( q1_Q10, offset_Q10 ); + q2_Q10 = silk_ADD32( q1_Q10, 1024 ); + rd1_Q10 = silk_SMULBB( q1_Q10, Lambda_Q10 ); + rd2_Q10 = silk_SMULBB( q2_Q10, Lambda_Q10 ); + } else if( q1_Q0 == 0 ) { + q1_Q10 = offset_Q10; + q2_Q10 = silk_ADD32( q1_Q10, 1024 - QUANT_LEVEL_ADJUST_Q10 ); + rd1_Q10 = silk_SMULBB( q1_Q10, Lambda_Q10 ); + rd2_Q10 = silk_SMULBB( q2_Q10, Lambda_Q10 ); + } else if( q1_Q0 == -1 ) { + q2_Q10 = offset_Q10; + q1_Q10 = silk_SUB32( q2_Q10, 1024 - QUANT_LEVEL_ADJUST_Q10 ); + rd1_Q10 = silk_SMULBB( -q1_Q10, Lambda_Q10 ); + rd2_Q10 = silk_SMULBB( q2_Q10, Lambda_Q10 ); + } else { /* q1_Q0 < -1 */ + q1_Q10 = silk_ADD32( silk_LSHIFT( q1_Q0, 10 ), QUANT_LEVEL_ADJUST_Q10 ); + q1_Q10 = silk_ADD32( q1_Q10, offset_Q10 ); + q2_Q10 = silk_ADD32( q1_Q10, 1024 ); + rd1_Q10 = silk_SMULBB( -q1_Q10, Lambda_Q10 ); + rd2_Q10 = silk_SMULBB( -q2_Q10, Lambda_Q10 ); + } + rr_Q10 = silk_SUB32( r_Q10, q1_Q10 ); + rd1_Q10 = silk_RSHIFT( silk_SMLABB( rd1_Q10, rr_Q10, rr_Q10 ), 10 ); + rr_Q10 = silk_SUB32( r_Q10, q2_Q10 ); + rd2_Q10 = silk_RSHIFT( silk_SMLABB( rd2_Q10, rr_Q10, rr_Q10 ), 10 ); + + if( rd1_Q10 < rd2_Q10 ) { + psSS[ 0 ].RD_Q10 = silk_ADD32( psDD->RD_Q10, rd1_Q10 ); + psSS[ 1 ].RD_Q10 = silk_ADD32( psDD->RD_Q10, rd2_Q10 ); + psSS[ 0 ].Q_Q10 = q1_Q10; + psSS[ 1 ].Q_Q10 = q2_Q10; + } else { + psSS[ 0 ].RD_Q10 = silk_ADD32( psDD->RD_Q10, rd2_Q10 ); + psSS[ 1 ].RD_Q10 = silk_ADD32( psDD->RD_Q10, rd1_Q10 ); + psSS[ 0 ].Q_Q10 = q2_Q10; + psSS[ 1 ].Q_Q10 = q1_Q10; + } + + /* Update states for best quantization */ + + /* Quantized excitation */ + exc_Q14 = silk_LSHIFT32( psSS[ 0 ].Q_Q10, 4 ); + if ( psDD->Seed < 0 ) { + exc_Q14 = -exc_Q14; + } + + /* Add predictions */ + LPC_exc_Q14 = silk_ADD32( exc_Q14, LTP_pred_Q14 ); + xq_Q14 = silk_ADD32_ovflw( LPC_exc_Q14, LPC_pred_Q14 ); + + /* Update states */ + psSS[ 0 ].Diff_Q14 = silk_SUB32_ovflw( xq_Q14, silk_LSHIFT32( x_Q10[ i ], 4 ) ); + sLF_AR_shp_Q14 = silk_SUB32_ovflw( psSS[ 0 ].Diff_Q14, n_AR_Q14 ); + psSS[ 0 ].sLTP_shp_Q14 = silk_SUB_SAT32( sLF_AR_shp_Q14, n_LF_Q14 ); + psSS[ 0 ].LF_AR_Q14 = sLF_AR_shp_Q14; + psSS[ 0 ].LPC_exc_Q14 = LPC_exc_Q14; + psSS[ 0 ].xq_Q14 = xq_Q14; + + /* Update states for second best quantization */ + + /* Quantized excitation */ + exc_Q14 = silk_LSHIFT32( psSS[ 1 ].Q_Q10, 4 ); + if ( psDD->Seed < 0 ) { + exc_Q14 = -exc_Q14; + } + + /* Add predictions */ + LPC_exc_Q14 = silk_ADD32( exc_Q14, LTP_pred_Q14 ); + xq_Q14 = silk_ADD32_ovflw( LPC_exc_Q14, LPC_pred_Q14 ); + + /* Update states */ + psSS[ 1 ].Diff_Q14 = silk_SUB32_ovflw( xq_Q14, silk_LSHIFT32( x_Q10[ i ], 4 ) ); + sLF_AR_shp_Q14 = silk_SUB32_ovflw( psSS[ 1 ].Diff_Q14, n_AR_Q14 ); + psSS[ 1 ].sLTP_shp_Q14 = silk_SUB_SAT32( sLF_AR_shp_Q14, n_LF_Q14 ); + psSS[ 1 ].LF_AR_Q14 = sLF_AR_shp_Q14; + psSS[ 1 ].LPC_exc_Q14 = LPC_exc_Q14; + psSS[ 1 ].xq_Q14 = xq_Q14; + } + + *smpl_buf_idx = ( *smpl_buf_idx - 1 ) % DECISION_DELAY; + if( *smpl_buf_idx < 0 ) *smpl_buf_idx += DECISION_DELAY; + last_smple_idx = ( *smpl_buf_idx + decisionDelay ) % DECISION_DELAY; + + /* Find winner */ + RDmin_Q10 = psSampleState[ 0 ][ 0 ].RD_Q10; + Winner_ind = 0; + for( k = 1; k < nStatesDelayedDecision; k++ ) { + if( psSampleState[ k ][ 0 ].RD_Q10 < RDmin_Q10 ) { + RDmin_Q10 = psSampleState[ k ][ 0 ].RD_Q10; + Winner_ind = k; + } + } + + /* Increase RD values of expired states */ + Winner_rand_state = psDelDec[ Winner_ind ].RandState[ last_smple_idx ]; + for( k = 0; k < nStatesDelayedDecision; k++ ) { + if( psDelDec[ k ].RandState[ last_smple_idx ] != Winner_rand_state ) { + psSampleState[ k ][ 0 ].RD_Q10 = silk_ADD32( psSampleState[ k ][ 0 ].RD_Q10, silk_int32_MAX >> 4 ); + psSampleState[ k ][ 1 ].RD_Q10 = silk_ADD32( psSampleState[ k ][ 1 ].RD_Q10, silk_int32_MAX >> 4 ); + silk_assert( psSampleState[ k ][ 0 ].RD_Q10 >= 0 ); + } + } + + /* Find worst in first set and best in second set */ + RDmax_Q10 = psSampleState[ 0 ][ 0 ].RD_Q10; + RDmin_Q10 = psSampleState[ 0 ][ 1 ].RD_Q10; + RDmax_ind = 0; + RDmin_ind = 0; + for( k = 1; k < nStatesDelayedDecision; k++ ) { + /* find worst in first set */ + if( psSampleState[ k ][ 0 ].RD_Q10 > RDmax_Q10 ) { + RDmax_Q10 = psSampleState[ k ][ 0 ].RD_Q10; + RDmax_ind = k; + } + /* find best in second set */ + if( psSampleState[ k ][ 1 ].RD_Q10 < RDmin_Q10 ) { + RDmin_Q10 = psSampleState[ k ][ 1 ].RD_Q10; + RDmin_ind = k; + } + } + + /* Replace a state if best from second set outperforms worst in first set */ + if( RDmin_Q10 < RDmax_Q10 ) { + silk_memcpy( ( (opus_int32 *)&psDelDec[ RDmax_ind ] ) + i, + ( (opus_int32 *)&psDelDec[ RDmin_ind ] ) + i, sizeof( NSQ_del_dec_struct ) - i * sizeof( opus_int32) ); + silk_memcpy( &psSampleState[ RDmax_ind ][ 0 ], &psSampleState[ RDmin_ind ][ 1 ], sizeof( NSQ_sample_struct ) ); + } + + /* Write samples from winner to output and long-term filter states */ + psDD = &psDelDec[ Winner_ind ]; + if( subfr > 0 || i >= decisionDelay ) { + pulses[ i - decisionDelay ] = (opus_int8)silk_RSHIFT_ROUND( psDD->Q_Q10[ last_smple_idx ], 10 ); + xq[ i - decisionDelay ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( + silk_SMULWW( psDD->Xq_Q14[ last_smple_idx ], delayedGain_Q10[ last_smple_idx ] ), 8 ) ); + NSQ->sLTP_shp_Q14[ NSQ->sLTP_shp_buf_idx - decisionDelay ] = psDD->Shape_Q14[ last_smple_idx ]; + sLTP_Q15[ NSQ->sLTP_buf_idx - decisionDelay ] = psDD->Pred_Q15[ last_smple_idx ]; + } + NSQ->sLTP_shp_buf_idx++; + NSQ->sLTP_buf_idx++; + + /* Update states */ + for( k = 0; k < nStatesDelayedDecision; k++ ) { + psDD = &psDelDec[ k ]; + psSS = &psSampleState[ k ][ 0 ]; + psDD->LF_AR_Q14 = psSS->LF_AR_Q14; + psDD->Diff_Q14 = psSS->Diff_Q14; + psDD->sLPC_Q14[ NSQ_LPC_BUF_LENGTH + i ] = psSS->xq_Q14; + psDD->Xq_Q14[ *smpl_buf_idx ] = psSS->xq_Q14; + psDD->Q_Q10[ *smpl_buf_idx ] = psSS->Q_Q10; + psDD->Pred_Q15[ *smpl_buf_idx ] = silk_LSHIFT32( psSS->LPC_exc_Q14, 1 ); + psDD->Shape_Q14[ *smpl_buf_idx ] = psSS->sLTP_shp_Q14; + psDD->Seed = silk_ADD32_ovflw( psDD->Seed, silk_RSHIFT_ROUND( psSS->Q_Q10, 10 ) ); + psDD->RandState[ *smpl_buf_idx ] = psDD->Seed; + psDD->RD_Q10 = psSS->RD_Q10; + } + delayedGain_Q10[ *smpl_buf_idx ] = Gain_Q10; + } + /* Update LPC states */ + for( k = 0; k < nStatesDelayedDecision; k++ ) { + psDD = &psDelDec[ k ]; + silk_memcpy( psDD->sLPC_Q14, &psDD->sLPC_Q14[ length ], NSQ_LPC_BUF_LENGTH * sizeof( opus_int32 ) ); + } + RESTORE_STACK; +} +#endif /* OVERRIDE_silk_noise_shape_quantizer_del_dec */ + +static OPUS_INLINE void silk_nsq_del_dec_scale_states( + const silk_encoder_state *psEncC, /* I Encoder State */ + silk_nsq_state *NSQ, /* I/O NSQ state */ + NSQ_del_dec_struct psDelDec[], /* I/O Delayed decision states */ + const opus_int16 x16[], /* I Input */ + opus_int32 x_sc_Q10[], /* O Input scaled with 1/Gain in Q10 */ + const opus_int16 sLTP[], /* I Re-whitened LTP state in Q0 */ + opus_int32 sLTP_Q15[], /* O LTP state matching scaled input */ + opus_int subfr, /* I Subframe number */ + opus_int nStatesDelayedDecision, /* I Number of del dec states */ + const opus_int LTP_scale_Q14, /* I LTP state scaling */ + const opus_int32 Gains_Q16[ MAX_NB_SUBFR ], /* I */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lag */ + const opus_int signal_type, /* I Signal type */ + const opus_int decisionDelay /* I Decision delay */ +) +{ + opus_int i, k, lag; + opus_int32 gain_adj_Q16, inv_gain_Q31, inv_gain_Q26; + NSQ_del_dec_struct *psDD; + + lag = pitchL[ subfr ]; + inv_gain_Q31 = silk_INVERSE32_varQ( silk_max( Gains_Q16[ subfr ], 1 ), 47 ); + silk_assert( inv_gain_Q31 != 0 ); + + /* Scale input */ + inv_gain_Q26 = silk_RSHIFT_ROUND( inv_gain_Q31, 5 ); + for( i = 0; i < psEncC->subfr_length; i++ ) { + x_sc_Q10[ i ] = silk_SMULWW( x16[ i ], inv_gain_Q26 ); + } + + /* After rewhitening the LTP state is un-scaled, so scale with inv_gain_Q16 */ + if( NSQ->rewhite_flag ) { + if( subfr == 0 ) { + /* Do LTP downscaling */ + inv_gain_Q31 = silk_LSHIFT( silk_SMULWB( inv_gain_Q31, LTP_scale_Q14 ), 2 ); + } + for( i = NSQ->sLTP_buf_idx - lag - LTP_ORDER / 2; i < NSQ->sLTP_buf_idx; i++ ) { + silk_assert( i < MAX_FRAME_LENGTH ); + sLTP_Q15[ i ] = silk_SMULWB( inv_gain_Q31, sLTP[ i ] ); + } + } + + /* Adjust for changing gain */ + if( Gains_Q16[ subfr ] != NSQ->prev_gain_Q16 ) { + gain_adj_Q16 = silk_DIV32_varQ( NSQ->prev_gain_Q16, Gains_Q16[ subfr ], 16 ); + + /* Scale long-term shaping state */ + for( i = NSQ->sLTP_shp_buf_idx - psEncC->ltp_mem_length; i < NSQ->sLTP_shp_buf_idx; i++ ) { + NSQ->sLTP_shp_Q14[ i ] = silk_SMULWW( gain_adj_Q16, NSQ->sLTP_shp_Q14[ i ] ); + } + + /* Scale long-term prediction state */ + if( signal_type == TYPE_VOICED && NSQ->rewhite_flag == 0 ) { + for( i = NSQ->sLTP_buf_idx - lag - LTP_ORDER / 2; i < NSQ->sLTP_buf_idx - decisionDelay; i++ ) { + sLTP_Q15[ i ] = silk_SMULWW( gain_adj_Q16, sLTP_Q15[ i ] ); + } + } + + for( k = 0; k < nStatesDelayedDecision; k++ ) { + psDD = &psDelDec[ k ]; + + /* Scale scalar states */ + psDD->LF_AR_Q14 = silk_SMULWW( gain_adj_Q16, psDD->LF_AR_Q14 ); + psDD->Diff_Q14 = silk_SMULWW( gain_adj_Q16, psDD->Diff_Q14 ); + + /* Scale short-term prediction and shaping states */ + for( i = 0; i < NSQ_LPC_BUF_LENGTH; i++ ) { + psDD->sLPC_Q14[ i ] = silk_SMULWW( gain_adj_Q16, psDD->sLPC_Q14[ i ] ); + } + for( i = 0; i < MAX_SHAPE_LPC_ORDER; i++ ) { + psDD->sAR2_Q14[ i ] = silk_SMULWW( gain_adj_Q16, psDD->sAR2_Q14[ i ] ); + } + for( i = 0; i < DECISION_DELAY; i++ ) { + psDD->Pred_Q15[ i ] = silk_SMULWW( gain_adj_Q16, psDD->Pred_Q15[ i ] ); + psDD->Shape_Q14[ i ] = silk_SMULWW( gain_adj_Q16, psDD->Shape_Q14[ i ] ); + } + } + + /* Save inverse gain */ + NSQ->prev_gain_Q16 = Gains_Q16[ subfr ]; + } +} diff --git a/port/third_party/opus/silk/PLC.c b/port/third_party/opus/silk/PLC.c new file mode 100644 index 00000000..b35bf750 --- /dev/null +++ b/port/third_party/opus/silk/PLC.c @@ -0,0 +1,493 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" +#include "PLC.h" + +#ifdef ENABLE_DEEP_PLC +#include "lpcnet.h" +#endif + +#define NB_ATT 2 +static const opus_int16 HARM_ATT_Q15[NB_ATT] = { 32440, 31130 }; /* 0.99, 0.95 */ +static const opus_int16 PLC_RAND_ATTENUATE_V_Q15[NB_ATT] = { 31130, 26214 }; /* 0.95, 0.8 */ +static const opus_int16 PLC_RAND_ATTENUATE_UV_Q15[NB_ATT] = { 32440, 29491 }; /* 0.99, 0.9 */ + +static OPUS_INLINE void silk_PLC_update( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl /* I/O Decoder control */ +); + +static OPUS_INLINE void silk_PLC_conceal( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl, /* I/O Decoder control */ + opus_int16 frame[], /* O LPC residual signal */ +#ifdef ENABLE_DEEP_PLC + LPCNetPLCState *lpcnet, +#endif + int arch /* I Run-time architecture */ +); + + +void silk_PLC_Reset( + silk_decoder_state *psDec /* I/O Decoder state */ +) +{ + psDec->sPLC.pitchL_Q8 = silk_LSHIFT( psDec->frame_length, 8 - 1 ); + psDec->sPLC.prevGain_Q16[ 0 ] = SILK_FIX_CONST( 1, 16 ); + psDec->sPLC.prevGain_Q16[ 1 ] = SILK_FIX_CONST( 1, 16 ); + psDec->sPLC.subfr_length = 20; + psDec->sPLC.nb_subfr = 2; +} + +void silk_PLC( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl, /* I/O Decoder control */ + opus_int16 frame[], /* I/O signal */ + opus_int lost, /* I Loss flag */ +#ifdef ENABLE_DEEP_PLC + LPCNetPLCState *lpcnet, +#endif + int arch /* I Run-time architecture */ +) +{ + /* PLC control function */ + if( psDec->fs_kHz != psDec->sPLC.fs_kHz ) { + silk_PLC_Reset( psDec ); + psDec->sPLC.fs_kHz = psDec->fs_kHz; + } + + if( lost ) { + /****************************/ + /* Generate Signal */ + /****************************/ + silk_PLC_conceal( psDec, psDecCtrl, frame, +#ifdef ENABLE_DEEP_PLC + lpcnet, +#endif + arch ); + + psDec->lossCnt++; + } else { + /****************************/ + /* Update state */ + /****************************/ + silk_PLC_update( psDec, psDecCtrl ); +#ifdef ENABLE_DEEP_PLC + if ( lpcnet != NULL && psDec->sPLC.fs_kHz == 16 ) { + int k; + for( k = 0; k < psDec->nb_subfr; k += 2 ) { + lpcnet_plc_update( lpcnet, frame + k * psDec->subfr_length ); + } + } +#endif + } +} + +/**************************************************/ +/* Update state of PLC */ +/**************************************************/ +static OPUS_INLINE void silk_PLC_update( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl /* I/O Decoder control */ +) +{ + opus_int32 LTP_Gain_Q14, temp_LTP_Gain_Q14; + opus_int i, j; + silk_PLC_struct *psPLC; + + psPLC = &psDec->sPLC; + + /* Update parameters used in case of packet loss */ + psDec->prevSignalType = psDec->indices.signalType; + LTP_Gain_Q14 = 0; + if( psDec->indices.signalType == TYPE_VOICED ) { + /* Find the parameters for the last subframe which contains a pitch pulse */ + for( j = 0; j * psDec->subfr_length < psDecCtrl->pitchL[ psDec->nb_subfr - 1 ]; j++ ) { + if( j == psDec->nb_subfr ) { + break; + } + temp_LTP_Gain_Q14 = 0; + for( i = 0; i < LTP_ORDER; i++ ) { + temp_LTP_Gain_Q14 += psDecCtrl->LTPCoef_Q14[ ( psDec->nb_subfr - 1 - j ) * LTP_ORDER + i ]; + } + if( temp_LTP_Gain_Q14 > LTP_Gain_Q14 ) { + LTP_Gain_Q14 = temp_LTP_Gain_Q14; + silk_memcpy( psPLC->LTPCoef_Q14, + &psDecCtrl->LTPCoef_Q14[ silk_SMULBB( psDec->nb_subfr - 1 - j, LTP_ORDER ) ], + LTP_ORDER * sizeof( opus_int16 ) ); + + psPLC->pitchL_Q8 = silk_LSHIFT( psDecCtrl->pitchL[ psDec->nb_subfr - 1 - j ], 8 ); + } + } + + silk_memset( psPLC->LTPCoef_Q14, 0, LTP_ORDER * sizeof( opus_int16 ) ); + psPLC->LTPCoef_Q14[ LTP_ORDER / 2 ] = LTP_Gain_Q14; + + /* Limit LT coefs */ + if( LTP_Gain_Q14 < V_PITCH_GAIN_START_MIN_Q14 ) { + opus_int scale_Q10; + opus_int32 tmp; + + tmp = silk_LSHIFT( V_PITCH_GAIN_START_MIN_Q14, 10 ); + scale_Q10 = silk_DIV32( tmp, silk_max( LTP_Gain_Q14, 1 ) ); + for( i = 0; i < LTP_ORDER; i++ ) { + psPLC->LTPCoef_Q14[ i ] = silk_RSHIFT( silk_SMULBB( psPLC->LTPCoef_Q14[ i ], scale_Q10 ), 10 ); + } + } else if( LTP_Gain_Q14 > V_PITCH_GAIN_START_MAX_Q14 ) { + opus_int scale_Q14; + opus_int32 tmp; + + tmp = silk_LSHIFT( V_PITCH_GAIN_START_MAX_Q14, 14 ); + scale_Q14 = silk_DIV32( tmp, silk_max( LTP_Gain_Q14, 1 ) ); + for( i = 0; i < LTP_ORDER; i++ ) { + psPLC->LTPCoef_Q14[ i ] = silk_RSHIFT( silk_SMULBB( psPLC->LTPCoef_Q14[ i ], scale_Q14 ), 14 ); + } + } + } else { + psPLC->pitchL_Q8 = silk_LSHIFT( silk_SMULBB( psDec->fs_kHz, 18 ), 8 ); + silk_memset( psPLC->LTPCoef_Q14, 0, LTP_ORDER * sizeof( opus_int16 )); + } + + /* Save LPC coeficients */ + silk_memcpy( psPLC->prevLPC_Q12, psDecCtrl->PredCoef_Q12[ 1 ], psDec->LPC_order * sizeof( opus_int16 ) ); + psPLC->prevLTP_scale_Q14 = psDecCtrl->LTP_scale_Q14; + + /* Save last two gains */ + silk_memcpy( psPLC->prevGain_Q16, &psDecCtrl->Gains_Q16[ psDec->nb_subfr - 2 ], 2 * sizeof( opus_int32 ) ); + + psPLC->subfr_length = psDec->subfr_length; + psPLC->nb_subfr = psDec->nb_subfr; +} + +static OPUS_INLINE void silk_PLC_energy(opus_int32 *energy1, opus_int *shift1, opus_int32 *energy2, opus_int *shift2, + const opus_int32 *exc_Q14, const opus_int32 *prevGain_Q10, int subfr_length, int nb_subfr) +{ + int i, k; + VARDECL( opus_int16, exc_buf ); + opus_int16 *exc_buf_ptr; + SAVE_STACK; + ALLOC( exc_buf, 2*subfr_length, opus_int16 ); + /* Find random noise component */ + /* Scale previous excitation signal */ + exc_buf_ptr = exc_buf; + for( k = 0; k < 2; k++ ) { + for( i = 0; i < subfr_length; i++ ) { + exc_buf_ptr[ i ] = (opus_int16)silk_SAT16( silk_RSHIFT( + silk_SMULWW( exc_Q14[ i + ( k + nb_subfr - 2 ) * subfr_length ], prevGain_Q10[ k ] ), 8 ) ); + } + exc_buf_ptr += subfr_length; + } + /* Find the subframe with lowest energy of the last two and use that as random noise generator */ + silk_sum_sqr_shift( energy1, shift1, exc_buf, subfr_length ); + silk_sum_sqr_shift( energy2, shift2, &exc_buf[ subfr_length ], subfr_length ); + RESTORE_STACK; +} + +static OPUS_INLINE void silk_PLC_conceal( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl, /* I/O Decoder control */ + opus_int16 frame[], /* O LPC residual signal */ +#ifdef ENABLE_DEEP_PLC + LPCNetPLCState *lpcnet, +#endif + int arch /* I Run-time architecture */ +) +{ + opus_int i, j, k; + opus_int lag, idx, sLTP_buf_idx, shift1, shift2; + opus_int32 rand_seed, harm_Gain_Q15, rand_Gain_Q15, inv_gain_Q30; + opus_int32 energy1, energy2, *rand_ptr, *pred_lag_ptr; + opus_int32 LPC_pred_Q10, LTP_pred_Q12; + opus_int16 rand_scale_Q14; + opus_int16 *B_Q14; + opus_int32 *sLPC_Q14_ptr; + opus_int16 A_Q12[ MAX_LPC_ORDER ]; +#ifdef SMALL_FOOTPRINT + opus_int16 *sLTP; +#else + VARDECL( opus_int16, sLTP ); +#endif + VARDECL( opus_int32, sLTP_Q14 ); + silk_PLC_struct *psPLC = &psDec->sPLC; + opus_int32 prevGain_Q10[2]; + SAVE_STACK; + + ALLOC( sLTP_Q14, psDec->ltp_mem_length + psDec->frame_length, opus_int32 ); +#ifdef SMALL_FOOTPRINT + /* Ugly hack that breaks aliasing rules to save stack: put sLTP at the very end of sLTP_Q14. */ + sLTP = ((opus_int16*)&sLTP_Q14[psDec->ltp_mem_length + psDec->frame_length])-psDec->ltp_mem_length; +#else + ALLOC( sLTP, psDec->ltp_mem_length, opus_int16 ); +#endif + + prevGain_Q10[0] = silk_RSHIFT( psPLC->prevGain_Q16[ 0 ], 6); + prevGain_Q10[1] = silk_RSHIFT( psPLC->prevGain_Q16[ 1 ], 6); + + if( psDec->first_frame_after_reset ) { + silk_memset( psPLC->prevLPC_Q12, 0, sizeof( psPLC->prevLPC_Q12 ) ); + } + + silk_PLC_energy(&energy1, &shift1, &energy2, &shift2, psDec->exc_Q14, prevGain_Q10, psDec->subfr_length, psDec->nb_subfr); + + if( silk_RSHIFT( energy1, shift2 ) < silk_RSHIFT( energy2, shift1 ) ) { + /* First sub-frame has lowest energy */ + rand_ptr = &psDec->exc_Q14[ silk_max_int( 0, ( psPLC->nb_subfr - 1 ) * psPLC->subfr_length - RAND_BUF_SIZE ) ]; + } else { + /* Second sub-frame has lowest energy */ + rand_ptr = &psDec->exc_Q14[ silk_max_int( 0, psPLC->nb_subfr * psPLC->subfr_length - RAND_BUF_SIZE ) ]; + } + + /* Set up Gain to random noise component */ + B_Q14 = psPLC->LTPCoef_Q14; + rand_scale_Q14 = psPLC->randScale_Q14; + + /* Set up attenuation gains */ + harm_Gain_Q15 = HARM_ATT_Q15[ silk_min_int( NB_ATT - 1, psDec->lossCnt ) ]; + if( psDec->prevSignalType == TYPE_VOICED ) { + rand_Gain_Q15 = PLC_RAND_ATTENUATE_V_Q15[ silk_min_int( NB_ATT - 1, psDec->lossCnt ) ]; + } else { + rand_Gain_Q15 = PLC_RAND_ATTENUATE_UV_Q15[ silk_min_int( NB_ATT - 1, psDec->lossCnt ) ]; + } + + /* LPC concealment. Apply BWE to previous LPC */ + silk_bwexpander( psPLC->prevLPC_Q12, psDec->LPC_order, SILK_FIX_CONST( BWE_COEF, 16 ) ); + + /* Preload LPC coeficients to array on stack. Gives small performance gain */ + silk_memcpy( A_Q12, psPLC->prevLPC_Q12, psDec->LPC_order * sizeof( opus_int16 ) ); + + /* First Lost frame */ + if( psDec->lossCnt == 0 ) { + rand_scale_Q14 = 1 << 14; + + /* Reduce random noise Gain for voiced frames */ + if( psDec->prevSignalType == TYPE_VOICED ) { + for( i = 0; i < LTP_ORDER; i++ ) { + rand_scale_Q14 -= B_Q14[ i ]; + } + rand_scale_Q14 = silk_max_16( 3277, rand_scale_Q14 ); /* 0.2 */ + rand_scale_Q14 = (opus_int16)silk_RSHIFT( silk_SMULBB( rand_scale_Q14, psPLC->prevLTP_scale_Q14 ), 14 ); + } else { + /* Reduce random noise for unvoiced frames with high LPC gain */ + opus_int32 invGain_Q30, down_scale_Q30; + + invGain_Q30 = silk_LPC_inverse_pred_gain( psPLC->prevLPC_Q12, psDec->LPC_order, arch ); + + down_scale_Q30 = silk_min_32( silk_RSHIFT( (opus_int32)1 << 30, LOG2_INV_LPC_GAIN_HIGH_THRES ), invGain_Q30 ); + down_scale_Q30 = silk_max_32( silk_RSHIFT( (opus_int32)1 << 30, LOG2_INV_LPC_GAIN_LOW_THRES ), down_scale_Q30 ); + down_scale_Q30 = silk_LSHIFT( down_scale_Q30, LOG2_INV_LPC_GAIN_HIGH_THRES ); + + rand_Gain_Q15 = silk_RSHIFT( silk_SMULWB( down_scale_Q30, rand_Gain_Q15 ), 14 ); + } + } + + rand_seed = psPLC->rand_seed; + lag = silk_RSHIFT_ROUND( psPLC->pitchL_Q8, 8 ); + sLTP_buf_idx = psDec->ltp_mem_length; + + /* Rewhiten LTP state */ + idx = psDec->ltp_mem_length - lag - psDec->LPC_order - LTP_ORDER / 2; + celt_assert( idx > 0 ); + silk_LPC_analysis_filter( &sLTP[ idx ], &psDec->outBuf[ idx ], A_Q12, psDec->ltp_mem_length - idx, psDec->LPC_order, arch ); + /* Scale LTP state */ + inv_gain_Q30 = silk_INVERSE32_varQ( psPLC->prevGain_Q16[ 1 ], 46 ); + inv_gain_Q30 = silk_min( inv_gain_Q30, silk_int32_MAX >> 1 ); + for( i = idx + psDec->LPC_order; i < psDec->ltp_mem_length; i++ ) { + sLTP_Q14[ i ] = silk_SMULWB( inv_gain_Q30, sLTP[ i ] ); + } + + /***************************/ + /* LTP synthesis filtering */ + /***************************/ + for( k = 0; k < psDec->nb_subfr; k++ ) { + /* Set up pointer */ + pred_lag_ptr = &sLTP_Q14[ sLTP_buf_idx - lag + LTP_ORDER / 2 ]; + for( i = 0; i < psDec->subfr_length; i++ ) { + /* Unrolled loop */ + /* Avoids introducing a bias because silk_SMLAWB() always rounds to -inf */ + LTP_pred_Q12 = 2; + LTP_pred_Q12 = silk_SMLAWB( LTP_pred_Q12, pred_lag_ptr[ 0 ], B_Q14[ 0 ] ); + LTP_pred_Q12 = silk_SMLAWB( LTP_pred_Q12, pred_lag_ptr[ -1 ], B_Q14[ 1 ] ); + LTP_pred_Q12 = silk_SMLAWB( LTP_pred_Q12, pred_lag_ptr[ -2 ], B_Q14[ 2 ] ); + LTP_pred_Q12 = silk_SMLAWB( LTP_pred_Q12, pred_lag_ptr[ -3 ], B_Q14[ 3 ] ); + LTP_pred_Q12 = silk_SMLAWB( LTP_pred_Q12, pred_lag_ptr[ -4 ], B_Q14[ 4 ] ); + pred_lag_ptr++; + + /* Generate LPC excitation */ + rand_seed = silk_RAND( rand_seed ); + idx = silk_RSHIFT( rand_seed, 25 ) & RAND_BUF_MASK; + sLTP_Q14[ sLTP_buf_idx ] = silk_LSHIFT32( silk_SMLAWB( LTP_pred_Q12, rand_ptr[ idx ], rand_scale_Q14 ), 2 ); + sLTP_buf_idx++; + } + + /* Gradually reduce LTP gain */ + for( j = 0; j < LTP_ORDER; j++ ) { + B_Q14[ j ] = silk_RSHIFT( silk_SMULBB( harm_Gain_Q15, B_Q14[ j ] ), 15 ); + } + /* Gradually reduce excitation gain */ + rand_scale_Q14 = silk_RSHIFT( silk_SMULBB( rand_scale_Q14, rand_Gain_Q15 ), 15 ); + + /* Slowly increase pitch lag */ + psPLC->pitchL_Q8 = silk_SMLAWB( psPLC->pitchL_Q8, psPLC->pitchL_Q8, PITCH_DRIFT_FAC_Q16 ); + psPLC->pitchL_Q8 = silk_min_32( psPLC->pitchL_Q8, silk_LSHIFT( silk_SMULBB( MAX_PITCH_LAG_MS, psDec->fs_kHz ), 8 ) ); + lag = silk_RSHIFT_ROUND( psPLC->pitchL_Q8, 8 ); + } + + /***************************/ + /* LPC synthesis filtering */ + /***************************/ + sLPC_Q14_ptr = &sLTP_Q14[ psDec->ltp_mem_length - MAX_LPC_ORDER ]; + + /* Copy LPC state */ + silk_memcpy( sLPC_Q14_ptr, psDec->sLPC_Q14_buf, MAX_LPC_ORDER * sizeof( opus_int32 ) ); + + celt_assert( psDec->LPC_order >= 10 ); /* check that unrolling works */ + for( i = 0; i < psDec->frame_length; i++ ) { + /* partly unrolled */ + /* Avoids introducing a bias because silk_SMLAWB() always rounds to -inf */ + LPC_pred_Q10 = silk_RSHIFT( psDec->LPC_order, 1 ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 1 ], A_Q12[ 0 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 2 ], A_Q12[ 1 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 3 ], A_Q12[ 2 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 4 ], A_Q12[ 3 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 5 ], A_Q12[ 4 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 6 ], A_Q12[ 5 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 7 ], A_Q12[ 6 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 8 ], A_Q12[ 7 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 9 ], A_Q12[ 8 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - 10 ], A_Q12[ 9 ] ); + for( j = 10; j < psDec->LPC_order; j++ ) { + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14_ptr[ MAX_LPC_ORDER + i - j - 1 ], A_Q12[ j ] ); + } + + /* Add prediction to LPC excitation */ + sLPC_Q14_ptr[ MAX_LPC_ORDER + i ] = silk_ADD_SAT32( sLPC_Q14_ptr[ MAX_LPC_ORDER + i ], + silk_LSHIFT_SAT32( LPC_pred_Q10, 4 )); + + /* Scale with Gain */ + frame[ i ] = (opus_int16)silk_SAT16( silk_SAT16( silk_RSHIFT_ROUND( silk_SMULWW( sLPC_Q14_ptr[ MAX_LPC_ORDER + i ], prevGain_Q10[ 1 ] ), 8 ) ) ); + } +#ifdef ENABLE_DEEP_PLC + if ( lpcnet != NULL && lpcnet->loaded && psDec->sPLC.fs_kHz == 16 ) { + int run_deep_plc = psDec->sPLC.enable_deep_plc || lpcnet->fec_fill_pos != 0; + if( run_deep_plc ) { + for( k = 0; k < psDec->nb_subfr; k += 2 ) { + lpcnet_plc_conceal( lpcnet, frame + k * psDec->subfr_length ); + } + /* We *should* be able to copy only from psDec->frame_length-MAX_LPC_ORDER, i.e. the last MAX_LPC_ORDER samples. */ + for( i = 0; i < psDec->frame_length; i++ ) { + sLPC_Q14_ptr[ MAX_LPC_ORDER + i ] = (int)floor(.5 + frame[ i ] * (float)(1 << 24) / prevGain_Q10[ 1 ] ); + } + } else { + for( k = 0; k < psDec->nb_subfr; k += 2 ) { + lpcnet_plc_update( lpcnet, frame + k * psDec->subfr_length ); + } + } + } +#endif + + /* Save LPC state */ + silk_memcpy( psDec->sLPC_Q14_buf, &sLPC_Q14_ptr[ psDec->frame_length ], MAX_LPC_ORDER * sizeof( opus_int32 ) ); + + /**************************************/ + /* Update states */ + /**************************************/ + psPLC->rand_seed = rand_seed; + psPLC->randScale_Q14 = rand_scale_Q14; + for( i = 0; i < MAX_NB_SUBFR; i++ ) { + psDecCtrl->pitchL[ i ] = lag; + } + RESTORE_STACK; +} + +/* Glues concealed frames with new good received frames */ +void silk_PLC_glue_frames( + silk_decoder_state *psDec, /* I/O decoder state */ + opus_int16 frame[], /* I/O signal */ + opus_int length /* I length of signal */ +) +{ + opus_int i, energy_shift; + opus_int32 energy; + silk_PLC_struct *psPLC; + psPLC = &psDec->sPLC; + + if( psDec->lossCnt ) { + /* Calculate energy in concealed residual */ + silk_sum_sqr_shift( &psPLC->conc_energy, &psPLC->conc_energy_shift, frame, length ); + + psPLC->last_frame_lost = 1; + } else { + if( psDec->sPLC.last_frame_lost ) { + /* Calculate residual in decoded signal if last frame was lost */ + silk_sum_sqr_shift( &energy, &energy_shift, frame, length ); + + /* Normalize energies */ + if( energy_shift > psPLC->conc_energy_shift ) { + psPLC->conc_energy = silk_RSHIFT( psPLC->conc_energy, energy_shift - psPLC->conc_energy_shift ); + } else if( energy_shift < psPLC->conc_energy_shift ) { + energy = silk_RSHIFT( energy, psPLC->conc_energy_shift - energy_shift ); + } + + /* Fade in the energy difference */ + if( energy > psPLC->conc_energy ) { + opus_int32 frac_Q24, LZ; + opus_int32 gain_Q16, slope_Q16; + + LZ = silk_CLZ32( psPLC->conc_energy ); + LZ = LZ - 1; + psPLC->conc_energy = silk_LSHIFT( psPLC->conc_energy, LZ ); + energy = silk_RSHIFT( energy, silk_max_32( 24 - LZ, 0 ) ); + + frac_Q24 = silk_DIV32( psPLC->conc_energy, silk_max( energy, 1 ) ); + + gain_Q16 = silk_LSHIFT( silk_SQRT_APPROX( frac_Q24 ), 4 ); + slope_Q16 = silk_DIV32_16( ( (opus_int32)1 << 16 ) - gain_Q16, length ); + /* Make slope 4x steeper to avoid missing onsets after DTX */ + slope_Q16 = silk_LSHIFT( slope_Q16, 2 ); +#ifdef ENABLE_DEEP_PLC + if ( psDec->sPLC.fs_kHz != 16 ) +#endif + { + for( i = 0; i < length; i++ ) { + frame[ i ] = silk_SMULWB( gain_Q16, frame[ i ] ); + gain_Q16 += slope_Q16; + if( gain_Q16 > (opus_int32)1 << 16 ) { + break; + } + } + } + } + } + psPLC->last_frame_lost = 0; + } +} diff --git a/port/third_party/opus/silk/PLC.h b/port/third_party/opus/silk/PLC.h new file mode 100644 index 00000000..1bebb786 --- /dev/null +++ b/port/third_party/opus/silk/PLC.h @@ -0,0 +1,65 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_PLC_H +#define SILK_PLC_H + +#include "main.h" + +#define BWE_COEF 0.99 +#define V_PITCH_GAIN_START_MIN_Q14 11469 /* 0.7 in Q14 */ +#define V_PITCH_GAIN_START_MAX_Q14 15565 /* 0.95 in Q14 */ +#define MAX_PITCH_LAG_MS 18 +#define RAND_BUF_SIZE 128 +#define RAND_BUF_MASK ( RAND_BUF_SIZE - 1 ) +#define LOG2_INV_LPC_GAIN_HIGH_THRES 3 /* 2^3 = 8 dB LPC gain */ +#define LOG2_INV_LPC_GAIN_LOW_THRES 8 /* 2^8 = 24 dB LPC gain */ +#define PITCH_DRIFT_FAC_Q16 655 /* 0.01 in Q16 */ + +void silk_PLC_Reset( + silk_decoder_state *psDec /* I/O Decoder state */ +); + +void silk_PLC( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl, /* I/O Decoder control */ + opus_int16 frame[], /* I/O signal */ + opus_int lost, /* I Loss flag */ +#ifdef ENABLE_DEEP_PLC + LPCNetPLCState *lpcnet, +#endif + int arch /* I Run-time architecture */ +); + +void silk_PLC_glue_frames( + silk_decoder_state *psDec, /* I/O decoder state */ + opus_int16 frame[], /* I/O signal */ + opus_int length /* I length of signal */ +); + +#endif + diff --git a/port/third_party/opus/silk/SigProc_FIX.h b/port/third_party/opus/silk/SigProc_FIX.h new file mode 100644 index 00000000..fbdfa82e --- /dev/null +++ b/port/third_party/opus/silk/SigProc_FIX.h @@ -0,0 +1,643 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_SIGPROC_FIX_H +#define SILK_SIGPROC_FIX_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +/*#define silk_MACRO_COUNT */ /* Used to enable WMOPS counting */ + +#define SILK_MAX_ORDER_LPC 24 /* max order of the LPC analysis in schur() and k2a() */ + +#include /* for memset(), memcpy(), memmove() */ +#include "typedef.h" +#include "resampler_structs.h" +#include "macros.h" +#include "cpu_support.h" + +#if defined(OPUS_X86_MAY_HAVE_SSE4_1) +#include "x86/SigProc_FIX_sse.h" +#endif + +#if (defined(OPUS_ARM_ASM) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)) +#include "arm/biquad_alt_arm.h" +#include "arm/LPC_inv_pred_gain_arm.h" +#endif + +/********************************************************************/ +/* SIGNAL PROCESSING FUNCTIONS */ +/********************************************************************/ + +/*! + * Initialize/reset the resampler state for a given pair of input/output sampling rates +*/ +opus_int silk_resampler_init( + silk_resampler_state_struct *S, /* I/O Resampler state */ + opus_int32 Fs_Hz_in, /* I Input sampling rate (Hz) */ + opus_int32 Fs_Hz_out, /* I Output sampling rate (Hz) */ + opus_int forEnc /* I If 1: encoder; if 0: decoder */ +); + +/*! + * Resampler: convert from one sampling rate to another + */ +opus_int silk_resampler( + silk_resampler_state_struct *S, /* I/O Resampler state */ + opus_int16 out[], /* O Output signal */ + const opus_int16 in[], /* I Input signal */ + opus_int32 inLen /* I Number of input samples */ +); + +/*! +* Downsample 2x, mediocre quality +*/ +void silk_resampler_down2( + opus_int32 *S, /* I/O State vector [ 2 ] */ + opus_int16 *out, /* O Output signal [ len ] */ + const opus_int16 *in, /* I Input signal [ floor(len/2) ] */ + opus_int32 inLen /* I Number of input samples */ +); + +/*! + * Downsample by a factor 2/3, low quality +*/ +void silk_resampler_down2_3( + opus_int32 *S, /* I/O State vector [ 6 ] */ + opus_int16 *out, /* O Output signal [ floor(2*inLen/3) ] */ + const opus_int16 *in, /* I Input signal [ inLen ] */ + opus_int32 inLen /* I Number of input samples */ +); + +/*! + * second order ARMA filter; + * slower than biquad() but uses more precise coefficients + * can handle (slowly) varying coefficients + */ +void silk_biquad_alt_stride1( + const opus_int16 *in, /* I input signal */ + const opus_int32 *B_Q28, /* I MA coefficients [3] */ + const opus_int32 *A_Q28, /* I AR coefficients [2] */ + opus_int32 *S, /* I/O State vector [2] */ + opus_int16 *out, /* O output signal */ + const opus_int32 len /* I signal length (must be even) */ +); + +void silk_biquad_alt_stride2_c( + const opus_int16 *in, /* I input signal */ + const opus_int32 *B_Q28, /* I MA coefficients [3] */ + const opus_int32 *A_Q28, /* I AR coefficients [2] */ + opus_int32 *S, /* I/O State vector [4] */ + opus_int16 *out, /* O output signal */ + const opus_int32 len /* I signal length (must be even) */ +); + +/* Variable order MA prediction error filter. */ +void silk_LPC_analysis_filter( + opus_int16 *out, /* O Output signal */ + const opus_int16 *in, /* I Input signal */ + const opus_int16 *B, /* I MA prediction coefficients, Q12 [order] */ + const opus_int32 len, /* I Signal length */ + const opus_int32 d, /* I Filter order */ + int arch /* I Run-time architecture */ +); + +/* Chirp (bandwidth expand) LP AR filter */ +void silk_bwexpander( + opus_int16 *ar, /* I/O AR filter to be expanded (without leading 1) */ + const opus_int d, /* I Length of ar */ + opus_int32 chirp_Q16 /* I Chirp factor (typically in the range 0 to 1) */ +); + +/* Chirp (bandwidth expand) LP AR filter */ +void silk_bwexpander_32( + opus_int32 *ar, /* I/O AR filter to be expanded (without leading 1) */ + const opus_int d, /* I Length of ar */ + opus_int32 chirp_Q16 /* I Chirp factor in Q16 */ +); + +/* Compute inverse of LPC prediction gain, and */ +/* test if LPC coefficients are stable (all poles within unit circle) */ +opus_int32 silk_LPC_inverse_pred_gain_c( /* O Returns inverse prediction gain in energy domain, Q30 */ + const opus_int16 *A_Q12, /* I Prediction coefficients, Q12 [order] */ + const opus_int order /* I Prediction order */ +); + +/* Split signal in two decimated bands using first-order allpass filters */ +void silk_ana_filt_bank_1( + const opus_int16 *in, /* I Input signal [N] */ + opus_int32 *S, /* I/O State vector [2] */ + opus_int16 *outL, /* O Low band [N/2] */ + opus_int16 *outH, /* O High band [N/2] */ + const opus_int32 N /* I Number of input samples */ +); + +#if !defined(OVERRIDE_silk_biquad_alt_stride2) +#define silk_biquad_alt_stride2(in, B_Q28, A_Q28, S, out, len, arch) ((void)(arch), silk_biquad_alt_stride2_c(in, B_Q28, A_Q28, S, out, len)) +#endif + +#if !defined(OVERRIDE_silk_LPC_inverse_pred_gain) +#define silk_LPC_inverse_pred_gain(A_Q12, order, arch) ((void)(arch), silk_LPC_inverse_pred_gain_c(A_Q12, order)) +#endif + +/********************************************************************/ +/* SCALAR FUNCTIONS */ +/********************************************************************/ + +/* Approximation of 128 * log2() (exact inverse of approx 2^() below) */ +/* Convert input to a log scale */ +opus_int32 silk_lin2log( + const opus_int32 inLin /* I input in linear scale */ +); + +/* Approximation of a sigmoid function */ +opus_int silk_sigm_Q15( + opus_int in_Q5 /* I */ +); + +/* Approximation of 2^() (exact inverse of approx log2() above) */ +/* Convert input to a linear scale */ +opus_int32 silk_log2lin( + const opus_int32 inLog_Q7 /* I input on log scale */ +); + +/* Compute number of bits to right shift the sum of squares of a vector */ +/* of int16s to make it fit in an int32 */ +void silk_sum_sqr_shift( + opus_int32 *energy, /* O Energy of x, after shifting to the right */ + opus_int *shift, /* O Number of bits right shift applied to energy */ + const opus_int16 *x, /* I Input vector */ + opus_int len /* I Length of input vector */ +); + +/* Calculates the reflection coefficients from the correlation sequence */ +/* Faster than schur64(), but much less accurate. */ +/* uses SMLAWB(), requiring armv5E and higher. */ +opus_int32 silk_schur( /* O Returns residual energy */ + opus_int16 *rc_Q15, /* O reflection coefficients [order] Q15 */ + const opus_int32 *c, /* I correlations [order+1] */ + const opus_int32 order /* I prediction order */ +); + +/* Calculates the reflection coefficients from the correlation sequence */ +/* Slower than schur(), but more accurate. */ +/* Uses SMULL(), available on armv4 */ +opus_int32 silk_schur64( /* O returns residual energy */ + opus_int32 rc_Q16[], /* O Reflection coefficients [order] Q16 */ + const opus_int32 c[], /* I Correlations [order+1] */ + opus_int32 order /* I Prediction order */ +); + +/* Step up function, converts reflection coefficients to prediction coefficients */ +void silk_k2a( + opus_int32 *A_Q24, /* O Prediction coefficients [order] Q24 */ + const opus_int16 *rc_Q15, /* I Reflection coefficients [order] Q15 */ + const opus_int32 order /* I Prediction order */ +); + +/* Step up function, converts reflection coefficients to prediction coefficients */ +void silk_k2a_Q16( + opus_int32 *A_Q24, /* O Prediction coefficients [order] Q24 */ + const opus_int32 *rc_Q16, /* I Reflection coefficients [order] Q16 */ + const opus_int32 order /* I Prediction order */ +); + +/* Apply sine window to signal vector. */ +/* Window types: */ +/* 1 -> sine window from 0 to pi/2 */ +/* 2 -> sine window from pi/2 to pi */ +/* every other sample of window is linearly interpolated, for speed */ +void silk_apply_sine_window( + opus_int16 px_win[], /* O Pointer to windowed signal */ + const opus_int16 px[], /* I Pointer to input signal */ + const opus_int win_type, /* I Selects a window type */ + const opus_int length /* I Window length, multiple of 4 */ +); + +/* Compute autocorrelation */ +void silk_autocorr( + opus_int32 *results, /* O Result (length correlationCount) */ + opus_int *scale, /* O Scaling of the correlation vector */ + const opus_int16 *inputData, /* I Input data to correlate */ + const opus_int inputDataSize, /* I Length of input */ + const opus_int correlationCount, /* I Number of correlation taps to compute */ + int arch /* I Run-time architecture */ +); + +void silk_decode_pitch( + opus_int16 lagIndex, /* I */ + opus_int8 contourIndex, /* O */ + opus_int pitch_lags[], /* O 4 pitch values */ + const opus_int Fs_kHz, /* I sampling frequency (kHz) */ + const opus_int nb_subfr /* I number of sub frames */ +); + +opus_int silk_pitch_analysis_core( /* O Voicing estimate: 0 voiced, 1 unvoiced */ + const opus_int16 *frame, /* I Signal of length PE_FRAME_LENGTH_MS*Fs_kHz */ + opus_int *pitch_out, /* O 4 pitch lag values */ + opus_int16 *lagIndex, /* O Lag Index */ + opus_int8 *contourIndex, /* O Pitch contour Index */ + opus_int *LTPCorr_Q15, /* I/O Normalized correlation; input: value from previous frame */ + opus_int prevLag, /* I Last lag of previous frame; set to zero is unvoiced */ + const opus_int32 search_thres1_Q16, /* I First stage threshold for lag candidates 0 - 1 */ + const opus_int search_thres2_Q13, /* I Final threshold for lag candidates 0 - 1 */ + const opus_int Fs_kHz, /* I Sample frequency (kHz) */ + const opus_int complexity, /* I Complexity setting, 0-2, where 2 is highest */ + const opus_int nb_subfr, /* I number of 5 ms subframes */ + int arch /* I Run-time architecture */ +); + +/* Compute Normalized Line Spectral Frequencies (NLSFs) from whitening filter coefficients */ +/* If not all roots are found, the a_Q16 coefficients are bandwidth expanded until convergence. */ +void silk_A2NLSF( + opus_int16 *NLSF, /* O Normalized Line Spectral Frequencies in Q15 (0..2^15-1) [d] */ + opus_int32 *a_Q16, /* I/O Monic whitening filter coefficients in Q16 [d] */ + const opus_int d /* I Filter order (must be even) */ +); + +/* compute whitening filter coefficients from normalized line spectral frequencies */ +void silk_NLSF2A( + opus_int16 *a_Q12, /* O monic whitening filter coefficients in Q12, [ d ] */ + const opus_int16 *NLSF, /* I normalized line spectral frequencies in Q15, [ d ] */ + const opus_int d, /* I filter order (should be even) */ + int arch /* I Run-time architecture */ +); + +/* Convert int32 coefficients to int16 coefs and make sure there's no wrap-around */ +void silk_LPC_fit( + opus_int16 *a_QOUT, /* O Output signal */ + opus_int32 *a_QIN, /* I/O Input signal */ + const opus_int QOUT, /* I Input Q domain */ + const opus_int QIN, /* I Input Q domain */ + const opus_int d /* I Filter order */ +); + +void silk_insertion_sort_increasing( + opus_int32 *a, /* I/O Unsorted / Sorted vector */ + opus_int *idx, /* O Index vector for the sorted elements */ + const opus_int L, /* I Vector length */ + const opus_int K /* I Number of correctly sorted positions */ +); + +void silk_insertion_sort_decreasing_int16( + opus_int16 *a, /* I/O Unsorted / Sorted vector */ + opus_int *idx, /* O Index vector for the sorted elements */ + const opus_int L, /* I Vector length */ + const opus_int K /* I Number of correctly sorted positions */ +); + +void silk_insertion_sort_increasing_all_values_int16( + opus_int16 *a, /* I/O Unsorted / Sorted vector */ + const opus_int L /* I Vector length */ +); + +/* NLSF stabilizer, for a single input data vector */ +void silk_NLSF_stabilize( + opus_int16 *NLSF_Q15, /* I/O Unstable/stabilized normalized LSF vector in Q15 [L] */ + const opus_int16 *NDeltaMin_Q15, /* I Min distance vector, NDeltaMin_Q15[L] must be >= 1 [L+1] */ + const opus_int L /* I Number of NLSF parameters in the input vector */ +); + +/* Laroia low complexity NLSF weights */ +void silk_NLSF_VQ_weights_laroia( + opus_int16 *pNLSFW_Q_OUT, /* O Pointer to input vector weights [D] */ + const opus_int16 *pNLSF_Q15, /* I Pointer to input vector [D] */ + const opus_int D /* I Input vector dimension (even) */ +); + +/* Compute reflection coefficients from input signal */ +void silk_burg_modified_c( + opus_int32 *res_nrg, /* O Residual energy */ + opus_int *res_nrg_Q, /* O Residual energy Q value */ + opus_int32 A_Q16[], /* O Prediction coefficients (length order) */ + const opus_int16 x[], /* I Input signal, length: nb_subfr * ( D + subfr_length ) */ + const opus_int32 minInvGain_Q30, /* I Inverse of max prediction gain */ + const opus_int subfr_length, /* I Input signal subframe length (incl. D preceding samples) */ + const opus_int nb_subfr, /* I Number of subframes stacked in x */ + const opus_int D, /* I Order */ + int arch /* I Run-time architecture */ +); + +/* Copy and multiply a vector by a constant */ +void silk_scale_copy_vector16( + opus_int16 *data_out, + const opus_int16 *data_in, + opus_int32 gain_Q16, /* I Gain in Q16 */ + const opus_int dataSize /* I Length */ +); + +/* Some for the LTP related function requires Q26 to work.*/ +void silk_scale_vector32_Q26_lshift_18( + opus_int32 *data1, /* I/O Q0/Q18 */ + opus_int32 gain_Q26, /* I Q26 */ + opus_int dataSize /* I length */ +); + +/********************************************************************/ +/* INLINE ARM MATH */ +/********************************************************************/ + +/* return sum( inVec1[i] * inVec2[i] ) */ + +opus_int32 silk_inner_prod_aligned( + const opus_int16 *const inVec1, /* I input vector 1 */ + const opus_int16 *const inVec2, /* I input vector 2 */ + const opus_int len, /* I vector lengths */ + int arch /* I Run-time architecture */ +); + + +opus_int32 silk_inner_prod_aligned_scale( + const opus_int16 *const inVec1, /* I input vector 1 */ + const opus_int16 *const inVec2, /* I input vector 2 */ + const opus_int scale, /* I number of bits to shift */ + const opus_int len /* I vector lengths */ +); + +opus_int64 silk_inner_prod16_c( + const opus_int16 *inVec1, /* I input vector 1 */ + const opus_int16 *inVec2, /* I input vector 2 */ + const opus_int len /* I vector lengths */ +); + +/********************************************************************/ +/* MACROS */ +/********************************************************************/ + +/* Rotate a32 right by 'rot' bits. Negative rot values result in rotating + left. Output is 32bit int. + Note: contemporary compilers recognize the C expression below and + compile it into a 'ror' instruction if available. No need for OPUS_INLINE ASM! */ +static OPUS_INLINE opus_int32 silk_ROR32( opus_int32 a32, opus_int rot ) +{ + opus_uint32 x = (opus_uint32) a32; + opus_uint32 r = (opus_uint32) rot; + opus_uint32 m = (opus_uint32) -rot; + if( rot == 0 ) { + return a32; + } else if( rot < 0 ) { + return (opus_int32) ((x << m) | (x >> (32 - m))); + } else { + return (opus_int32) ((x << (32 - r)) | (x >> r)); + } +} + +/* Allocate opus_int16 aligned to 4-byte memory address */ +#if EMBEDDED_ARM +#define silk_DWORD_ALIGN __attribute__((aligned(4))) +#else +#define silk_DWORD_ALIGN +#endif + +/* Useful Macros that can be adjusted to other platforms */ +#define silk_memcpy(dest, src, size) memcpy((dest), (src), (size)) +#define silk_memset(dest, src, size) memset((dest), (src), (size)) +#define silk_memmove(dest, src, size) memmove((dest), (src), (size)) + +/* Fixed point macros */ + +/* (a32 * b32) output have to be 32bit int */ +#define silk_MUL(a32, b32) ((a32) * (b32)) + +/* (a32 * b32) output have to be 32bit uint */ +#define silk_MUL_uint(a32, b32) silk_MUL(a32, b32) + +/* a32 + (b32 * c32) output have to be 32bit int */ +#define silk_MLA(a32, b32, c32) silk_ADD32((a32),((b32) * (c32))) + +/* a32 + (b32 * c32) output have to be 32bit uint */ +#define silk_MLA_uint(a32, b32, c32) silk_MLA(a32, b32, c32) + +/* ((a32 >> 16) * (b32 >> 16)) output have to be 32bit int */ +#define silk_SMULTT(a32, b32) (((a32) >> 16) * ((b32) >> 16)) + +/* a32 + ((a32 >> 16) * (b32 >> 16)) output have to be 32bit int */ +#define silk_SMLATT(a32, b32, c32) silk_ADD32((a32),((b32) >> 16) * ((c32) >> 16)) + +#define silk_SMLALBB(a64, b16, c16) silk_ADD64((a64),(opus_int64)((opus_int32)(b16) * (opus_int32)(c16))) + +/* (a32 * b32) */ +#define silk_SMULL(a32, b32) ((opus_int64)(a32) * /*(opus_int64)*/(b32)) + +/* Adds two signed 32-bit values in a way that can overflow, while not relying on undefined behaviour + (just standard two's complement implementation-specific behaviour) */ +#define silk_ADD32_ovflw(a, b) ((opus_int32)((opus_uint32)(a) + (opus_uint32)(b))) +/* Subtractss two signed 32-bit values in a way that can overflow, while not relying on undefined behaviour + (just standard two's complement implementation-specific behaviour) */ +#define silk_SUB32_ovflw(a, b) ((opus_int32)((opus_uint32)(a) - (opus_uint32)(b))) + +/* Multiply-accumulate macros that allow overflow in the addition (ie, no asserts in debug mode) */ +#define silk_MLA_ovflw(a32, b32, c32) silk_ADD32_ovflw((a32), (opus_uint32)(b32) * (opus_uint32)(c32)) +#define silk_SMLABB_ovflw(a32, b32, c32) (silk_ADD32_ovflw((a32) , ((opus_int32)((opus_int16)(b32))) * (opus_int32)((opus_int16)(c32)))) + +#define silk_DIV32_16(a32, b16) ((opus_int32)((a32) / (b16))) +#define silk_DIV32(a32, b32) ((opus_int32)((a32) / (b32))) + +/* These macros enables checking for overflow in silk_API_Debug.h*/ +#define silk_ADD16(a, b) ((a) + (b)) +#define silk_ADD32(a, b) ((a) + (b)) +#define silk_ADD64(a, b) ((a) + (b)) + +#define silk_SUB16(a, b) ((a) - (b)) +#define silk_SUB32(a, b) ((a) - (b)) +#define silk_SUB64(a, b) ((a) - (b)) + +#define silk_SAT8(a) ((a) > silk_int8_MAX ? silk_int8_MAX : \ + ((a) < silk_int8_MIN ? silk_int8_MIN : (a))) +#define silk_SAT16(a) ((a) > silk_int16_MAX ? silk_int16_MAX : \ + ((a) < silk_int16_MIN ? silk_int16_MIN : (a))) +#define silk_SAT32(a) ((a) > silk_int32_MAX ? silk_int32_MAX : \ + ((a) < silk_int32_MIN ? silk_int32_MIN : (a))) + +#define silk_CHECK_FIT8(a) (a) +#define silk_CHECK_FIT16(a) (a) +#define silk_CHECK_FIT32(a) (a) + +#define silk_ADD_SAT16(a, b) (opus_int16)silk_SAT16( silk_ADD32( (opus_int32)(a), (b) ) ) +#define silk_ADD_SAT64(a, b) ((((a) + (b)) & 0x8000000000000000LL) == 0 ? \ + ((((a) & (b)) & 0x8000000000000000LL) != 0 ? silk_int64_MIN : (a)+(b)) : \ + ((((a) | (b)) & 0x8000000000000000LL) == 0 ? silk_int64_MAX : (a)+(b)) ) + +#define silk_SUB_SAT16(a, b) (opus_int16)silk_SAT16( silk_SUB32( (opus_int32)(a), (b) ) ) +#define silk_SUB_SAT64(a, b) ((((a)-(b)) & 0x8000000000000000LL) == 0 ? \ + (( (a) & ((b)^0x8000000000000000LL) & 0x8000000000000000LL) ? silk_int64_MIN : (a)-(b)) : \ + ((((a)^0x8000000000000000LL) & (b) & 0x8000000000000000LL) ? silk_int64_MAX : (a)-(b)) ) + +/* Saturation for positive input values */ +#define silk_POS_SAT32(a) ((a) > silk_int32_MAX ? silk_int32_MAX : (a)) + +/* Add with saturation for positive input values */ +#define silk_ADD_POS_SAT8(a, b) ((((a)+(b)) & 0x80) ? silk_int8_MAX : ((a)+(b))) +#define silk_ADD_POS_SAT16(a, b) ((((a)+(b)) & 0x8000) ? silk_int16_MAX : ((a)+(b))) +#define silk_ADD_POS_SAT32(a, b) ((((opus_uint32)(a)+(opus_uint32)(b)) & 0x80000000) ? silk_int32_MAX : ((a)+(b))) + +#define silk_LSHIFT8(a, shift) ((opus_int8)((opus_uint8)(a)<<(shift))) /* shift >= 0, shift < 8 */ +#define silk_LSHIFT16(a, shift) ((opus_int16)((opus_uint16)(a)<<(shift))) /* shift >= 0, shift < 16 */ +#define silk_LSHIFT32(a, shift) ((opus_int32)((opus_uint32)(a)<<(shift))) /* shift >= 0, shift < 32 */ +#define silk_LSHIFT64(a, shift) ((opus_int64)((opus_uint64)(a)<<(shift))) /* shift >= 0, shift < 64 */ +#define silk_LSHIFT(a, shift) silk_LSHIFT32(a, shift) /* shift >= 0, shift < 32 */ + +#define silk_RSHIFT8(a, shift) ((a)>>(shift)) /* shift >= 0, shift < 8 */ +#define silk_RSHIFT16(a, shift) ((a)>>(shift)) /* shift >= 0, shift < 16 */ +#define silk_RSHIFT32(a, shift) ((a)>>(shift)) /* shift >= 0, shift < 32 */ +#define silk_RSHIFT64(a, shift) ((a)>>(shift)) /* shift >= 0, shift < 64 */ +#define silk_RSHIFT(a, shift) silk_RSHIFT32(a, shift) /* shift >= 0, shift < 32 */ + +/* saturates before shifting */ +#define silk_LSHIFT_SAT32(a, shift) (silk_LSHIFT32( silk_LIMIT( (a), silk_RSHIFT32( silk_int32_MIN, (shift) ), \ + silk_RSHIFT32( silk_int32_MAX, (shift) ) ), (shift) )) + +#define silk_LSHIFT_ovflw(a, shift) ((opus_int32)((opus_uint32)(a) << (shift))) /* shift >= 0, allowed to overflow */ +#define silk_LSHIFT_uint(a, shift) ((a) << (shift)) /* shift >= 0 */ +#define silk_RSHIFT_uint(a, shift) ((a) >> (shift)) /* shift >= 0 */ + +#define silk_ADD_LSHIFT(a, b, shift) ((a) + silk_LSHIFT((b), (shift))) /* shift >= 0 */ +#define silk_ADD_LSHIFT32(a, b, shift) silk_ADD32((a), silk_LSHIFT32((b), (shift))) /* shift >= 0 */ +#define silk_ADD_LSHIFT_uint(a, b, shift) ((a) + silk_LSHIFT_uint((b), (shift))) /* shift >= 0 */ +#define silk_ADD_RSHIFT(a, b, shift) ((a) + silk_RSHIFT((b), (shift))) /* shift >= 0 */ +#define silk_ADD_RSHIFT32(a, b, shift) silk_ADD32((a), silk_RSHIFT32((b), (shift))) /* shift >= 0 */ +#define silk_ADD_RSHIFT_uint(a, b, shift) ((a) + silk_RSHIFT_uint((b), (shift))) /* shift >= 0 */ +#define silk_SUB_LSHIFT32(a, b, shift) silk_SUB32((a), silk_LSHIFT32((b), (shift))) /* shift >= 0 */ +#define silk_SUB_RSHIFT32(a, b, shift) silk_SUB32((a), silk_RSHIFT32((b), (shift))) /* shift >= 0 */ + +/* Requires that shift > 0 */ +#define silk_RSHIFT_ROUND(a, shift) ((shift) == 1 ? ((a) >> 1) + ((a) & 1) : (((a) >> ((shift) - 1)) + 1) >> 1) +#define silk_RSHIFT_ROUND64(a, shift) ((shift) == 1 ? ((a) >> 1) + ((a) & 1) : (((a) >> ((shift) - 1)) + 1) >> 1) + +/* Number of rightshift required to fit the multiplication */ +#define silk_NSHIFT_MUL_32_32(a, b) ( -(31- (32-silk_CLZ32(silk_abs(a)) + (32-silk_CLZ32(silk_abs(b))))) ) +#define silk_NSHIFT_MUL_16_16(a, b) ( -(15- (16-silk_CLZ16(silk_abs(a)) + (16-silk_CLZ16(silk_abs(b))))) ) + + +#define silk_min(a, b) (((a) < (b)) ? (a) : (b)) +#define silk_max(a, b) (((a) > (b)) ? (a) : (b)) + +/* Macro to convert floating-point constants to fixed-point */ +#define SILK_FIX_CONST( C, Q ) ((opus_int32)((C) * ((opus_int64)1 << (Q)) + 0.5)) + +/* silk_min() versions with typecast in the function call */ +static OPUS_INLINE opus_int silk_min_int(opus_int a, opus_int b) +{ + return (((a) < (b)) ? (a) : (b)); +} +static OPUS_INLINE opus_int16 silk_min_16(opus_int16 a, opus_int16 b) +{ + return (((a) < (b)) ? (a) : (b)); +} +static OPUS_INLINE opus_int32 silk_min_32(opus_int32 a, opus_int32 b) +{ + return (((a) < (b)) ? (a) : (b)); +} +static OPUS_INLINE opus_int64 silk_min_64(opus_int64 a, opus_int64 b) +{ + return (((a) < (b)) ? (a) : (b)); +} + +/* silk_min() versions with typecast in the function call */ +static OPUS_INLINE opus_int silk_max_int(opus_int a, opus_int b) +{ + return (((a) > (b)) ? (a) : (b)); +} +static OPUS_INLINE opus_int16 silk_max_16(opus_int16 a, opus_int16 b) +{ + return (((a) > (b)) ? (a) : (b)); +} +static OPUS_INLINE opus_int32 silk_max_32(opus_int32 a, opus_int32 b) +{ + return (((a) > (b)) ? (a) : (b)); +} +static OPUS_INLINE opus_int64 silk_max_64(opus_int64 a, opus_int64 b) +{ + return (((a) > (b)) ? (a) : (b)); +} + +#define silk_LIMIT( a, limit1, limit2) ((limit1) > (limit2) ? ((a) > (limit1) ? (limit1) : ((a) < (limit2) ? (limit2) : (a))) \ + : ((a) > (limit2) ? (limit2) : ((a) < (limit1) ? (limit1) : (a)))) + +#define silk_LIMIT_int silk_LIMIT +#define silk_LIMIT_16 silk_LIMIT +#define silk_LIMIT_32 silk_LIMIT + +#define silk_abs(a) (((a) > 0) ? (a) : -(a)) /* Be careful, silk_abs returns wrong when input equals to silk_intXX_MIN */ +#define silk_abs_int(a) (((a) ^ ((a) >> (8 * sizeof(a) - 1))) - ((a) >> (8 * sizeof(a) - 1))) +#define silk_abs_int32(a) (((a) ^ ((a) >> 31)) - ((a) >> 31)) +#define silk_abs_int64(a) (((a) > 0) ? (a) : -(a)) + +#define silk_sign(a) ((a) > 0 ? 1 : ( (a) < 0 ? -1 : 0 )) + +/* PSEUDO-RANDOM GENERATOR */ +/* Make sure to store the result as the seed for the next call (also in between */ +/* frames), otherwise result won't be random at all. When only using some of the */ +/* bits, take the most significant bits by right-shifting. */ +#define RAND_MULTIPLIER 196314165 +#define RAND_INCREMENT 907633515 +#define silk_RAND(seed) (silk_MLA_ovflw((RAND_INCREMENT), (seed), (RAND_MULTIPLIER))) + +/* Add some multiplication functions that can be easily mapped to ARM. */ + +/* silk_SMMUL: Signed top word multiply. + ARMv6 2 instruction cycles. + ARMv3M+ 3 instruction cycles. use SMULL and ignore LSB registers.(except xM)*/ +/*#define silk_SMMUL(a32, b32) (opus_int32)silk_RSHIFT(silk_SMLAL(silk_SMULWB((a32), (b32)), (a32), silk_RSHIFT_ROUND((b32), 16)), 16)*/ +/* the following seems faster on x86 */ +#define silk_SMMUL(a32, b32) (opus_int32)silk_RSHIFT64(silk_SMULL((a32), (b32)), 32) + +#if !defined(OVERRIDE_silk_burg_modified) +#define silk_burg_modified(res_nrg, res_nrg_Q, A_Q16, x, minInvGain_Q30, subfr_length, nb_subfr, D, arch) \ + ((void)(arch), silk_burg_modified_c(res_nrg, res_nrg_Q, A_Q16, x, minInvGain_Q30, subfr_length, nb_subfr, D, arch)) +#endif + +#if !defined(OVERRIDE_silk_inner_prod16) +#define silk_inner_prod16(inVec1, inVec2, len, arch) \ + ((void)(arch),silk_inner_prod16_c(inVec1, inVec2, len)) +#endif + +#include "Inlines.h" +#include "MacroCount.h" +#include "MacroDebug.h" + +#ifdef OPUS_ARM_INLINE_ASM +#include "arm/SigProc_FIX_armv4.h" +#endif + +#ifdef OPUS_ARM_INLINE_EDSP +#include "arm/SigProc_FIX_armv5e.h" +#endif + +#if defined(MIPSr1_ASM) +#include "mips/sigproc_fix_mipsr1.h" +#endif + + +#ifdef __cplusplus +} +#endif + +#endif /* SILK_SIGPROC_FIX_H */ diff --git a/port/third_party/opus/silk/VAD.c b/port/third_party/opus/silk/VAD.c new file mode 100644 index 00000000..d0cda521 --- /dev/null +++ b/port/third_party/opus/silk/VAD.c @@ -0,0 +1,360 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" + +/* Silk VAD noise level estimation */ +# if !defined(OPUS_X86_MAY_HAVE_SSE4_1) +static OPUS_INLINE void silk_VAD_GetNoiseLevels( + const opus_int32 pX[ VAD_N_BANDS ], /* I subband energies */ + silk_VAD_state *psSilk_VAD /* I/O Pointer to Silk VAD state */ +); +#endif + +/**********************************/ +/* Initialization of the Silk VAD */ +/**********************************/ +opus_int silk_VAD_Init( /* O Return value, 0 if success */ + silk_VAD_state *psSilk_VAD /* I/O Pointer to Silk VAD state */ +) +{ + opus_int b, ret = 0; + + /* reset state memory */ + silk_memset( psSilk_VAD, 0, sizeof( silk_VAD_state ) ); + + /* init noise levels */ + /* Initialize array with approx pink noise levels (psd proportional to inverse of frequency) */ + for( b = 0; b < VAD_N_BANDS; b++ ) { + psSilk_VAD->NoiseLevelBias[ b ] = silk_max_32( silk_DIV32_16( VAD_NOISE_LEVELS_BIAS, b + 1 ), 1 ); + } + + /* Initialize state */ + for( b = 0; b < VAD_N_BANDS; b++ ) { + psSilk_VAD->NL[ b ] = silk_MUL( 100, psSilk_VAD->NoiseLevelBias[ b ] ); + psSilk_VAD->inv_NL[ b ] = silk_DIV32( silk_int32_MAX, psSilk_VAD->NL[ b ] ); + } + psSilk_VAD->counter = 15; + + /* init smoothed energy-to-noise ratio*/ + for( b = 0; b < VAD_N_BANDS; b++ ) { + psSilk_VAD->NrgRatioSmth_Q8[ b ] = 100 * 256; /* 100 * 256 --> 20 dB SNR */ + } + + return( ret ); +} + +/* Weighting factors for tilt measure */ +static const opus_int32 tiltWeights[ VAD_N_BANDS ] = { 30000, 6000, -12000, -12000 }; + +/***************************************/ +/* Get the speech activity level in Q8 */ +/***************************************/ +opus_int silk_VAD_GetSA_Q8_c( /* O Return value, 0 if success */ + silk_encoder_state *psEncC, /* I/O Encoder state */ + const opus_int16 pIn[] /* I PCM input */ +) +{ + opus_int SA_Q15, pSNR_dB_Q7, input_tilt; + opus_int decimated_framelength1, decimated_framelength2; + opus_int decimated_framelength; + opus_int dec_subframe_length, dec_subframe_offset, SNR_Q7, i, b, s; + opus_int32 sumSquared, smooth_coef_Q16; + opus_int16 HPstateTmp; + VARDECL( opus_int16, X ); + opus_int32 Xnrg[ VAD_N_BANDS ]; + opus_int32 NrgToNoiseRatio_Q8[ VAD_N_BANDS ]; + opus_int32 speech_nrg, x_tmp; + opus_int X_offset[ VAD_N_BANDS ]; + opus_int ret = 0; + silk_VAD_state *psSilk_VAD = &psEncC->sVAD; + SAVE_STACK; + + /* Safety checks */ + silk_assert( VAD_N_BANDS == 4 ); + celt_assert( MAX_FRAME_LENGTH >= psEncC->frame_length ); + celt_assert( psEncC->frame_length <= 512 ); + celt_assert( psEncC->frame_length == 8 * silk_RSHIFT( psEncC->frame_length, 3 ) ); + + /***********************/ + /* Filter and Decimate */ + /***********************/ + decimated_framelength1 = silk_RSHIFT( psEncC->frame_length, 1 ); + decimated_framelength2 = silk_RSHIFT( psEncC->frame_length, 2 ); + decimated_framelength = silk_RSHIFT( psEncC->frame_length, 3 ); + /* Decimate into 4 bands: + 0 L 3L L 3L 5L + - -- - -- -- + 8 8 2 4 4 + + [0-1 kHz| temp. |1-2 kHz| 2-4 kHz | 4-8 kHz | + + They're arranged to allow the minimal ( frame_length / 4 ) extra + scratch space during the downsampling process */ + X_offset[ 0 ] = 0; + X_offset[ 1 ] = decimated_framelength + decimated_framelength2; + X_offset[ 2 ] = X_offset[ 1 ] + decimated_framelength; + X_offset[ 3 ] = X_offset[ 2 ] + decimated_framelength2; + ALLOC( X, X_offset[ 3 ] + decimated_framelength1, opus_int16 ); + + /* 0-8 kHz to 0-4 kHz and 4-8 kHz */ + silk_ana_filt_bank_1( pIn, &psSilk_VAD->AnaState[ 0 ], + X, &X[ X_offset[ 3 ] ], psEncC->frame_length ); + + /* 0-4 kHz to 0-2 kHz and 2-4 kHz */ + silk_ana_filt_bank_1( X, &psSilk_VAD->AnaState1[ 0 ], + X, &X[ X_offset[ 2 ] ], decimated_framelength1 ); + + /* 0-2 kHz to 0-1 kHz and 1-2 kHz */ + silk_ana_filt_bank_1( X, &psSilk_VAD->AnaState2[ 0 ], + X, &X[ X_offset[ 1 ] ], decimated_framelength2 ); + + /*********************************************/ + /* HP filter on lowest band (differentiator) */ + /*********************************************/ + X[ decimated_framelength - 1 ] = silk_RSHIFT( X[ decimated_framelength - 1 ], 1 ); + HPstateTmp = X[ decimated_framelength - 1 ]; + for( i = decimated_framelength - 1; i > 0; i-- ) { + X[ i - 1 ] = silk_RSHIFT( X[ i - 1 ], 1 ); + X[ i ] -= X[ i - 1 ]; + } + X[ 0 ] -= psSilk_VAD->HPstate; + psSilk_VAD->HPstate = HPstateTmp; + + /*************************************/ + /* Calculate the energy in each band */ + /*************************************/ + for( b = 0; b < VAD_N_BANDS; b++ ) { + /* Find the decimated framelength in the non-uniformly divided bands */ + decimated_framelength = silk_RSHIFT( psEncC->frame_length, silk_min_int( VAD_N_BANDS - b, VAD_N_BANDS - 1 ) ); + + /* Split length into subframe lengths */ + dec_subframe_length = silk_RSHIFT( decimated_framelength, VAD_INTERNAL_SUBFRAMES_LOG2 ); + dec_subframe_offset = 0; + + /* Compute energy per sub-frame */ + /* initialize with summed energy of last subframe */ + Xnrg[ b ] = psSilk_VAD->XnrgSubfr[ b ]; + for( s = 0; s < VAD_INTERNAL_SUBFRAMES; s++ ) { + sumSquared = 0; + for( i = 0; i < dec_subframe_length; i++ ) { + /* The energy will be less than dec_subframe_length * ( silk_int16_MIN / 8 ) ^ 2. */ + /* Therefore we can accumulate with no risk of overflow (unless dec_subframe_length > 128) */ + x_tmp = silk_RSHIFT( + X[ X_offset[ b ] + i + dec_subframe_offset ], 3 ); + sumSquared = silk_SMLABB( sumSquared, x_tmp, x_tmp ); + + /* Safety check */ + silk_assert( sumSquared >= 0 ); + } + + /* Add/saturate summed energy of current subframe */ + if( s < VAD_INTERNAL_SUBFRAMES - 1 ) { + Xnrg[ b ] = silk_ADD_POS_SAT32( Xnrg[ b ], sumSquared ); + } else { + /* Look-ahead subframe */ + Xnrg[ b ] = silk_ADD_POS_SAT32( Xnrg[ b ], silk_RSHIFT( sumSquared, 1 ) ); + } + + dec_subframe_offset += dec_subframe_length; + } + psSilk_VAD->XnrgSubfr[ b ] = sumSquared; + } + + /********************/ + /* Noise estimation */ + /********************/ + silk_VAD_GetNoiseLevels( &Xnrg[ 0 ], psSilk_VAD ); + + /***********************************************/ + /* Signal-plus-noise to noise ratio estimation */ + /***********************************************/ + sumSquared = 0; + input_tilt = 0; + for( b = 0; b < VAD_N_BANDS; b++ ) { + speech_nrg = Xnrg[ b ] - psSilk_VAD->NL[ b ]; + if( speech_nrg > 0 ) { + /* Divide, with sufficient resolution */ + if( ( Xnrg[ b ] & 0xFF800000 ) == 0 ) { + NrgToNoiseRatio_Q8[ b ] = silk_DIV32( silk_LSHIFT( Xnrg[ b ], 8 ), psSilk_VAD->NL[ b ] + 1 ); + } else { + NrgToNoiseRatio_Q8[ b ] = silk_DIV32( Xnrg[ b ], silk_RSHIFT( psSilk_VAD->NL[ b ], 8 ) + 1 ); + } + + /* Convert to log domain */ + SNR_Q7 = silk_lin2log( NrgToNoiseRatio_Q8[ b ] ) - 8 * 128; + + /* Sum-of-squares */ + sumSquared = silk_SMLABB( sumSquared, SNR_Q7, SNR_Q7 ); /* Q14 */ + + /* Tilt measure */ + if( speech_nrg < ( (opus_int32)1 << 20 ) ) { + /* Scale down SNR value for small subband speech energies */ + SNR_Q7 = silk_SMULWB( silk_LSHIFT( silk_SQRT_APPROX( speech_nrg ), 6 ), SNR_Q7 ); + } + input_tilt = silk_SMLAWB( input_tilt, tiltWeights[ b ], SNR_Q7 ); + } else { + NrgToNoiseRatio_Q8[ b ] = 256; + } + } + + /* Mean-of-squares */ + sumSquared = silk_DIV32_16( sumSquared, VAD_N_BANDS ); /* Q14 */ + + /* Root-mean-square approximation, scale to dBs, and write to output pointer */ + pSNR_dB_Q7 = (opus_int16)( 3 * silk_SQRT_APPROX( sumSquared ) ); /* Q7 */ + + /*********************************/ + /* Speech Probability Estimation */ + /*********************************/ + SA_Q15 = silk_sigm_Q15( silk_SMULWB( VAD_SNR_FACTOR_Q16, pSNR_dB_Q7 ) - VAD_NEGATIVE_OFFSET_Q5 ); + + /**************************/ + /* Frequency Tilt Measure */ + /**************************/ + psEncC->input_tilt_Q15 = silk_LSHIFT( silk_sigm_Q15( input_tilt ) - 16384, 1 ); + + /**************************************************/ + /* Scale the sigmoid output based on power levels */ + /**************************************************/ + speech_nrg = 0; + for( b = 0; b < VAD_N_BANDS; b++ ) { + /* Accumulate signal-without-noise energies, higher frequency bands have more weight */ + speech_nrg += ( b + 1 ) * silk_RSHIFT( Xnrg[ b ] - psSilk_VAD->NL[ b ], 4 ); + } + + if( psEncC->frame_length == 20 * psEncC->fs_kHz ) { + speech_nrg = silk_RSHIFT32( speech_nrg, 1 ); + } + /* Power scaling */ + if( speech_nrg <= 0 ) { + SA_Q15 = silk_RSHIFT( SA_Q15, 1 ); + } else if( speech_nrg < 16384 ) { + speech_nrg = silk_LSHIFT32( speech_nrg, 16 ); + + /* square-root */ + speech_nrg = silk_SQRT_APPROX( speech_nrg ); + SA_Q15 = silk_SMULWB( 32768 + speech_nrg, SA_Q15 ); + } + + /* Copy the resulting speech activity in Q8 */ + psEncC->speech_activity_Q8 = silk_min_int( silk_RSHIFT( SA_Q15, 7 ), silk_uint8_MAX ); + + /***********************************/ + /* Energy Level and SNR estimation */ + /***********************************/ + /* Smoothing coefficient */ + smooth_coef_Q16 = silk_SMULWB( VAD_SNR_SMOOTH_COEF_Q18, silk_SMULWB( (opus_int32)SA_Q15, SA_Q15 ) ); + + if( psEncC->frame_length == 10 * psEncC->fs_kHz ) { + smooth_coef_Q16 >>= 1; + } + + for( b = 0; b < VAD_N_BANDS; b++ ) { + /* compute smoothed energy-to-noise ratio per band */ + psSilk_VAD->NrgRatioSmth_Q8[ b ] = silk_SMLAWB( psSilk_VAD->NrgRatioSmth_Q8[ b ], + NrgToNoiseRatio_Q8[ b ] - psSilk_VAD->NrgRatioSmth_Q8[ b ], smooth_coef_Q16 ); + + /* signal to noise ratio in dB per band */ + SNR_Q7 = 3 * ( silk_lin2log( psSilk_VAD->NrgRatioSmth_Q8[b] ) - 8 * 128 ); + /* quality = sigmoid( 0.25 * ( SNR_dB - 16 ) ); */ + psEncC->input_quality_bands_Q15[ b ] = silk_sigm_Q15( silk_RSHIFT( SNR_Q7 - 16 * 128, 4 ) ); + } + + RESTORE_STACK; + return( ret ); +} + +/**************************/ +/* Noise level estimation */ +/**************************/ +# if !defined(OPUS_X86_MAY_HAVE_SSE4_1) +static OPUS_INLINE +#endif +void silk_VAD_GetNoiseLevels( + const opus_int32 pX[ VAD_N_BANDS ], /* I subband energies */ + silk_VAD_state *psSilk_VAD /* I/O Pointer to Silk VAD state */ +) +{ + opus_int k; + opus_int32 nl, nrg, inv_nrg; + opus_int coef, min_coef; + + /* Initially faster smoothing */ + if( psSilk_VAD->counter < 1000 ) { /* 1000 = 20 sec */ + min_coef = silk_DIV32_16( silk_int16_MAX, silk_RSHIFT( psSilk_VAD->counter, 4 ) + 1 ); + /* Increment frame counter */ + psSilk_VAD->counter++; + } else { + min_coef = 0; + } + + for( k = 0; k < VAD_N_BANDS; k++ ) { + /* Get old noise level estimate for current band */ + nl = psSilk_VAD->NL[ k ]; + silk_assert( nl >= 0 ); + + /* Add bias */ + nrg = silk_ADD_POS_SAT32( pX[ k ], psSilk_VAD->NoiseLevelBias[ k ] ); + silk_assert( nrg > 0 ); + + /* Invert energies */ + inv_nrg = silk_DIV32( silk_int32_MAX, nrg ); + silk_assert( inv_nrg >= 0 ); + + /* Less update when subband energy is high */ + if( nrg > silk_LSHIFT( nl, 3 ) ) { + coef = VAD_NOISE_LEVEL_SMOOTH_COEF_Q16 >> 3; + } else if( nrg < nl ) { + coef = VAD_NOISE_LEVEL_SMOOTH_COEF_Q16; + } else { + coef = silk_SMULWB( silk_SMULWW( inv_nrg, nl ), VAD_NOISE_LEVEL_SMOOTH_COEF_Q16 << 1 ); + } + + /* Initially faster smoothing */ + coef = silk_max_int( coef, min_coef ); + + /* Smooth inverse energies */ + psSilk_VAD->inv_NL[ k ] = silk_SMLAWB( psSilk_VAD->inv_NL[ k ], inv_nrg - psSilk_VAD->inv_NL[ k ], coef ); + silk_assert( psSilk_VAD->inv_NL[ k ] >= 0 ); + + /* Compute noise level by inverting again */ + nl = silk_DIV32( silk_int32_MAX, psSilk_VAD->inv_NL[ k ] ); + silk_assert( nl >= 0 ); + + /* Limit noise levels (guarantee 7 bits of head room) */ + nl = silk_min( nl, 0x00FFFFFF ); + + /* Store as part of state */ + psSilk_VAD->NL[ k ] = nl; + } +} diff --git a/port/third_party/opus/silk/VQ_WMat_EC.c b/port/third_party/opus/silk/VQ_WMat_EC.c new file mode 100644 index 00000000..245a7e4b --- /dev/null +++ b/port/third_party/opus/silk/VQ_WMat_EC.c @@ -0,0 +1,131 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Entropy constrained matrix-weighted VQ, hard-coded to 5-element vectors, for a single input data vector */ +void silk_VQ_WMat_EC_c( + opus_int8 *ind, /* O index of best codebook vector */ + opus_int32 *res_nrg_Q15, /* O best residual energy */ + opus_int32 *rate_dist_Q8, /* O best total bitrate */ + opus_int *gain_Q7, /* O sum of absolute LTP coefficients */ + const opus_int32 *XX_Q17, /* I correlation matrix */ + const opus_int32 *xX_Q17, /* I correlation vector */ + const opus_int8 *cb_Q7, /* I codebook */ + const opus_uint8 *cb_gain_Q7, /* I codebook effective gain */ + const opus_uint8 *cl_Q5, /* I code length for each codebook vector */ + const opus_int subfr_len, /* I number of samples per subframe */ + const opus_int32 max_gain_Q7, /* I maximum sum of absolute LTP coefficients */ + const opus_int L /* I number of vectors in codebook */ +) +{ + opus_int k, gain_tmp_Q7; + const opus_int8 *cb_row_Q7; + opus_int32 neg_xX_Q24[ 5 ]; + opus_int32 sum1_Q15, sum2_Q24; + opus_int32 bits_res_Q8, bits_tot_Q8; + + /* Negate and convert to new Q domain */ + neg_xX_Q24[ 0 ] = -silk_LSHIFT32( xX_Q17[ 0 ], 7 ); + neg_xX_Q24[ 1 ] = -silk_LSHIFT32( xX_Q17[ 1 ], 7 ); + neg_xX_Q24[ 2 ] = -silk_LSHIFT32( xX_Q17[ 2 ], 7 ); + neg_xX_Q24[ 3 ] = -silk_LSHIFT32( xX_Q17[ 3 ], 7 ); + neg_xX_Q24[ 4 ] = -silk_LSHIFT32( xX_Q17[ 4 ], 7 ); + + /* Loop over codebook */ + *rate_dist_Q8 = silk_int32_MAX; + *res_nrg_Q15 = silk_int32_MAX; + cb_row_Q7 = cb_Q7; + /* If things go really bad, at least *ind is set to something safe. */ + *ind = 0; + for( k = 0; k < L; k++ ) { + opus_int32 penalty; + gain_tmp_Q7 = cb_gain_Q7[k]; + /* Weighted rate */ + /* Quantization error: 1 - 2 * xX * cb + cb' * XX * cb */ + sum1_Q15 = SILK_FIX_CONST( 1.001, 15 ); + + /* Penalty for too large gain */ + penalty = silk_LSHIFT32( silk_max( silk_SUB32( gain_tmp_Q7, max_gain_Q7 ), 0 ), 11 ); + + /* first row of XX_Q17 */ + sum2_Q24 = silk_MLA( neg_xX_Q24[ 0 ], XX_Q17[ 1 ], cb_row_Q7[ 1 ] ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 2 ], cb_row_Q7[ 2 ] ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 3 ], cb_row_Q7[ 3 ] ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 4 ], cb_row_Q7[ 4 ] ); + sum2_Q24 = silk_LSHIFT32( sum2_Q24, 1 ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 0 ], cb_row_Q7[ 0 ] ); + sum1_Q15 = silk_SMLAWB( sum1_Q15, sum2_Q24, cb_row_Q7[ 0 ] ); + + /* second row of XX_Q17 */ + sum2_Q24 = silk_MLA( neg_xX_Q24[ 1 ], XX_Q17[ 7 ], cb_row_Q7[ 2 ] ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 8 ], cb_row_Q7[ 3 ] ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 9 ], cb_row_Q7[ 4 ] ); + sum2_Q24 = silk_LSHIFT32( sum2_Q24, 1 ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 6 ], cb_row_Q7[ 1 ] ); + sum1_Q15 = silk_SMLAWB( sum1_Q15, sum2_Q24, cb_row_Q7[ 1 ] ); + + /* third row of XX_Q17 */ + sum2_Q24 = silk_MLA( neg_xX_Q24[ 2 ], XX_Q17[ 13 ], cb_row_Q7[ 3 ] ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 14 ], cb_row_Q7[ 4 ] ); + sum2_Q24 = silk_LSHIFT32( sum2_Q24, 1 ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 12 ], cb_row_Q7[ 2 ] ); + sum1_Q15 = silk_SMLAWB( sum1_Q15, sum2_Q24, cb_row_Q7[ 2 ] ); + + /* fourth row of XX_Q17 */ + sum2_Q24 = silk_MLA( neg_xX_Q24[ 3 ], XX_Q17[ 19 ], cb_row_Q7[ 4 ] ); + sum2_Q24 = silk_LSHIFT32( sum2_Q24, 1 ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 18 ], cb_row_Q7[ 3 ] ); + sum1_Q15 = silk_SMLAWB( sum1_Q15, sum2_Q24, cb_row_Q7[ 3 ] ); + + /* last row of XX_Q17 */ + sum2_Q24 = silk_LSHIFT32( neg_xX_Q24[ 4 ], 1 ); + sum2_Q24 = silk_MLA( sum2_Q24, XX_Q17[ 24 ], cb_row_Q7[ 4 ] ); + sum1_Q15 = silk_SMLAWB( sum1_Q15, sum2_Q24, cb_row_Q7[ 4 ] ); + + /* find best */ + if( sum1_Q15 >= 0 ) { + /* Translate residual energy to bits using high-rate assumption (6 dB ==> 1 bit/sample) */ + bits_res_Q8 = silk_SMULBB( subfr_len, silk_lin2log( sum1_Q15 + penalty) - (15 << 7) ); + /* In the following line we reduce the codelength component by half ("-1"); seems to slightly improve quality */ + bits_tot_Q8 = silk_ADD_LSHIFT32( bits_res_Q8, cl_Q5[ k ], 3-1 ); + if( bits_tot_Q8 <= *rate_dist_Q8 ) { + *rate_dist_Q8 = bits_tot_Q8; + *res_nrg_Q15 = sum1_Q15 + penalty; + *ind = (opus_int8)k; + *gain_Q7 = gain_tmp_Q7; + } + } + + /* Go to next cbk vector */ + cb_row_Q7 += LTP_ORDER; + } +} diff --git a/port/third_party/opus/silk/ana_filt_bank_1.c b/port/third_party/opus/silk/ana_filt_bank_1.c new file mode 100644 index 00000000..24cfb03f --- /dev/null +++ b/port/third_party/opus/silk/ana_filt_bank_1.c @@ -0,0 +1,74 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" + +/* Coefficients for 2-band filter bank based on first-order allpass filters */ +static opus_int16 A_fb1_20 = 5394 << 1; +static opus_int16 A_fb1_21 = -24290; /* (opus_int16)(20623 << 1) */ + +/* Split signal into two decimated bands using first-order allpass filters */ +void silk_ana_filt_bank_1( + const opus_int16 *in, /* I Input signal [N] */ + opus_int32 *S, /* I/O State vector [2] */ + opus_int16 *outL, /* O Low band [N/2] */ + opus_int16 *outH, /* O High band [N/2] */ + const opus_int32 N /* I Number of input samples */ +) +{ + opus_int k, N2 = silk_RSHIFT( N, 1 ); + opus_int32 in32, X, Y, out_1, out_2; + + /* Internal variables and state are in Q10 format */ + for( k = 0; k < N2; k++ ) { + /* Convert to Q10 */ + in32 = silk_LSHIFT( (opus_int32)in[ 2 * k ], 10 ); + + /* All-pass section for even input sample */ + Y = silk_SUB32( in32, S[ 0 ] ); + X = silk_SMLAWB( Y, Y, A_fb1_21 ); + out_1 = silk_ADD32( S[ 0 ], X ); + S[ 0 ] = silk_ADD32( in32, X ); + + /* Convert to Q10 */ + in32 = silk_LSHIFT( (opus_int32)in[ 2 * k + 1 ], 10 ); + + /* All-pass section for odd input sample, and add to output of previous section */ + Y = silk_SUB32( in32, S[ 1 ] ); + X = silk_SMULWB( Y, A_fb1_20 ); + out_2 = silk_ADD32( S[ 1 ], X ); + S[ 1 ] = silk_ADD32( in32, X ); + + /* Add/subtract, convert back to int16 and store to output */ + outL[ k ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( silk_ADD32( out_2, out_1 ), 11 ) ); + outH[ k ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( silk_SUB32( out_2, out_1 ), 11 ) ); + } +} diff --git a/port/third_party/opus/silk/biquad_alt.c b/port/third_party/opus/silk/biquad_alt.c new file mode 100644 index 00000000..54566a43 --- /dev/null +++ b/port/third_party/opus/silk/biquad_alt.c @@ -0,0 +1,121 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +/* * + * silk_biquad_alt.c * + * * + * Second order ARMA filter * + * Can handle slowly varying filter coefficients * + * */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" + +/* Second order ARMA filter, alternative implementation */ +void silk_biquad_alt_stride1( + const opus_int16 *in, /* I input signal */ + const opus_int32 *B_Q28, /* I MA coefficients [3] */ + const opus_int32 *A_Q28, /* I AR coefficients [2] */ + opus_int32 *S, /* I/O State vector [2] */ + opus_int16 *out, /* O output signal */ + const opus_int32 len /* I signal length (must be even) */ +) +{ + /* DIRECT FORM II TRANSPOSED (uses 2 element state vector) */ + opus_int k; + opus_int32 inval, A0_U_Q28, A0_L_Q28, A1_U_Q28, A1_L_Q28, out32_Q14; + + /* Negate A_Q28 values and split in two parts */ + A0_L_Q28 = ( -A_Q28[ 0 ] ) & 0x00003FFF; /* lower part */ + A0_U_Q28 = silk_RSHIFT( -A_Q28[ 0 ], 14 ); /* upper part */ + A1_L_Q28 = ( -A_Q28[ 1 ] ) & 0x00003FFF; /* lower part */ + A1_U_Q28 = silk_RSHIFT( -A_Q28[ 1 ], 14 ); /* upper part */ + + for( k = 0; k < len; k++ ) { + /* S[ 0 ], S[ 1 ]: Q12 */ + inval = in[ k ]; + out32_Q14 = silk_LSHIFT( silk_SMLAWB( S[ 0 ], B_Q28[ 0 ], inval ), 2 ); + + S[ 0 ] = S[1] + silk_RSHIFT_ROUND( silk_SMULWB( out32_Q14, A0_L_Q28 ), 14 ); + S[ 0 ] = silk_SMLAWB( S[ 0 ], out32_Q14, A0_U_Q28 ); + S[ 0 ] = silk_SMLAWB( S[ 0 ], B_Q28[ 1 ], inval); + + S[ 1 ] = silk_RSHIFT_ROUND( silk_SMULWB( out32_Q14, A1_L_Q28 ), 14 ); + S[ 1 ] = silk_SMLAWB( S[ 1 ], out32_Q14, A1_U_Q28 ); + S[ 1 ] = silk_SMLAWB( S[ 1 ], B_Q28[ 2 ], inval ); + + /* Scale back to Q0 and saturate */ + out[ k ] = (opus_int16)silk_SAT16( silk_RSHIFT( out32_Q14 + (1<<14) - 1, 14 ) ); + } +} + +void silk_biquad_alt_stride2_c( + const opus_int16 *in, /* I input signal */ + const opus_int32 *B_Q28, /* I MA coefficients [3] */ + const opus_int32 *A_Q28, /* I AR coefficients [2] */ + opus_int32 *S, /* I/O State vector [4] */ + opus_int16 *out, /* O output signal */ + const opus_int32 len /* I signal length (must be even) */ +) +{ + /* DIRECT FORM II TRANSPOSED (uses 2 element state vector) */ + opus_int k; + opus_int32 A0_U_Q28, A0_L_Q28, A1_U_Q28, A1_L_Q28, out32_Q14[ 2 ]; + + /* Negate A_Q28 values and split in two parts */ + A0_L_Q28 = ( -A_Q28[ 0 ] ) & 0x00003FFF; /* lower part */ + A0_U_Q28 = silk_RSHIFT( -A_Q28[ 0 ], 14 ); /* upper part */ + A1_L_Q28 = ( -A_Q28[ 1 ] ) & 0x00003FFF; /* lower part */ + A1_U_Q28 = silk_RSHIFT( -A_Q28[ 1 ], 14 ); /* upper part */ + + for( k = 0; k < len; k++ ) { + /* S[ 0 ], S[ 1 ], S[ 2 ], S[ 3 ]: Q12 */ + out32_Q14[ 0 ] = silk_LSHIFT( silk_SMLAWB( S[ 0 ], B_Q28[ 0 ], in[ 2 * k + 0 ] ), 2 ); + out32_Q14[ 1 ] = silk_LSHIFT( silk_SMLAWB( S[ 2 ], B_Q28[ 0 ], in[ 2 * k + 1 ] ), 2 ); + + S[ 0 ] = S[ 1 ] + silk_RSHIFT_ROUND( silk_SMULWB( out32_Q14[ 0 ], A0_L_Q28 ), 14 ); + S[ 2 ] = S[ 3 ] + silk_RSHIFT_ROUND( silk_SMULWB( out32_Q14[ 1 ], A0_L_Q28 ), 14 ); + S[ 0 ] = silk_SMLAWB( S[ 0 ], out32_Q14[ 0 ], A0_U_Q28 ); + S[ 2 ] = silk_SMLAWB( S[ 2 ], out32_Q14[ 1 ], A0_U_Q28 ); + S[ 0 ] = silk_SMLAWB( S[ 0 ], B_Q28[ 1 ], in[ 2 * k + 0 ] ); + S[ 2 ] = silk_SMLAWB( S[ 2 ], B_Q28[ 1 ], in[ 2 * k + 1 ] ); + + S[ 1 ] = silk_RSHIFT_ROUND( silk_SMULWB( out32_Q14[ 0 ], A1_L_Q28 ), 14 ); + S[ 3 ] = silk_RSHIFT_ROUND( silk_SMULWB( out32_Q14[ 1 ], A1_L_Q28 ), 14 ); + S[ 1 ] = silk_SMLAWB( S[ 1 ], out32_Q14[ 0 ], A1_U_Q28 ); + S[ 3 ] = silk_SMLAWB( S[ 3 ], out32_Q14[ 1 ], A1_U_Q28 ); + S[ 1 ] = silk_SMLAWB( S[ 1 ], B_Q28[ 2 ], in[ 2 * k + 0 ] ); + S[ 3 ] = silk_SMLAWB( S[ 3 ], B_Q28[ 2 ], in[ 2 * k + 1 ] ); + + /* Scale back to Q0 and saturate */ + out[ 2 * k + 0 ] = (opus_int16)silk_SAT16( silk_RSHIFT( out32_Q14[ 0 ] + (1<<14) - 1, 14 ) ); + out[ 2 * k + 1 ] = (opus_int16)silk_SAT16( silk_RSHIFT( out32_Q14[ 1 ] + (1<<14) - 1, 14 ) ); + } +} diff --git a/port/third_party/opus/silk/bwexpander.c b/port/third_party/opus/silk/bwexpander.c new file mode 100644 index 00000000..afa97907 --- /dev/null +++ b/port/third_party/opus/silk/bwexpander.c @@ -0,0 +1,51 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" + +/* Chirp (bandwidth expand) LP AR filter */ +void silk_bwexpander( + opus_int16 *ar, /* I/O AR filter to be expanded (without leading 1) */ + const opus_int d, /* I Length of ar */ + opus_int32 chirp_Q16 /* I Chirp factor (typically in the range 0 to 1) */ +) +{ + opus_int i; + opus_int32 chirp_minus_one_Q16 = chirp_Q16 - 65536; + + /* NB: Dont use silk_SMULWB, instead of silk_RSHIFT_ROUND( silk_MUL(), 16 ), below. */ + /* Bias in silk_SMULWB can lead to unstable filters */ + for( i = 0; i < d - 1; i++ ) { + ar[ i ] = (opus_int16)silk_RSHIFT_ROUND( silk_MUL( chirp_Q16, ar[ i ] ), 16 ); + chirp_Q16 += silk_RSHIFT_ROUND( silk_MUL( chirp_Q16, chirp_minus_one_Q16 ), 16 ); + } + ar[ d - 1 ] = (opus_int16)silk_RSHIFT_ROUND( silk_MUL( chirp_Q16, ar[ d - 1 ] ), 16 ); +} diff --git a/port/third_party/opus/silk/bwexpander_32.c b/port/third_party/opus/silk/bwexpander_32.c new file mode 100644 index 00000000..0f32b9df --- /dev/null +++ b/port/third_party/opus/silk/bwexpander_32.c @@ -0,0 +1,51 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" + +/* Chirp (bandwidth expand) LP AR filter. + This logic is reused in _celt_lpc(). Any bug fixes should also be applied there. */ +void silk_bwexpander_32( + opus_int32 *ar, /* I/O AR filter to be expanded (without leading 1) */ + const opus_int d, /* I Length of ar */ + opus_int32 chirp_Q16 /* I Chirp factor in Q16 */ +) +{ + opus_int i; + opus_int32 chirp_minus_one_Q16 = chirp_Q16 - 65536; + + for( i = 0; i < d - 1; i++ ) { + ar[ i ] = silk_SMULWW( chirp_Q16, ar[ i ] ); + chirp_Q16 += silk_RSHIFT_ROUND( silk_MUL( chirp_Q16, chirp_minus_one_Q16 ), 16 ); + } + ar[ d - 1 ] = silk_SMULWW( chirp_Q16, ar[ d - 1 ] ); +} + diff --git a/port/third_party/opus/silk/check_control_input.c b/port/third_party/opus/silk/check_control_input.c new file mode 100644 index 00000000..739fb01f --- /dev/null +++ b/port/third_party/opus/silk/check_control_input.c @@ -0,0 +1,106 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "control.h" +#include "errors.h" + +/* Check encoder control struct */ +opus_int check_control_input( + silk_EncControlStruct *encControl /* I Control structure */ +) +{ + celt_assert( encControl != NULL ); + + if( ( ( encControl->API_sampleRate != 8000 ) && + ( encControl->API_sampleRate != 12000 ) && + ( encControl->API_sampleRate != 16000 ) && + ( encControl->API_sampleRate != 24000 ) && + ( encControl->API_sampleRate != 32000 ) && + ( encControl->API_sampleRate != 44100 ) && + ( encControl->API_sampleRate != 48000 ) ) || + ( ( encControl->desiredInternalSampleRate != 8000 ) && + ( encControl->desiredInternalSampleRate != 12000 ) && + ( encControl->desiredInternalSampleRate != 16000 ) ) || + ( ( encControl->maxInternalSampleRate != 8000 ) && + ( encControl->maxInternalSampleRate != 12000 ) && + ( encControl->maxInternalSampleRate != 16000 ) ) || + ( ( encControl->minInternalSampleRate != 8000 ) && + ( encControl->minInternalSampleRate != 12000 ) && + ( encControl->minInternalSampleRate != 16000 ) ) || + ( encControl->minInternalSampleRate > encControl->desiredInternalSampleRate ) || + ( encControl->maxInternalSampleRate < encControl->desiredInternalSampleRate ) || + ( encControl->minInternalSampleRate > encControl->maxInternalSampleRate ) ) { + celt_assert( 0 ); + return SILK_ENC_FS_NOT_SUPPORTED; + } + if( encControl->payloadSize_ms != 10 && + encControl->payloadSize_ms != 20 && + encControl->payloadSize_ms != 40 && + encControl->payloadSize_ms != 60 ) { + celt_assert( 0 ); + return SILK_ENC_PACKET_SIZE_NOT_SUPPORTED; + } + if( encControl->packetLossPercentage < 0 || encControl->packetLossPercentage > 100 ) { + celt_assert( 0 ); + return SILK_ENC_INVALID_LOSS_RATE; + } + if( encControl->useDTX < 0 || encControl->useDTX > 1 ) { + celt_assert( 0 ); + return SILK_ENC_INVALID_DTX_SETTING; + } + if( encControl->useCBR < 0 || encControl->useCBR > 1 ) { + celt_assert( 0 ); + return SILK_ENC_INVALID_CBR_SETTING; + } + if( encControl->useInBandFEC < 0 || encControl->useInBandFEC > 1 ) { + celt_assert( 0 ); + return SILK_ENC_INVALID_INBAND_FEC_SETTING; + } + if( encControl->nChannelsAPI < 1 || encControl->nChannelsAPI > ENCODER_NUM_CHANNELS ) { + celt_assert( 0 ); + return SILK_ENC_INVALID_NUMBER_OF_CHANNELS_ERROR; + } + if( encControl->nChannelsInternal < 1 || encControl->nChannelsInternal > ENCODER_NUM_CHANNELS ) { + celt_assert( 0 ); + return SILK_ENC_INVALID_NUMBER_OF_CHANNELS_ERROR; + } + if( encControl->nChannelsInternal > encControl->nChannelsAPI ) { + celt_assert( 0 ); + return SILK_ENC_INVALID_NUMBER_OF_CHANNELS_ERROR; + } + if( encControl->complexity < 0 || encControl->complexity > 10 ) { + celt_assert( 0 ); + return SILK_ENC_INVALID_COMPLEXITY_SETTING; + } + + return SILK_NO_ERROR; +} diff --git a/port/third_party/opus/silk/code_signs.c b/port/third_party/opus/silk/code_signs.c new file mode 100644 index 00000000..dfd1dca9 --- /dev/null +++ b/port/third_party/opus/silk/code_signs.c @@ -0,0 +1,115 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/*#define silk_enc_map(a) ((a) > 0 ? 1 : 0)*/ +/*#define silk_dec_map(a) ((a) > 0 ? 1 : -1)*/ +/* shifting avoids if-statement */ +#define silk_enc_map(a) ( silk_RSHIFT( (a), 15 ) + 1 ) +#define silk_dec_map(a) ( silk_LSHIFT( (a), 1 ) - 1 ) + +/* Encodes signs of excitation */ +void silk_encode_signs( + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + const opus_int8 pulses[], /* I pulse signal */ + opus_int length, /* I length of input */ + const opus_int signalType, /* I Signal type */ + const opus_int quantOffsetType, /* I Quantization offset type */ + const opus_int sum_pulses[ MAX_NB_SHELL_BLOCKS ] /* I Sum of absolute pulses per block */ +) +{ + opus_int i, j, p; + opus_uint8 icdf[ 2 ]; + const opus_int8 *q_ptr; + const opus_uint8 *icdf_ptr; + + icdf[ 1 ] = 0; + q_ptr = pulses; + i = silk_SMULBB( 7, silk_ADD_LSHIFT( quantOffsetType, signalType, 1 ) ); + icdf_ptr = &silk_sign_iCDF[ i ]; + length = silk_RSHIFT( length + SHELL_CODEC_FRAME_LENGTH/2, LOG2_SHELL_CODEC_FRAME_LENGTH ); + for( i = 0; i < length; i++ ) { + p = sum_pulses[ i ]; + if( p > 0 ) { + icdf[ 0 ] = icdf_ptr[ silk_min( p & 0x1F, 6 ) ]; + for( j = 0; j < SHELL_CODEC_FRAME_LENGTH; j++ ) { + if( q_ptr[ j ] != 0 ) { + ec_enc_icdf( psRangeEnc, silk_enc_map( q_ptr[ j ]), icdf, 8 ); + } + } + } + q_ptr += SHELL_CODEC_FRAME_LENGTH; + } +} + +/* Decodes signs of excitation */ +void silk_decode_signs( + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int16 pulses[], /* I/O pulse signal */ + opus_int length, /* I length of input */ + const opus_int signalType, /* I Signal type */ + const opus_int quantOffsetType, /* I Quantization offset type */ + const opus_int sum_pulses[ MAX_NB_SHELL_BLOCKS ] /* I Sum of absolute pulses per block */ +) +{ + opus_int i, j, p; + opus_uint8 icdf[ 2 ]; + opus_int16 *q_ptr; + const opus_uint8 *icdf_ptr; + + icdf[ 1 ] = 0; + q_ptr = pulses; + i = silk_SMULBB( 7, silk_ADD_LSHIFT( quantOffsetType, signalType, 1 ) ); + icdf_ptr = &silk_sign_iCDF[ i ]; + length = silk_RSHIFT( length + SHELL_CODEC_FRAME_LENGTH/2, LOG2_SHELL_CODEC_FRAME_LENGTH ); + for( i = 0; i < length; i++ ) { + p = sum_pulses[ i ]; + if( p > 0 ) { + icdf[ 0 ] = icdf_ptr[ silk_min( p & 0x1F, 6 ) ]; + for( j = 0; j < SHELL_CODEC_FRAME_LENGTH; j++ ) { + if( q_ptr[ j ] > 0 ) { + /* attach sign */ +#if 0 + /* conditional implementation */ + if( ec_dec_icdf( psRangeDec, icdf, 8 ) == 0 ) { + q_ptr[ j ] = -q_ptr[ j ]; + } +#else + /* implementation with shift, subtraction, multiplication */ + q_ptr[ j ] *= silk_dec_map( ec_dec_icdf( psRangeDec, icdf, 8 ) ); +#endif + } + } + } + q_ptr += SHELL_CODEC_FRAME_LENGTH; + } +} diff --git a/port/third_party/opus/silk/control.h b/port/third_party/opus/silk/control.h new file mode 100644 index 00000000..f5633e62 --- /dev/null +++ b/port/third_party/opus/silk/control.h @@ -0,0 +1,161 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_CONTROL_H +#define SILK_CONTROL_H + +#include "typedef.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Decoder API flags */ +#define FLAG_DECODE_NORMAL 0 +#define FLAG_PACKET_LOST 1 +#define FLAG_DECODE_LBRR 2 + +/***********************************************/ +/* Structure for controlling encoder operation */ +/***********************************************/ +typedef struct { + /* I: Number of channels; 1/2 */ + opus_int32 nChannelsAPI; + + /* I: Number of channels; 1/2 */ + opus_int32 nChannelsInternal; + + /* I: Input signal sampling rate in Hertz; 8000/12000/16000/24000/32000/44100/48000 */ + opus_int32 API_sampleRate; + + /* I: Maximum internal sampling rate in Hertz; 8000/12000/16000 */ + opus_int32 maxInternalSampleRate; + + /* I: Minimum internal sampling rate in Hertz; 8000/12000/16000 */ + opus_int32 minInternalSampleRate; + + /* I: Soft request for internal sampling rate in Hertz; 8000/12000/16000 */ + opus_int32 desiredInternalSampleRate; + + /* I: Number of samples per packet in milliseconds; 10/20/40/60 */ + opus_int payloadSize_ms; + + /* I: Bitrate during active speech in bits/second; internally limited */ + opus_int32 bitRate; + + /* I: Uplink packet loss in percent (0-100) */ + opus_int packetLossPercentage; + + /* I: Complexity mode; 0 is lowest, 10 is highest complexity */ + opus_int complexity; + + /* I: Flag to enable in-band Forward Error Correction (FEC); 0/1 */ + opus_int useInBandFEC; + + /* I: Flag to enable in-band Deep REDundancy (DRED); 0/1 */ + opus_int useDRED; + + /* I: Flag to actually code in-band Forward Error Correction (FEC) in the current packet; 0/1 */ + opus_int LBRR_coded; + + /* I: Flag to enable discontinuous transmission (DTX); 0/1 */ + opus_int useDTX; + + /* I: Flag to use constant bitrate */ + opus_int useCBR; + + /* I: Maximum number of bits allowed for the frame */ + opus_int maxBits; + + /* I: Causes a smooth downmix to mono */ + opus_int toMono; + + /* I: Opus encoder is allowing us to switch bandwidth */ + opus_int opusCanSwitch; + + /* I: Make frames as independent as possible (but still use LPC) */ + opus_int reducedDependency; + + /* O: Internal sampling rate used, in Hertz; 8000/12000/16000 */ + opus_int32 internalSampleRate; + + /* O: Flag that bandwidth switching is allowed (because low voice activity) */ + opus_int allowBandwidthSwitch; + + /* O: Flag that SILK runs in WB mode without variable LP filter (use for switching between WB/SWB/FB) */ + opus_int inWBmodeWithoutVariableLP; + + /* O: Stereo width */ + opus_int stereoWidth_Q14; + + /* O: Tells the Opus encoder we're ready to switch */ + opus_int switchReady; + + /* O: SILK Signal type */ + opus_int signalType; + + /* O: SILK offset (dithering) */ + opus_int offset; +} silk_EncControlStruct; + +/**************************************************************************/ +/* Structure for controlling decoder operation and reading decoder status */ +/**************************************************************************/ +typedef struct { + /* I: Number of channels; 1/2 */ + opus_int32 nChannelsAPI; + + /* I: Number of channels; 1/2 */ + opus_int32 nChannelsInternal; + + /* I: Output signal sampling rate in Hertz; 8000/12000/16000/24000/32000/44100/48000 */ + opus_int32 API_sampleRate; + + /* I: Internal sampling rate used, in Hertz; 8000/12000/16000 */ + opus_int32 internalSampleRate; + + /* I: Number of samples per packet in milliseconds; 10/20/40/60 */ + opus_int payloadSize_ms; + + /* O: Pitch lag of previous frame (0 if unvoiced), measured in samples at 48 kHz */ + opus_int prevPitchLag; + + /* I: Enable Deep PLC */ + opus_int enable_deep_plc; + +#ifdef ENABLE_OSCE + /* I: OSCE method */ + opus_int osce_method; +#endif +} silk_DecControlStruct; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/port/third_party/opus/silk/control_SNR.c b/port/third_party/opus/silk/control_SNR.c new file mode 100644 index 00000000..9a6db275 --- /dev/null +++ b/port/third_party/opus/silk/control_SNR.c @@ -0,0 +1,113 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "tuning_parameters.h" + +/* These tables hold SNR values divided by 21 (so they fit in 8 bits) + for different target bitrates spaced at 400 bps interval. The first + 10 values are omitted (0-4 kb/s) because they're all zeros. + These tables were obtained by running different SNRs through the + encoder and measuring the active bitrate. */ +static const unsigned char silk_TargetRate_NB_21[117 - 10] = { + 0, 15, 39, 52, 61, 68, + 74, 79, 84, 88, 92, 95, 99,102,105,108,111,114,117,119,122,124, + 126,129,131,133,135,137,139,142,143,145,147,149,151,153,155,157, + 158,160,162,163,165,167,168,170,171,173,174,176,177,179,180,182, + 183,185,186,187,189,190,192,193,194,196,197,199,200,201,203,204, + 205,207,208,209,211,212,213,215,216,217,219,220,221,223,224,225, + 227,228,230,231,232,234,235,236,238,239,241,242,243,245,246,248, + 249,250,252,253,255 +}; + +static const unsigned char silk_TargetRate_MB_21[165 - 10] = { + 0, 0, 28, 43, 52, 59, + 65, 70, 74, 78, 81, 85, 87, 90, 93, 95, 98,100,102,105,107,109, + 111,113,115,116,118,120,122,123,125,127,128,130,131,133,134,136, + 137,138,140,141,143,144,145,147,148,149,151,152,153,154,156,157, + 158,159,160,162,163,164,165,166,167,168,169,171,172,173,174,175, + 176,177,178,179,180,181,182,183,184,185,186,187,188,188,189,190, + 191,192,193,194,195,196,197,198,199,200,201,202,203,203,204,205, + 206,207,208,209,210,211,212,213,214,214,215,216,217,218,219,220, + 221,222,223,224,224,225,226,227,228,229,230,231,232,233,234,235, + 236,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250, + 251,252,253,254,255 +}; + +static const unsigned char silk_TargetRate_WB_21[201 - 10] = { + 0, 0, 0, 8, 29, 41, + 49, 56, 62, 66, 70, 74, 77, 80, 83, 86, 88, 91, 93, 95, 97, 99, + 101,103,105,107,108,110,112,113,115,116,118,119,121,122,123,125, + 126,127,129,130,131,132,134,135,136,137,138,140,141,142,143,144, + 145,146,147,148,149,150,151,152,153,154,156,157,158,159,159,160, + 161,162,163,164,165,166,167,168,169,170,171,171,172,173,174,175, + 176,177,177,178,179,180,181,181,182,183,184,185,185,186,187,188, + 189,189,190,191,192,192,193,194,195,195,196,197,198,198,199,200, + 200,201,202,203,203,204,205,206,206,207,208,209,209,210,211,211, + 212,213,214,214,215,216,216,217,218,219,219,220,221,221,222,223, + 224,224,225,226,226,227,228,229,229,230,231,232,232,233,234,234, + 235,236,237,237,238,239,240,240,241,242,243,243,244,245,246,246, + 247,248,249,249,250,251,252,253,255 +}; + +/* Control SNR of redidual quantizer */ +opus_int silk_control_SNR( + silk_encoder_state *psEncC, /* I/O Pointer to Silk encoder state */ + opus_int32 TargetRate_bps /* I Target max bitrate (bps) */ +) +{ + int id; + int bound; + const unsigned char *snr_table; + + psEncC->TargetRate_bps = TargetRate_bps; + if( psEncC->nb_subfr == 2 ) { + TargetRate_bps -= 2000 + psEncC->fs_kHz/16; + } + if( psEncC->fs_kHz == 8 ) { + bound = sizeof(silk_TargetRate_NB_21); + snr_table = silk_TargetRate_NB_21; + } else if( psEncC->fs_kHz == 12 ) { + bound = sizeof(silk_TargetRate_MB_21); + snr_table = silk_TargetRate_MB_21; + } else { + bound = sizeof(silk_TargetRate_WB_21); + snr_table = silk_TargetRate_WB_21; + } + id = (TargetRate_bps+200)/400; + id = silk_min(id - 10, bound-1); + if( id <= 0 ) { + psEncC->SNR_dB_Q7 = 0; + } else { + psEncC->SNR_dB_Q7 = snr_table[id]*21; + } + return SILK_NO_ERROR; +} diff --git a/port/third_party/opus/silk/control_audio_bandwidth.c b/port/third_party/opus/silk/control_audio_bandwidth.c new file mode 100644 index 00000000..f6d22d83 --- /dev/null +++ b/port/third_party/opus/silk/control_audio_bandwidth.c @@ -0,0 +1,132 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "tuning_parameters.h" + +/* Control internal sampling rate */ +opus_int silk_control_audio_bandwidth( + silk_encoder_state *psEncC, /* I/O Pointer to Silk encoder state */ + silk_EncControlStruct *encControl /* I Control structure */ +) +{ + opus_int fs_kHz; + opus_int orig_kHz; + opus_int32 fs_Hz; + + orig_kHz = psEncC->fs_kHz; + /* Handle a bandwidth-switching reset where we need to be aware what the last sampling rate was. */ + if( orig_kHz == 0 ) { + orig_kHz = psEncC->sLP.saved_fs_kHz; + } + fs_kHz = orig_kHz; + fs_Hz = silk_SMULBB( fs_kHz, 1000 ); + if( fs_Hz == 0 ) { + /* Encoder has just been initialized */ + fs_Hz = silk_min( psEncC->desiredInternal_fs_Hz, psEncC->API_fs_Hz ); + fs_kHz = silk_DIV32_16( fs_Hz, 1000 ); + } else if( fs_Hz > psEncC->API_fs_Hz || fs_Hz > psEncC->maxInternal_fs_Hz || fs_Hz < psEncC->minInternal_fs_Hz ) { + /* Make sure internal rate is not higher than external rate or maximum allowed, or lower than minimum allowed */ + fs_Hz = psEncC->API_fs_Hz; + fs_Hz = silk_min( fs_Hz, psEncC->maxInternal_fs_Hz ); + fs_Hz = silk_max( fs_Hz, psEncC->minInternal_fs_Hz ); + fs_kHz = silk_DIV32_16( fs_Hz, 1000 ); + } else { + /* State machine for the internal sampling rate switching */ + if( psEncC->sLP.transition_frame_no >= TRANSITION_FRAMES ) { + /* Stop transition phase */ + psEncC->sLP.mode = 0; + } + if( psEncC->allow_bandwidth_switch || encControl->opusCanSwitch ) { + /* Check if we should switch down */ + if( silk_SMULBB( orig_kHz, 1000 ) > psEncC->desiredInternal_fs_Hz ) + { + /* Switch down */ + if( psEncC->sLP.mode == 0 ) { + /* New transition */ + psEncC->sLP.transition_frame_no = TRANSITION_FRAMES; + + /* Reset transition filter state */ + silk_memset( psEncC->sLP.In_LP_State, 0, sizeof( psEncC->sLP.In_LP_State ) ); + } + if( encControl->opusCanSwitch ) { + /* Stop transition phase */ + psEncC->sLP.mode = 0; + + /* Switch to a lower sample frequency */ + fs_kHz = orig_kHz == 16 ? 12 : 8; + } else { + if( psEncC->sLP.transition_frame_no <= 0 ) { + encControl->switchReady = 1; + /* Make room for redundancy */ + encControl->maxBits -= encControl->maxBits * 5 / ( encControl->payloadSize_ms + 5 ); + } else { + /* Direction: down (at double speed) */ + psEncC->sLP.mode = -2; + } + } + } + else + /* Check if we should switch up */ + if( silk_SMULBB( orig_kHz, 1000 ) < psEncC->desiredInternal_fs_Hz ) + { + /* Switch up */ + if( encControl->opusCanSwitch ) { + /* Switch to a higher sample frequency */ + fs_kHz = orig_kHz == 8 ? 12 : 16; + + /* New transition */ + psEncC->sLP.transition_frame_no = 0; + + /* Reset transition filter state */ + silk_memset( psEncC->sLP.In_LP_State, 0, sizeof( psEncC->sLP.In_LP_State ) ); + + /* Direction: up */ + psEncC->sLP.mode = 1; + } else { + if( psEncC->sLP.mode == 0 ) { + encControl->switchReady = 1; + /* Make room for redundancy */ + encControl->maxBits -= encControl->maxBits * 5 / ( encControl->payloadSize_ms + 5 ); + } else { + /* Direction: up */ + psEncC->sLP.mode = 1; + } + } + } else { + if (psEncC->sLP.mode<0) + psEncC->sLP.mode = 1; + } + } + } + + return fs_kHz; +} diff --git a/port/third_party/opus/silk/control_codec.c b/port/third_party/opus/silk/control_codec.c new file mode 100644 index 00000000..784ffe66 --- /dev/null +++ b/port/third_party/opus/silk/control_codec.c @@ -0,0 +1,423 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#ifdef FIXED_POINT +#include "main_FIX.h" +#define silk_encoder_state_Fxx silk_encoder_state_FIX +#else +#include "main_FLP.h" +#define silk_encoder_state_Fxx silk_encoder_state_FLP +#endif +#include "stack_alloc.h" +#include "tuning_parameters.h" +#include "pitch_est_defines.h" + +static opus_int silk_setup_resamplers( + silk_encoder_state_Fxx *psEnc, /* I/O */ + opus_int fs_kHz /* I */ +); + +static opus_int silk_setup_fs( + silk_encoder_state_Fxx *psEnc, /* I/O */ + opus_int fs_kHz, /* I */ + opus_int PacketSize_ms /* I */ +); + +static opus_int silk_setup_complexity( + silk_encoder_state *psEncC, /* I/O */ + opus_int Complexity /* I */ +); + +static OPUS_INLINE opus_int silk_setup_LBRR( + silk_encoder_state *psEncC, /* I/O */ + const silk_EncControlStruct *encControl /* I */ +); + + +/* Control encoder */ +opus_int silk_control_encoder( + silk_encoder_state_Fxx *psEnc, /* I/O Pointer to Silk encoder state */ + silk_EncControlStruct *encControl, /* I Control structure */ + const opus_int allow_bw_switch, /* I Flag to allow switching audio bandwidth */ + const opus_int channelNb, /* I Channel number */ + const opus_int force_fs_kHz +) +{ + opus_int fs_kHz, ret = 0; + + psEnc->sCmn.useDTX = encControl->useDTX; + psEnc->sCmn.useCBR = encControl->useCBR; + psEnc->sCmn.API_fs_Hz = encControl->API_sampleRate; + psEnc->sCmn.maxInternal_fs_Hz = encControl->maxInternalSampleRate; + psEnc->sCmn.minInternal_fs_Hz = encControl->minInternalSampleRate; + psEnc->sCmn.desiredInternal_fs_Hz = encControl->desiredInternalSampleRate; + psEnc->sCmn.useInBandFEC = encControl->useInBandFEC; + psEnc->sCmn.nChannelsAPI = encControl->nChannelsAPI; + psEnc->sCmn.nChannelsInternal = encControl->nChannelsInternal; + psEnc->sCmn.allow_bandwidth_switch = allow_bw_switch; + psEnc->sCmn.channelNb = channelNb; + + if( psEnc->sCmn.controlled_since_last_payload != 0 && psEnc->sCmn.prefillFlag == 0 ) { + if( psEnc->sCmn.API_fs_Hz != psEnc->sCmn.prev_API_fs_Hz && psEnc->sCmn.fs_kHz > 0 ) { + /* Change in API sampling rate in the middle of encoding a packet */ + ret += silk_setup_resamplers( psEnc, psEnc->sCmn.fs_kHz ); + } + return ret; + } + + /* Beyond this point we know that there are no previously coded frames in the payload buffer */ + + /********************************************/ + /* Determine internal sampling rate */ + /********************************************/ + fs_kHz = silk_control_audio_bandwidth( &psEnc->sCmn, encControl ); + if( force_fs_kHz ) { + fs_kHz = force_fs_kHz; + } + /********************************************/ + /* Prepare resampler and buffered data */ + /********************************************/ + ret += silk_setup_resamplers( psEnc, fs_kHz ); + + /********************************************/ + /* Set internal sampling frequency */ + /********************************************/ + ret += silk_setup_fs( psEnc, fs_kHz, encControl->payloadSize_ms ); + + /********************************************/ + /* Set encoding complexity */ + /********************************************/ + ret += silk_setup_complexity( &psEnc->sCmn, encControl->complexity ); + + /********************************************/ + /* Set packet loss rate measured by farend */ + /********************************************/ + psEnc->sCmn.PacketLoss_perc = encControl->packetLossPercentage; + + /********************************************/ + /* Set LBRR usage */ + /********************************************/ + ret += silk_setup_LBRR( &psEnc->sCmn, encControl ); + + psEnc->sCmn.controlled_since_last_payload = 1; + + return ret; +} + +static opus_int silk_setup_resamplers( + silk_encoder_state_Fxx *psEnc, /* I/O */ + opus_int fs_kHz /* I */ +) +{ + opus_int ret = SILK_NO_ERROR; + SAVE_STACK; + + if( psEnc->sCmn.fs_kHz != fs_kHz || psEnc->sCmn.prev_API_fs_Hz != psEnc->sCmn.API_fs_Hz ) + { + if( psEnc->sCmn.fs_kHz == 0 ) { + /* Initialize the resampler for enc_API.c preparing resampling from API_fs_Hz to fs_kHz */ + ret += silk_resampler_init( &psEnc->sCmn.resampler_state, psEnc->sCmn.API_fs_Hz, fs_kHz * 1000, 1 ); + } else { + VARDECL( opus_int16, x_buf_API_fs_Hz ); + VARDECL( silk_resampler_state_struct, temp_resampler_state ); +#ifdef FIXED_POINT + opus_int16 *x_bufFIX = psEnc->x_buf; +#else + VARDECL( opus_int16, x_bufFIX ); + opus_int32 new_buf_samples; +#endif + opus_int32 api_buf_samples; + opus_int32 old_buf_samples; + opus_int32 buf_length_ms; + + buf_length_ms = silk_LSHIFT( psEnc->sCmn.nb_subfr * 5, 1 ) + LA_SHAPE_MS; + old_buf_samples = buf_length_ms * psEnc->sCmn.fs_kHz; + +#ifndef FIXED_POINT + new_buf_samples = buf_length_ms * fs_kHz; + ALLOC( x_bufFIX, silk_max( old_buf_samples, new_buf_samples ), + opus_int16 ); + silk_float2short_array( x_bufFIX, psEnc->x_buf, old_buf_samples ); +#endif + + /* Initialize resampler for temporary resampling of x_buf data to API_fs_Hz */ + ALLOC( temp_resampler_state, 1, silk_resampler_state_struct ); + ret += silk_resampler_init( temp_resampler_state, silk_SMULBB( psEnc->sCmn.fs_kHz, 1000 ), psEnc->sCmn.API_fs_Hz, 0 ); + + /* Calculate number of samples to temporarily upsample */ + api_buf_samples = buf_length_ms * silk_DIV32_16( psEnc->sCmn.API_fs_Hz, 1000 ); + + /* Temporary resampling of x_buf data to API_fs_Hz */ + ALLOC( x_buf_API_fs_Hz, api_buf_samples, opus_int16 ); + ret += silk_resampler( temp_resampler_state, x_buf_API_fs_Hz, x_bufFIX, old_buf_samples ); + + /* Initialize the resampler for enc_API.c preparing resampling from API_fs_Hz to fs_kHz */ + ret += silk_resampler_init( &psEnc->sCmn.resampler_state, psEnc->sCmn.API_fs_Hz, silk_SMULBB( fs_kHz, 1000 ), 1 ); + + /* Correct resampler state by resampling buffered data from API_fs_Hz to fs_kHz */ + ret += silk_resampler( &psEnc->sCmn.resampler_state, x_bufFIX, x_buf_API_fs_Hz, api_buf_samples ); + +#ifndef FIXED_POINT + silk_short2float_array( psEnc->x_buf, x_bufFIX, new_buf_samples); +#endif + } + } + + psEnc->sCmn.prev_API_fs_Hz = psEnc->sCmn.API_fs_Hz; + + RESTORE_STACK; + return ret; +} + +static opus_int silk_setup_fs( + silk_encoder_state_Fxx *psEnc, /* I/O */ + opus_int fs_kHz, /* I */ + opus_int PacketSize_ms /* I */ +) +{ + opus_int ret = SILK_NO_ERROR; + + /* Set packet size */ + if( PacketSize_ms != psEnc->sCmn.PacketSize_ms ) { + if( ( PacketSize_ms != 10 ) && + ( PacketSize_ms != 20 ) && + ( PacketSize_ms != 40 ) && + ( PacketSize_ms != 60 ) ) { + ret = SILK_ENC_PACKET_SIZE_NOT_SUPPORTED; + } + if( PacketSize_ms <= 10 ) { + psEnc->sCmn.nFramesPerPacket = 1; + psEnc->sCmn.nb_subfr = PacketSize_ms == 10 ? 2 : 1; + psEnc->sCmn.frame_length = silk_SMULBB( PacketSize_ms, fs_kHz ); + psEnc->sCmn.pitch_LPC_win_length = silk_SMULBB( FIND_PITCH_LPC_WIN_MS_2_SF, fs_kHz ); + if( psEnc->sCmn.fs_kHz == 8 ) { + psEnc->sCmn.pitch_contour_iCDF = silk_pitch_contour_10_ms_NB_iCDF; + } else { + psEnc->sCmn.pitch_contour_iCDF = silk_pitch_contour_10_ms_iCDF; + } + } else { + psEnc->sCmn.nFramesPerPacket = silk_DIV32_16( PacketSize_ms, MAX_FRAME_LENGTH_MS ); + psEnc->sCmn.nb_subfr = MAX_NB_SUBFR; + psEnc->sCmn.frame_length = silk_SMULBB( 20, fs_kHz ); + psEnc->sCmn.pitch_LPC_win_length = silk_SMULBB( FIND_PITCH_LPC_WIN_MS, fs_kHz ); + if( psEnc->sCmn.fs_kHz == 8 ) { + psEnc->sCmn.pitch_contour_iCDF = silk_pitch_contour_NB_iCDF; + } else { + psEnc->sCmn.pitch_contour_iCDF = silk_pitch_contour_iCDF; + } + } + psEnc->sCmn.PacketSize_ms = PacketSize_ms; + psEnc->sCmn.TargetRate_bps = 0; /* trigger new SNR computation */ + } + + /* Set internal sampling frequency */ + celt_assert( fs_kHz == 8 || fs_kHz == 12 || fs_kHz == 16 ); + celt_assert( psEnc->sCmn.nb_subfr == 2 || psEnc->sCmn.nb_subfr == 4 ); + if( psEnc->sCmn.fs_kHz != fs_kHz ) { + /* reset part of the state */ + silk_memset( &psEnc->sShape, 0, sizeof( psEnc->sShape ) ); + silk_memset( &psEnc->sCmn.sNSQ, 0, sizeof( psEnc->sCmn.sNSQ ) ); + silk_memset( psEnc->sCmn.prev_NLSFq_Q15, 0, sizeof( psEnc->sCmn.prev_NLSFq_Q15 ) ); + silk_memset( &psEnc->sCmn.sLP.In_LP_State, 0, sizeof( psEnc->sCmn.sLP.In_LP_State ) ); + psEnc->sCmn.inputBufIx = 0; + psEnc->sCmn.nFramesEncoded = 0; + psEnc->sCmn.TargetRate_bps = 0; /* trigger new SNR computation */ + + /* Initialize non-zero parameters */ + psEnc->sCmn.prevLag = 100; + psEnc->sCmn.first_frame_after_reset = 1; + psEnc->sShape.LastGainIndex = 10; + psEnc->sCmn.sNSQ.lagPrev = 100; + psEnc->sCmn.sNSQ.prev_gain_Q16 = 65536; + psEnc->sCmn.prevSignalType = TYPE_NO_VOICE_ACTIVITY; + + psEnc->sCmn.fs_kHz = fs_kHz; + if( psEnc->sCmn.fs_kHz == 8 ) { + if( psEnc->sCmn.nb_subfr == MAX_NB_SUBFR ) { + psEnc->sCmn.pitch_contour_iCDF = silk_pitch_contour_NB_iCDF; + } else { + psEnc->sCmn.pitch_contour_iCDF = silk_pitch_contour_10_ms_NB_iCDF; + } + } else { + if( psEnc->sCmn.nb_subfr == MAX_NB_SUBFR ) { + psEnc->sCmn.pitch_contour_iCDF = silk_pitch_contour_iCDF; + } else { + psEnc->sCmn.pitch_contour_iCDF = silk_pitch_contour_10_ms_iCDF; + } + } + if( psEnc->sCmn.fs_kHz == 8 || psEnc->sCmn.fs_kHz == 12 ) { + psEnc->sCmn.predictLPCOrder = MIN_LPC_ORDER; + psEnc->sCmn.psNLSF_CB = &silk_NLSF_CB_NB_MB; + } else { + psEnc->sCmn.predictLPCOrder = MAX_LPC_ORDER; + psEnc->sCmn.psNLSF_CB = &silk_NLSF_CB_WB; + } + psEnc->sCmn.subfr_length = SUB_FRAME_LENGTH_MS * fs_kHz; + psEnc->sCmn.frame_length = silk_SMULBB( psEnc->sCmn.subfr_length, psEnc->sCmn.nb_subfr ); + psEnc->sCmn.ltp_mem_length = silk_SMULBB( LTP_MEM_LENGTH_MS, fs_kHz ); + psEnc->sCmn.la_pitch = silk_SMULBB( LA_PITCH_MS, fs_kHz ); + psEnc->sCmn.max_pitch_lag = silk_SMULBB( 18, fs_kHz ); + if( psEnc->sCmn.nb_subfr == MAX_NB_SUBFR ) { + psEnc->sCmn.pitch_LPC_win_length = silk_SMULBB( FIND_PITCH_LPC_WIN_MS, fs_kHz ); + } else { + psEnc->sCmn.pitch_LPC_win_length = silk_SMULBB( FIND_PITCH_LPC_WIN_MS_2_SF, fs_kHz ); + } + if( psEnc->sCmn.fs_kHz == 16 ) { + psEnc->sCmn.pitch_lag_low_bits_iCDF = silk_uniform8_iCDF; + } else if( psEnc->sCmn.fs_kHz == 12 ) { + psEnc->sCmn.pitch_lag_low_bits_iCDF = silk_uniform6_iCDF; + } else { + psEnc->sCmn.pitch_lag_low_bits_iCDF = silk_uniform4_iCDF; + } + } + + /* Check that settings are valid */ + celt_assert( ( psEnc->sCmn.subfr_length * psEnc->sCmn.nb_subfr ) == psEnc->sCmn.frame_length ); + + return ret; +} + +static opus_int silk_setup_complexity( + silk_encoder_state *psEncC, /* I/O */ + opus_int Complexity /* I */ +) +{ + opus_int ret = 0; + + /* Set encoding complexity */ + celt_assert( Complexity >= 0 && Complexity <= 10 ); + if( Complexity < 1 ) { + psEncC->pitchEstimationComplexity = SILK_PE_MIN_COMPLEX; + psEncC->pitchEstimationThreshold_Q16 = SILK_FIX_CONST( 0.8, 16 ); + psEncC->pitchEstimationLPCOrder = 6; + psEncC->shapingLPCOrder = 12; + psEncC->la_shape = 3 * psEncC->fs_kHz; + psEncC->nStatesDelayedDecision = 1; + psEncC->useInterpolatedNLSFs = 0; + psEncC->NLSF_MSVQ_Survivors = 2; + psEncC->warping_Q16 = 0; + } else if( Complexity < 2 ) { + psEncC->pitchEstimationComplexity = SILK_PE_MID_COMPLEX; + psEncC->pitchEstimationThreshold_Q16 = SILK_FIX_CONST( 0.76, 16 ); + psEncC->pitchEstimationLPCOrder = 8; + psEncC->shapingLPCOrder = 14; + psEncC->la_shape = 5 * psEncC->fs_kHz; + psEncC->nStatesDelayedDecision = 1; + psEncC->useInterpolatedNLSFs = 0; + psEncC->NLSF_MSVQ_Survivors = 3; + psEncC->warping_Q16 = 0; + } else if( Complexity < 3 ) { + psEncC->pitchEstimationComplexity = SILK_PE_MIN_COMPLEX; + psEncC->pitchEstimationThreshold_Q16 = SILK_FIX_CONST( 0.8, 16 ); + psEncC->pitchEstimationLPCOrder = 6; + psEncC->shapingLPCOrder = 12; + psEncC->la_shape = 3 * psEncC->fs_kHz; + psEncC->nStatesDelayedDecision = 2; + psEncC->useInterpolatedNLSFs = 0; + psEncC->NLSF_MSVQ_Survivors = 2; + psEncC->warping_Q16 = 0; + } else if( Complexity < 4 ) { + psEncC->pitchEstimationComplexity = SILK_PE_MID_COMPLEX; + psEncC->pitchEstimationThreshold_Q16 = SILK_FIX_CONST( 0.76, 16 ); + psEncC->pitchEstimationLPCOrder = 8; + psEncC->shapingLPCOrder = 14; + psEncC->la_shape = 5 * psEncC->fs_kHz; + psEncC->nStatesDelayedDecision = 2; + psEncC->useInterpolatedNLSFs = 0; + psEncC->NLSF_MSVQ_Survivors = 4; + psEncC->warping_Q16 = 0; + } else if( Complexity < 6 ) { + psEncC->pitchEstimationComplexity = SILK_PE_MID_COMPLEX; + psEncC->pitchEstimationThreshold_Q16 = SILK_FIX_CONST( 0.74, 16 ); + psEncC->pitchEstimationLPCOrder = 10; + psEncC->shapingLPCOrder = 16; + psEncC->la_shape = 5 * psEncC->fs_kHz; + psEncC->nStatesDelayedDecision = 2; + psEncC->useInterpolatedNLSFs = 1; + psEncC->NLSF_MSVQ_Survivors = 6; + psEncC->warping_Q16 = psEncC->fs_kHz * SILK_FIX_CONST( WARPING_MULTIPLIER, 16 ); + } else if( Complexity < 8 ) { + psEncC->pitchEstimationComplexity = SILK_PE_MID_COMPLEX; + psEncC->pitchEstimationThreshold_Q16 = SILK_FIX_CONST( 0.72, 16 ); + psEncC->pitchEstimationLPCOrder = 12; + psEncC->shapingLPCOrder = 20; + psEncC->la_shape = 5 * psEncC->fs_kHz; + psEncC->nStatesDelayedDecision = 3; + psEncC->useInterpolatedNLSFs = 1; + psEncC->NLSF_MSVQ_Survivors = 8; + psEncC->warping_Q16 = psEncC->fs_kHz * SILK_FIX_CONST( WARPING_MULTIPLIER, 16 ); + } else { + psEncC->pitchEstimationComplexity = SILK_PE_MAX_COMPLEX; + psEncC->pitchEstimationThreshold_Q16 = SILK_FIX_CONST( 0.7, 16 ); + psEncC->pitchEstimationLPCOrder = 16; + psEncC->shapingLPCOrder = 24; + psEncC->la_shape = 5 * psEncC->fs_kHz; + psEncC->nStatesDelayedDecision = MAX_DEL_DEC_STATES; + psEncC->useInterpolatedNLSFs = 1; + psEncC->NLSF_MSVQ_Survivors = 16; + psEncC->warping_Q16 = psEncC->fs_kHz * SILK_FIX_CONST( WARPING_MULTIPLIER, 16 ); + } + + /* Do not allow higher pitch estimation LPC order than predict LPC order */ + psEncC->pitchEstimationLPCOrder = silk_min_int( psEncC->pitchEstimationLPCOrder, psEncC->predictLPCOrder ); + psEncC->shapeWinLength = SUB_FRAME_LENGTH_MS * psEncC->fs_kHz + 2 * psEncC->la_shape; + psEncC->Complexity = Complexity; + + celt_assert( psEncC->pitchEstimationLPCOrder <= MAX_FIND_PITCH_LPC_ORDER ); + celt_assert( psEncC->shapingLPCOrder <= MAX_SHAPE_LPC_ORDER ); + celt_assert( psEncC->nStatesDelayedDecision <= MAX_DEL_DEC_STATES ); + celt_assert( psEncC->warping_Q16 <= 32767 ); + celt_assert( psEncC->la_shape <= LA_SHAPE_MAX ); + celt_assert( psEncC->shapeWinLength <= SHAPE_LPC_WIN_MAX ); + + return ret; +} + +static OPUS_INLINE opus_int silk_setup_LBRR( + silk_encoder_state *psEncC, /* I/O */ + const silk_EncControlStruct *encControl /* I */ +) +{ + opus_int LBRR_in_previous_packet, ret = SILK_NO_ERROR; + + LBRR_in_previous_packet = psEncC->LBRR_enabled; + psEncC->LBRR_enabled = encControl->LBRR_coded; + if( psEncC->LBRR_enabled ) { + /* Set gain increase for coding LBRR excitation */ + if( LBRR_in_previous_packet == 0 ) { + /* Previous packet did not have LBRR, and was therefore coded at a higher bitrate */ + psEncC->LBRR_GainIncreases = 7; + } else { + psEncC->LBRR_GainIncreases = silk_max_int( 7 - silk_SMULWB( (opus_int32)psEncC->PacketLoss_perc, SILK_FIX_CONST( 0.2, 16 ) ), 3 ); + } + } + + return ret; +} diff --git a/port/third_party/opus/silk/debug.c b/port/third_party/opus/silk/debug.c new file mode 100644 index 00000000..46a24a47 --- /dev/null +++ b/port/third_party/opus/silk/debug.c @@ -0,0 +1,174 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +typedef int prevent_empty_translation_unit_warning; + +#include "debug.h" + +#if SILK_DEBUG || SILK_TIC_TOC +#include "SigProc_FIX.h" +#endif + +#if SILK_TIC_TOC + +#if (defined(_WIN32) || defined(_WINCE)) +#include /* timer */ +#else /* Linux or Mac*/ +#include +#endif + +#ifdef _WIN32 +unsigned long silk_GetHighResolutionTime(void) /* O time in usec*/ +{ + /* Returns a time counter in microsec */ + /* the resolution is platform dependent */ + /* but is typically 1.62 us resolution */ + LARGE_INTEGER lpPerformanceCount; + LARGE_INTEGER lpFrequency; + QueryPerformanceCounter(&lpPerformanceCount); + QueryPerformanceFrequency(&lpFrequency); + return (unsigned long)((1000000*(lpPerformanceCount.QuadPart)) / lpFrequency.QuadPart); +} +#else /* Linux or Mac*/ +unsigned long GetHighResolutionTime(void) /* O time in usec*/ +{ + struct timeval tv; + gettimeofday(&tv, 0); + return((tv.tv_sec*1000000)+(tv.tv_usec)); +} +#endif + +int silk_Timer_nTimers = 0; +int silk_Timer_depth_ctr = 0; +char silk_Timer_tags[silk_NUM_TIMERS_MAX][silk_NUM_TIMERS_MAX_TAG_LEN]; +#ifdef _WIN32 +LARGE_INTEGER silk_Timer_start[silk_NUM_TIMERS_MAX]; +#else +unsigned long silk_Timer_start[silk_NUM_TIMERS_MAX]; +#endif +unsigned int silk_Timer_cnt[silk_NUM_TIMERS_MAX]; +opus_int64 silk_Timer_min[silk_NUM_TIMERS_MAX]; +opus_int64 silk_Timer_sum[silk_NUM_TIMERS_MAX]; +opus_int64 silk_Timer_max[silk_NUM_TIMERS_MAX]; +opus_int64 silk_Timer_depth[silk_NUM_TIMERS_MAX]; + +#ifdef _WIN32 +void silk_TimerSave(char *file_name) +{ + if( silk_Timer_nTimers > 0 ) + { + int k; + FILE *fp; + LARGE_INTEGER lpFrequency; + LARGE_INTEGER lpPerformanceCount1, lpPerformanceCount2; + int del = 0x7FFFFFFF; + double avg, sum_avg; + /* estimate overhead of calling performance counters */ + for( k = 0; k < 1000; k++ ) { + QueryPerformanceCounter(&lpPerformanceCount1); + QueryPerformanceCounter(&lpPerformanceCount2); + lpPerformanceCount2.QuadPart -= lpPerformanceCount1.QuadPart; + if( (int)lpPerformanceCount2.LowPart < del ) + del = lpPerformanceCount2.LowPart; + } + QueryPerformanceFrequency(&lpFrequency); + /* print results to file */ + sum_avg = 0.0f; + for( k = 0; k < silk_Timer_nTimers; k++ ) { + if (silk_Timer_depth[k] == 0) { + sum_avg += (1e6 * silk_Timer_sum[k] / silk_Timer_cnt[k] - del) / lpFrequency.QuadPart * silk_Timer_cnt[k]; + } + } + fp = fopen(file_name, "w"); + fprintf(fp, " min avg %% max count\n"); + for( k = 0; k < silk_Timer_nTimers; k++ ) { + if (silk_Timer_depth[k] == 0) { + fprintf(fp, "%-28s", silk_Timer_tags[k]); + } else if (silk_Timer_depth[k] == 1) { + fprintf(fp, " %-27s", silk_Timer_tags[k]); + } else if (silk_Timer_depth[k] == 2) { + fprintf(fp, " %-26s", silk_Timer_tags[k]); + } else if (silk_Timer_depth[k] == 3) { + fprintf(fp, " %-25s", silk_Timer_tags[k]); + } else { + fprintf(fp, " %-24s", silk_Timer_tags[k]); + } + avg = (1e6 * silk_Timer_sum[k] / silk_Timer_cnt[k] - del) / lpFrequency.QuadPart; + fprintf(fp, "%8.2f", (1e6 * (silk_max_64(silk_Timer_min[k] - del, 0))) / lpFrequency.QuadPart); + fprintf(fp, "%12.2f %6.2f", avg, 100.0 * avg / sum_avg * silk_Timer_cnt[k]); + fprintf(fp, "%12.2f", (1e6 * (silk_max_64(silk_Timer_max[k] - del, 0))) / lpFrequency.QuadPart); + fprintf(fp, "%10d\n", silk_Timer_cnt[k]); + } + fprintf(fp, " microseconds\n"); + fclose(fp); + } +} +#else +void silk_TimerSave(char *file_name) +{ + if( silk_Timer_nTimers > 0 ) + { + int k; + FILE *fp; + /* print results to file */ + fp = fopen(file_name, "w"); + fprintf(fp, " min avg max count\n"); + for( k = 0; k < silk_Timer_nTimers; k++ ) + { + if (silk_Timer_depth[k] == 0) { + fprintf(fp, "%-28s", silk_Timer_tags[k]); + } else if (silk_Timer_depth[k] == 1) { + fprintf(fp, " %-27s", silk_Timer_tags[k]); + } else if (silk_Timer_depth[k] == 2) { + fprintf(fp, " %-26s", silk_Timer_tags[k]); + } else if (silk_Timer_depth[k] == 3) { + fprintf(fp, " %-25s", silk_Timer_tags[k]); + } else { + fprintf(fp, " %-24s", silk_Timer_tags[k]); + } + fprintf(fp, "%d ", silk_Timer_min[k]); + fprintf(fp, "%f ", (double)silk_Timer_sum[k] / (double)silk_Timer_cnt[k]); + fprintf(fp, "%d ", silk_Timer_max[k]); + fprintf(fp, "%10d\n", silk_Timer_cnt[k]); + } + fprintf(fp, " microseconds\n"); + fclose(fp); + } +} +#endif + +#endif /* SILK_TIC_TOC */ + +#if SILK_DEBUG +FILE *silk_debug_store_fp[ silk_NUM_STORES_MAX ]; +int silk_debug_store_count = 0; +#endif /* SILK_DEBUG */ + diff --git a/port/third_party/opus/silk/debug.h b/port/third_party/opus/silk/debug.h new file mode 100644 index 00000000..36163e47 --- /dev/null +++ b/port/third_party/opus/silk/debug.h @@ -0,0 +1,267 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_DEBUG_H +#define SILK_DEBUG_H + +/* Set to 1 to enable DEBUG_STORE_DATA() macros for dumping + * intermediate signals from the codec. + */ +#define SILK_DEBUG 0 + +/* Flag for using timers */ +#define SILK_TIC_TOC 0 + +#if SILK_DEBUG || SILK_TIC_TOC +#include "typedef.h" +#include /* strcpy, strcmp */ +#include /* file writing */ +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if SILK_TIC_TOC + +unsigned long GetHighResolutionTime(void); /* O time in usec*/ + +#if (defined(_WIN32) || defined(_WINCE)) +#include /* timer */ +#else /* Linux or Mac*/ +#include +#endif + +/*********************************/ +/* timer functions for profiling */ +/*********************************/ +/* example: */ +/* */ +/* TIC(LPC) */ +/* do_LPC(in_vec, order, acoef); // do LPC analysis */ +/* TOC(LPC) */ +/* */ +/* and call the following just before exiting (from main) */ +/* */ +/* silk_TimerSave("silk_TimingData.txt"); */ +/* */ +/* results are now in silk_TimingData.txt */ + +void silk_TimerSave(char *file_name); + +/* max number of timers (in different locations) */ +#define silk_NUM_TIMERS_MAX 50 +/* max length of name tags in TIC(..), TOC(..) */ +#define silk_NUM_TIMERS_MAX_TAG_LEN 30 + +extern int silk_Timer_nTimers; +extern int silk_Timer_depth_ctr; +extern char silk_Timer_tags[silk_NUM_TIMERS_MAX][silk_NUM_TIMERS_MAX_TAG_LEN]; +#ifdef _WIN32 +extern LARGE_INTEGER silk_Timer_start[silk_NUM_TIMERS_MAX]; +#else +extern unsigned long silk_Timer_start[silk_NUM_TIMERS_MAX]; +#endif +extern unsigned int silk_Timer_cnt[silk_NUM_TIMERS_MAX]; +extern opus_int64 silk_Timer_sum[silk_NUM_TIMERS_MAX]; +extern opus_int64 silk_Timer_max[silk_NUM_TIMERS_MAX]; +extern opus_int64 silk_Timer_min[silk_NUM_TIMERS_MAX]; +extern opus_int64 silk_Timer_depth[silk_NUM_TIMERS_MAX]; + +/* WARNING: TIC()/TOC can measure only up to 0.1 seconds at a time */ +#ifdef _WIN32 +#define TIC(TAG_NAME) { \ + static int init = 0; \ + static int ID = -1; \ + if( init == 0 ) \ + { \ + int k; \ + init = 1; \ + for( k = 0; k < silk_Timer_nTimers; k++ ) { \ + if( strcmp(silk_Timer_tags[k], #TAG_NAME) == 0 ) { \ + ID = k; \ + break; \ + } \ + } \ + if (ID == -1) { \ + ID = silk_Timer_nTimers; \ + silk_Timer_nTimers++; \ + silk_Timer_depth[ID] = silk_Timer_depth_ctr; \ + strcpy(silk_Timer_tags[ID], #TAG_NAME); \ + silk_Timer_cnt[ID] = 0; \ + silk_Timer_sum[ID] = 0; \ + silk_Timer_min[ID] = 0xFFFFFFFF; \ + silk_Timer_max[ID] = 0; \ + } \ + } \ + silk_Timer_depth_ctr++; \ + QueryPerformanceCounter(&silk_Timer_start[ID]); \ +} +#else +#define TIC(TAG_NAME) { \ + static int init = 0; \ + static int ID = -1; \ + if( init == 0 ) \ + { \ + int k; \ + init = 1; \ + for( k = 0; k < silk_Timer_nTimers; k++ ) { \ + if( strcmp(silk_Timer_tags[k], #TAG_NAME) == 0 ) { \ + ID = k; \ + break; \ + } \ + } \ + if (ID == -1) { \ + ID = silk_Timer_nTimers; \ + silk_Timer_nTimers++; \ + silk_Timer_depth[ID] = silk_Timer_depth_ctr; \ + strcpy(silk_Timer_tags[ID], #TAG_NAME); \ + silk_Timer_cnt[ID] = 0; \ + silk_Timer_sum[ID] = 0; \ + silk_Timer_min[ID] = 0xFFFFFFFF; \ + silk_Timer_max[ID] = 0; \ + } \ + } \ + silk_Timer_depth_ctr++; \ + silk_Timer_start[ID] = GetHighResolutionTime(); \ +} +#endif + +#ifdef _WIN32 +#define TOC(TAG_NAME) { \ + LARGE_INTEGER lpPerformanceCount; \ + static int init = 0; \ + static int ID = 0; \ + if( init == 0 ) \ + { \ + int k; \ + init = 1; \ + for( k = 0; k < silk_Timer_nTimers; k++ ) { \ + if( strcmp(silk_Timer_tags[k], #TAG_NAME) == 0 ) { \ + ID = k; \ + break; \ + } \ + } \ + } \ + QueryPerformanceCounter(&lpPerformanceCount); \ + lpPerformanceCount.QuadPart -= silk_Timer_start[ID].QuadPart; \ + if((lpPerformanceCount.QuadPart < 100000000) && \ + (lpPerformanceCount.QuadPart >= 0)) { \ + silk_Timer_cnt[ID]++; \ + silk_Timer_sum[ID] += lpPerformanceCount.QuadPart; \ + if( lpPerformanceCount.QuadPart > silk_Timer_max[ID] ) \ + silk_Timer_max[ID] = lpPerformanceCount.QuadPart; \ + if( lpPerformanceCount.QuadPart < silk_Timer_min[ID] ) \ + silk_Timer_min[ID] = lpPerformanceCount.QuadPart; \ + } \ + silk_Timer_depth_ctr--; \ +} +#else +#define TOC(TAG_NAME) { \ + unsigned long endTime; \ + static int init = 0; \ + static int ID = 0; \ + if( init == 0 ) \ + { \ + int k; \ + init = 1; \ + for( k = 0; k < silk_Timer_nTimers; k++ ) { \ + if( strcmp(silk_Timer_tags[k], #TAG_NAME) == 0 ) { \ + ID = k; \ + break; \ + } \ + } \ + } \ + endTime = GetHighResolutionTime(); \ + endTime -= silk_Timer_start[ID]; \ + if((endTime < 100000000) && \ + (endTime >= 0)) { \ + silk_Timer_cnt[ID]++; \ + silk_Timer_sum[ID] += endTime; \ + if( endTime > silk_Timer_max[ID] ) \ + silk_Timer_max[ID] = endTime; \ + if( endTime < silk_Timer_min[ID] ) \ + silk_Timer_min[ID] = endTime; \ + } \ + silk_Timer_depth_ctr--; \ +} +#endif + +#else /* SILK_TIC_TOC */ + +/* define macros as empty strings */ +#define TIC(TAG_NAME) +#define TOC(TAG_NAME) +#define silk_TimerSave(FILE_NAME) + +#endif /* SILK_TIC_TOC */ + + +#if SILK_DEBUG +/************************************/ +/* write data to file for debugging */ +/************************************/ +/* Example: DEBUG_STORE_DATA(testfile.pcm, &RIN[0], 160*sizeof(opus_int16)); */ + +#define silk_NUM_STORES_MAX 100 +extern FILE *silk_debug_store_fp[ silk_NUM_STORES_MAX ]; +extern int silk_debug_store_count; + +/* Faster way of storing the data */ +#define DEBUG_STORE_DATA( FILE_NAME, DATA_PTR, N_BYTES ) { \ + static opus_int init = 0, cnt = 0; \ + static FILE **fp; \ + if (init == 0) { \ + init = 1; \ + cnt = silk_debug_store_count++; \ + silk_debug_store_fp[ cnt ] = fopen(#FILE_NAME, "wb"); \ + } \ + fwrite((DATA_PTR), (N_BYTES), 1, silk_debug_store_fp[ cnt ]); \ +} + +/* Call this at the end of main() */ +#define SILK_DEBUG_STORE_CLOSE_FILES { \ + opus_int i; \ + for( i = 0; i < silk_debug_store_count; i++ ) { \ + fclose( silk_debug_store_fp[ i ] ); \ + } \ +} + +#else /* SILK_DEBUG */ + +/* define macros as empty strings */ +#define DEBUG_STORE_DATA(FILE_NAME, DATA_PTR, N_BYTES) +#define SILK_DEBUG_STORE_CLOSE_FILES + +#endif /* SILK_DEBUG */ + +#ifdef __cplusplus +} +#endif + +#endif /* SILK_DEBUG_H */ diff --git a/port/third_party/opus/silk/dec_API.c b/port/third_party/opus/silk/dec_API.c new file mode 100644 index 00000000..c1091d13 --- /dev/null +++ b/port/third_party/opus/silk/dec_API.c @@ -0,0 +1,486 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include "API.h" +#include "main.h" +#include "stack_alloc.h" +#include "os_support.h" + +#ifdef ENABLE_OSCE +#include "osce.h" +#include "osce_structs.h" +#endif + +/************************/ +/* Decoder Super Struct */ +/************************/ +typedef struct { + silk_decoder_state channel_state[ DECODER_NUM_CHANNELS ]; + stereo_dec_state sStereo; + opus_int nChannelsAPI; + opus_int nChannelsInternal; + opus_int prev_decode_only_middle; +#ifdef ENABLE_OSCE + OSCEModel osce_model; +#endif +} silk_decoder; + +/*********************/ +/* Decoder functions */ +/*********************/ + + + +opus_int silk_LoadOSCEModels(void *decState, const unsigned char *data, int len) +{ +#ifdef ENABLE_OSCE + opus_int ret = SILK_NO_ERROR; + + ret = osce_load_models(&((silk_decoder *)decState)->osce_model, data, len); + ((silk_decoder *)decState)->osce_model.loaded = (ret == 0); + return ret; +#else + (void) decState; + (void) data; + (void) len; + return SILK_NO_ERROR; +#endif +} + +opus_int silk_Get_Decoder_Size( /* O Returns error code */ + opus_int *decSizeBytes /* O Number of bytes in SILK decoder state */ +) +{ + opus_int ret = SILK_NO_ERROR; + + *decSizeBytes = sizeof( silk_decoder ); + + return ret; +} + +/* Reset decoder state */ +opus_int silk_ResetDecoder( /* O Returns error code */ + void *decState /* I/O State */ +) +{ + opus_int n, ret = SILK_NO_ERROR; + silk_decoder_state *channel_state = ((silk_decoder *)decState)->channel_state; + + for( n = 0; n < DECODER_NUM_CHANNELS; n++ ) { + ret = silk_reset_decoder( &channel_state[ n ] ); + } + silk_memset(&((silk_decoder *)decState)->sStereo, 0, sizeof(((silk_decoder *)decState)->sStereo)); + /* Not strictly needed, but it's cleaner that way */ + ((silk_decoder *)decState)->prev_decode_only_middle = 0; + + return ret; +} + + +opus_int silk_InitDecoder( /* O Returns error code */ + void *decState /* I/O State */ +) +{ + opus_int n, ret = SILK_NO_ERROR; + silk_decoder_state *channel_state = ((silk_decoder *)decState)->channel_state; +#ifdef ENABLE_OSCE + ((silk_decoder *)decState)->osce_model.loaded = 0; +#endif +#ifndef USE_WEIGHTS_FILE + /* load osce models */ + silk_LoadOSCEModels(decState, NULL, 0); +#endif + + for( n = 0; n < DECODER_NUM_CHANNELS; n++ ) { + ret = silk_init_decoder( &channel_state[ n ] ); + } + silk_memset(&((silk_decoder *)decState)->sStereo, 0, sizeof(((silk_decoder *)decState)->sStereo)); + /* Not strictly needed, but it's cleaner that way */ + ((silk_decoder *)decState)->prev_decode_only_middle = 0; + + return ret; +} + +/* Decode a frame */ +opus_int silk_Decode( /* O Returns error code */ + void* decState, /* I/O State */ + silk_DecControlStruct* decControl, /* I/O Control Structure */ + opus_int lostFlag, /* I 0: no loss, 1 loss, 2 decode fec */ + opus_int newPacketFlag, /* I Indicates first decoder call for this packet */ + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int16 *samplesOut, /* O Decoded output speech vector */ + opus_int32 *nSamplesOut, /* O Number of samples decoded */ +#ifdef ENABLE_DEEP_PLC + LPCNetPLCState *lpcnet, +#endif + int arch /* I Run-time architecture */ +) +{ + opus_int i, n, decode_only_middle = 0, ret = SILK_NO_ERROR; + opus_int32 nSamplesOutDec, LBRR_symbol; + opus_int16 *samplesOut1_tmp[ 2 ]; + VARDECL( opus_int16, samplesOut1_tmp_storage1 ); + VARDECL( opus_int16, samplesOut1_tmp_storage2 ); + VARDECL( opus_int16, samplesOut2_tmp ); + opus_int32 MS_pred_Q13[ 2 ] = { 0 }; + opus_int16 *resample_out_ptr; + silk_decoder *psDec = ( silk_decoder * )decState; + silk_decoder_state *channel_state = psDec->channel_state; + opus_int has_side; + opus_int stereo_to_mono; + int delay_stack_alloc; + SAVE_STACK; + + celt_assert( decControl->nChannelsInternal == 1 || decControl->nChannelsInternal == 2 ); + + /**********************************/ + /* Test if first frame in payload */ + /**********************************/ + if( newPacketFlag ) { + for( n = 0; n < decControl->nChannelsInternal; n++ ) { + channel_state[ n ].nFramesDecoded = 0; /* Used to count frames in packet */ + } + } + + /* If Mono -> Stereo transition in bitstream: init state of second channel */ + if( decControl->nChannelsInternal > psDec->nChannelsInternal ) { + ret += silk_init_decoder( &channel_state[ 1 ] ); + } + + stereo_to_mono = decControl->nChannelsInternal == 1 && psDec->nChannelsInternal == 2 && + ( decControl->internalSampleRate == 1000*channel_state[ 0 ].fs_kHz ); + + if( channel_state[ 0 ].nFramesDecoded == 0 ) { + for( n = 0; n < decControl->nChannelsInternal; n++ ) { + opus_int fs_kHz_dec; + if( decControl->payloadSize_ms == 0 ) { + /* Assuming packet loss, use 10 ms */ + channel_state[ n ].nFramesPerPacket = 1; + channel_state[ n ].nb_subfr = 2; + } else if( decControl->payloadSize_ms == 10 ) { + channel_state[ n ].nFramesPerPacket = 1; + channel_state[ n ].nb_subfr = 2; + } else if( decControl->payloadSize_ms == 20 ) { + channel_state[ n ].nFramesPerPacket = 1; + channel_state[ n ].nb_subfr = 4; + } else if( decControl->payloadSize_ms == 40 ) { + channel_state[ n ].nFramesPerPacket = 2; + channel_state[ n ].nb_subfr = 4; + } else if( decControl->payloadSize_ms == 60 ) { + channel_state[ n ].nFramesPerPacket = 3; + channel_state[ n ].nb_subfr = 4; + } else { + celt_assert( 0 ); + RESTORE_STACK; + return SILK_DEC_INVALID_FRAME_SIZE; + } + fs_kHz_dec = ( decControl->internalSampleRate >> 10 ) + 1; + if( fs_kHz_dec != 8 && fs_kHz_dec != 12 && fs_kHz_dec != 16 ) { + celt_assert( 0 ); + RESTORE_STACK; + return SILK_DEC_INVALID_SAMPLING_FREQUENCY; + } + ret += silk_decoder_set_fs( &channel_state[ n ], fs_kHz_dec, decControl->API_sampleRate ); + } + } + + if( decControl->nChannelsAPI == 2 && decControl->nChannelsInternal == 2 && ( psDec->nChannelsAPI == 1 || psDec->nChannelsInternal == 1 ) ) { + silk_memset( psDec->sStereo.pred_prev_Q13, 0, sizeof( psDec->sStereo.pred_prev_Q13 ) ); + silk_memset( psDec->sStereo.sSide, 0, sizeof( psDec->sStereo.sSide ) ); + silk_memcpy( &channel_state[ 1 ].resampler_state, &channel_state[ 0 ].resampler_state, sizeof( silk_resampler_state_struct ) ); + } + psDec->nChannelsAPI = decControl->nChannelsAPI; + psDec->nChannelsInternal = decControl->nChannelsInternal; + + if( decControl->API_sampleRate > (opus_int32)MAX_API_FS_KHZ * 1000 || decControl->API_sampleRate < 8000 ) { + ret = SILK_DEC_INVALID_SAMPLING_FREQUENCY; + RESTORE_STACK; + return( ret ); + } + + if( lostFlag != FLAG_PACKET_LOST && channel_state[ 0 ].nFramesDecoded == 0 ) { + /* First decoder call for this payload */ + /* Decode VAD flags and LBRR flag */ + for( n = 0; n < decControl->nChannelsInternal; n++ ) { + for( i = 0; i < channel_state[ n ].nFramesPerPacket; i++ ) { + channel_state[ n ].VAD_flags[ i ] = ec_dec_bit_logp(psRangeDec, 1); + } + channel_state[ n ].LBRR_flag = ec_dec_bit_logp(psRangeDec, 1); + } + /* Decode LBRR flags */ + for( n = 0; n < decControl->nChannelsInternal; n++ ) { + silk_memset( channel_state[ n ].LBRR_flags, 0, sizeof( channel_state[ n ].LBRR_flags ) ); + if( channel_state[ n ].LBRR_flag ) { + if( channel_state[ n ].nFramesPerPacket == 1 ) { + channel_state[ n ].LBRR_flags[ 0 ] = 1; + } else { + LBRR_symbol = ec_dec_icdf( psRangeDec, silk_LBRR_flags_iCDF_ptr[ channel_state[ n ].nFramesPerPacket - 2 ], 8 ) + 1; + for( i = 0; i < channel_state[ n ].nFramesPerPacket; i++ ) { + channel_state[ n ].LBRR_flags[ i ] = silk_RSHIFT( LBRR_symbol, i ) & 1; + } + } + } + } + + if( lostFlag == FLAG_DECODE_NORMAL ) { + /* Regular decoding: skip all LBRR data */ + for( i = 0; i < channel_state[ 0 ].nFramesPerPacket; i++ ) { + for( n = 0; n < decControl->nChannelsInternal; n++ ) { + if( channel_state[ n ].LBRR_flags[ i ] ) { + opus_int16 pulses[ MAX_FRAME_LENGTH ]; + opus_int condCoding; + + if( decControl->nChannelsInternal == 2 && n == 0 ) { + silk_stereo_decode_pred( psRangeDec, MS_pred_Q13 ); + if( channel_state[ 1 ].LBRR_flags[ i ] == 0 ) { + silk_stereo_decode_mid_only( psRangeDec, &decode_only_middle ); + } + } + /* Use conditional coding if previous frame available */ + if( i > 0 && channel_state[ n ].LBRR_flags[ i - 1 ] ) { + condCoding = CODE_CONDITIONALLY; + } else { + condCoding = CODE_INDEPENDENTLY; + } + silk_decode_indices( &channel_state[ n ], psRangeDec, i, 1, condCoding ); + silk_decode_pulses( psRangeDec, pulses, channel_state[ n ].indices.signalType, + channel_state[ n ].indices.quantOffsetType, channel_state[ n ].frame_length ); + } + } + } + } + } + + /* Get MS predictor index */ + if( decControl->nChannelsInternal == 2 ) { + if( lostFlag == FLAG_DECODE_NORMAL || + ( lostFlag == FLAG_DECODE_LBRR && channel_state[ 0 ].LBRR_flags[ channel_state[ 0 ].nFramesDecoded ] == 1 ) ) + { + silk_stereo_decode_pred( psRangeDec, MS_pred_Q13 ); + /* For LBRR data, decode mid-only flag only if side-channel's LBRR flag is false */ + if( ( lostFlag == FLAG_DECODE_NORMAL && channel_state[ 1 ].VAD_flags[ channel_state[ 0 ].nFramesDecoded ] == 0 ) || + ( lostFlag == FLAG_DECODE_LBRR && channel_state[ 1 ].LBRR_flags[ channel_state[ 0 ].nFramesDecoded ] == 0 ) ) + { + silk_stereo_decode_mid_only( psRangeDec, &decode_only_middle ); + } else { + decode_only_middle = 0; + } + } else { + for( n = 0; n < 2; n++ ) { + MS_pred_Q13[ n ] = psDec->sStereo.pred_prev_Q13[ n ]; + } + } + } + + /* Reset side channel decoder prediction memory for first frame with side coding */ + if( decControl->nChannelsInternal == 2 && decode_only_middle == 0 && psDec->prev_decode_only_middle == 1 ) { + silk_memset( psDec->channel_state[ 1 ].outBuf, 0, sizeof(psDec->channel_state[ 1 ].outBuf) ); + silk_memset( psDec->channel_state[ 1 ].sLPC_Q14_buf, 0, sizeof(psDec->channel_state[ 1 ].sLPC_Q14_buf) ); + psDec->channel_state[ 1 ].lagPrev = 100; + psDec->channel_state[ 1 ].LastGainIndex = 10; + psDec->channel_state[ 1 ].prevSignalType = TYPE_NO_VOICE_ACTIVITY; + psDec->channel_state[ 1 ].first_frame_after_reset = 1; + } + + /* Check if the temp buffer fits into the output PCM buffer. If it fits, + we can delay allocating the temp buffer until after the SILK peak stack + usage. We need to use a < and not a <= because of the two extra samples. */ + delay_stack_alloc = decControl->internalSampleRate*decControl->nChannelsInternal + < decControl->API_sampleRate*decControl->nChannelsAPI; + ALLOC( samplesOut1_tmp_storage1, delay_stack_alloc ? ALLOC_NONE + : decControl->nChannelsInternal*(channel_state[ 0 ].frame_length + 2 ), + opus_int16 ); + if ( delay_stack_alloc ) + { + samplesOut1_tmp[ 0 ] = samplesOut; + samplesOut1_tmp[ 1 ] = samplesOut + channel_state[ 0 ].frame_length + 2; + } else { + samplesOut1_tmp[ 0 ] = samplesOut1_tmp_storage1; + samplesOut1_tmp[ 1 ] = samplesOut1_tmp_storage1 + channel_state[ 0 ].frame_length + 2; + } + + if( lostFlag == FLAG_DECODE_NORMAL ) { + has_side = !decode_only_middle; + } else { + has_side = !psDec->prev_decode_only_middle + || (decControl->nChannelsInternal == 2 && lostFlag == FLAG_DECODE_LBRR && channel_state[1].LBRR_flags[ channel_state[1].nFramesDecoded ] == 1 ); + } + channel_state[ 0 ].sPLC.enable_deep_plc = decControl->enable_deep_plc; + /* Call decoder for one frame */ + for( n = 0; n < decControl->nChannelsInternal; n++ ) { + if( n == 0 || has_side ) { + opus_int FrameIndex; + opus_int condCoding; + + FrameIndex = channel_state[ 0 ].nFramesDecoded - n; + /* Use independent coding if no previous frame available */ + if( FrameIndex <= 0 ) { + condCoding = CODE_INDEPENDENTLY; + } else if( lostFlag == FLAG_DECODE_LBRR ) { + condCoding = channel_state[ n ].LBRR_flags[ FrameIndex - 1 ] ? CODE_CONDITIONALLY : CODE_INDEPENDENTLY; + } else if( n > 0 && psDec->prev_decode_only_middle ) { + /* If we skipped a side frame in this packet, we don't + need LTP scaling; the LTP state is well-defined. */ + condCoding = CODE_INDEPENDENTLY_NO_LTP_SCALING; + } else { + condCoding = CODE_CONDITIONALLY; + } +#ifdef ENABLE_OSCE + if ( channel_state[n].osce.method != decControl->osce_method ) { + osce_reset( &channel_state[n].osce, decControl->osce_method ); + } +#endif + ret += silk_decode_frame( &channel_state[ n ], psRangeDec, &samplesOut1_tmp[ n ][ 2 ], &nSamplesOutDec, lostFlag, condCoding, +#ifdef ENABLE_DEEP_PLC + n == 0 ? lpcnet : NULL, +#endif +#ifdef ENABLE_OSCE + &psDec->osce_model, +#endif + arch); + } else { + silk_memset( &samplesOut1_tmp[ n ][ 2 ], 0, nSamplesOutDec * sizeof( opus_int16 ) ); + } + channel_state[ n ].nFramesDecoded++; + } + + if( decControl->nChannelsAPI == 2 && decControl->nChannelsInternal == 2 ) { + /* Convert Mid/Side to Left/Right */ + silk_stereo_MS_to_LR( &psDec->sStereo, samplesOut1_tmp[ 0 ], samplesOut1_tmp[ 1 ], MS_pred_Q13, channel_state[ 0 ].fs_kHz, nSamplesOutDec ); + } else { + /* Buffering */ + silk_memcpy( samplesOut1_tmp[ 0 ], psDec->sStereo.sMid, 2 * sizeof( opus_int16 ) ); + silk_memcpy( psDec->sStereo.sMid, &samplesOut1_tmp[ 0 ][ nSamplesOutDec ], 2 * sizeof( opus_int16 ) ); + } + + /* Number of output samples */ + *nSamplesOut = silk_DIV32( nSamplesOutDec * decControl->API_sampleRate, silk_SMULBB( channel_state[ 0 ].fs_kHz, 1000 ) ); + + /* Set up pointers to temp buffers */ + ALLOC( samplesOut2_tmp, + decControl->nChannelsAPI == 2 ? *nSamplesOut : ALLOC_NONE, opus_int16 ); + if( decControl->nChannelsAPI == 2 ) { + resample_out_ptr = samplesOut2_tmp; + } else { + resample_out_ptr = samplesOut; + } + + ALLOC( samplesOut1_tmp_storage2, delay_stack_alloc + ? decControl->nChannelsInternal*(channel_state[ 0 ].frame_length + 2 ) + : ALLOC_NONE, + opus_int16 ); + if ( delay_stack_alloc ) { + OPUS_COPY(samplesOut1_tmp_storage2, samplesOut, decControl->nChannelsInternal*(channel_state[ 0 ].frame_length + 2)); + samplesOut1_tmp[ 0 ] = samplesOut1_tmp_storage2; + samplesOut1_tmp[ 1 ] = samplesOut1_tmp_storage2 + channel_state[ 0 ].frame_length + 2; + } + for( n = 0; n < silk_min( decControl->nChannelsAPI, decControl->nChannelsInternal ); n++ ) { + + /* Resample decoded signal to API_sampleRate */ + ret += silk_resampler( &channel_state[ n ].resampler_state, resample_out_ptr, &samplesOut1_tmp[ n ][ 1 ], nSamplesOutDec ); + + /* Interleave if stereo output and stereo stream */ + if( decControl->nChannelsAPI == 2 ) { + for( i = 0; i < *nSamplesOut; i++ ) { + samplesOut[ n + 2 * i ] = resample_out_ptr[ i ]; + } + } + } + + /* Create two channel output from mono stream */ + if( decControl->nChannelsAPI == 2 && decControl->nChannelsInternal == 1 ) { + if ( stereo_to_mono ){ + /* Resample right channel for newly collapsed stereo just in case + we weren't doing collapsing when switching to mono */ + ret += silk_resampler( &channel_state[ 1 ].resampler_state, resample_out_ptr, &samplesOut1_tmp[ 0 ][ 1 ], nSamplesOutDec ); + + for( i = 0; i < *nSamplesOut; i++ ) { + samplesOut[ 1 + 2 * i ] = resample_out_ptr[ i ]; + } + } else { + for( i = 0; i < *nSamplesOut; i++ ) { + samplesOut[ 1 + 2 * i ] = samplesOut[ 0 + 2 * i ]; + } + } + } + + /* Export pitch lag, measured at 48 kHz sampling rate */ + if( channel_state[ 0 ].prevSignalType == TYPE_VOICED ) { + int mult_tab[ 3 ] = { 6, 4, 3 }; + decControl->prevPitchLag = channel_state[ 0 ].lagPrev * mult_tab[ ( channel_state[ 0 ].fs_kHz - 8 ) >> 2 ]; + } else { + decControl->prevPitchLag = 0; + } + + if( lostFlag == FLAG_PACKET_LOST ) { + /* On packet loss, remove the gain clamping to prevent having the energy "bounce back" + if we lose packets when the energy is going down */ + for ( i = 0; i < psDec->nChannelsInternal; i++ ) + psDec->channel_state[ i ].LastGainIndex = 10; + } else { + psDec->prev_decode_only_middle = decode_only_middle; + } + RESTORE_STACK; + return ret; +} + +#if 0 +/* Getting table of contents for a packet */ +opus_int silk_get_TOC( + const opus_uint8 *payload, /* I Payload data */ + const opus_int nBytesIn, /* I Number of input bytes */ + const opus_int nFramesPerPayload, /* I Number of SILK frames per payload */ + silk_TOC_struct *Silk_TOC /* O Type of content */ +) +{ + opus_int i, flags, ret = SILK_NO_ERROR; + + if( nBytesIn < 1 ) { + return -1; + } + if( nFramesPerPayload < 0 || nFramesPerPayload > 3 ) { + return -1; + } + + silk_memset( Silk_TOC, 0, sizeof( *Silk_TOC ) ); + + /* For stereo, extract the flags for the mid channel */ + flags = silk_RSHIFT( payload[ 0 ], 7 - nFramesPerPayload ) & ( silk_LSHIFT( 1, nFramesPerPayload + 1 ) - 1 ); + + Silk_TOC->inbandFECFlag = flags & 1; + for( i = nFramesPerPayload - 1; i >= 0 ; i-- ) { + flags = silk_RSHIFT( flags, 1 ); + Silk_TOC->VADFlags[ i ] = flags & 1; + Silk_TOC->VADFlag |= flags & 1; + } + + return ret; +} +#endif diff --git a/port/third_party/opus/silk/decode_core.c b/port/third_party/opus/silk/decode_core.c new file mode 100644 index 00000000..1c352a65 --- /dev/null +++ b/port/third_party/opus/silk/decode_core.c @@ -0,0 +1,237 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" + +/**********************************************************/ +/* Core decoder. Performs inverse NSQ operation LTP + LPC */ +/**********************************************************/ +void silk_decode_core( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl, /* I Decoder control */ + opus_int16 xq[], /* O Decoded speech */ + const opus_int16 pulses[ MAX_FRAME_LENGTH ], /* I Pulse signal */ + int arch /* I Run-time architecture */ +) +{ + opus_int i, k, lag = 0, start_idx, sLTP_buf_idx, NLSF_interpolation_flag, signalType; + opus_int16 *A_Q12, *B_Q14, *pxq, A_Q12_tmp[ MAX_LPC_ORDER ]; + VARDECL( opus_int16, sLTP ); + VARDECL( opus_int32, sLTP_Q15 ); + opus_int32 LTP_pred_Q13, LPC_pred_Q10, Gain_Q10, inv_gain_Q31, gain_adj_Q16, rand_seed, offset_Q10; + opus_int32 *pred_lag_ptr, *pexc_Q14, *pres_Q14; + VARDECL( opus_int32, res_Q14 ); + VARDECL( opus_int32, sLPC_Q14 ); + SAVE_STACK; + + silk_assert( psDec->prev_gain_Q16 != 0 ); + + ALLOC( sLTP, psDec->ltp_mem_length, opus_int16 ); + ALLOC( sLTP_Q15, psDec->ltp_mem_length + psDec->frame_length, opus_int32 ); + ALLOC( res_Q14, psDec->subfr_length, opus_int32 ); + ALLOC( sLPC_Q14, psDec->subfr_length + MAX_LPC_ORDER, opus_int32 ); + + offset_Q10 = silk_Quantization_Offsets_Q10[ psDec->indices.signalType >> 1 ][ psDec->indices.quantOffsetType ]; + + if( psDec->indices.NLSFInterpCoef_Q2 < 1 << 2 ) { + NLSF_interpolation_flag = 1; + } else { + NLSF_interpolation_flag = 0; + } + + /* Decode excitation */ + rand_seed = psDec->indices.Seed; + for( i = 0; i < psDec->frame_length; i++ ) { + rand_seed = silk_RAND( rand_seed ); + psDec->exc_Q14[ i ] = silk_LSHIFT( (opus_int32)pulses[ i ], 14 ); + if( psDec->exc_Q14[ i ] > 0 ) { + psDec->exc_Q14[ i ] -= QUANT_LEVEL_ADJUST_Q10 << 4; + } else + if( psDec->exc_Q14[ i ] < 0 ) { + psDec->exc_Q14[ i ] += QUANT_LEVEL_ADJUST_Q10 << 4; + } + psDec->exc_Q14[ i ] += offset_Q10 << 4; + if( rand_seed < 0 ) { + psDec->exc_Q14[ i ] = -psDec->exc_Q14[ i ]; + } + + rand_seed = silk_ADD32_ovflw( rand_seed, pulses[ i ] ); + } + + /* Copy LPC state */ + silk_memcpy( sLPC_Q14, psDec->sLPC_Q14_buf, MAX_LPC_ORDER * sizeof( opus_int32 ) ); + + pexc_Q14 = psDec->exc_Q14; + pxq = xq; + sLTP_buf_idx = psDec->ltp_mem_length; + /* Loop over subframes */ + for( k = 0; k < psDec->nb_subfr; k++ ) { + pres_Q14 = res_Q14; + A_Q12 = psDecCtrl->PredCoef_Q12[ k >> 1 ]; + + /* Preload LPC coeficients to array on stack. Gives small performance gain */ + silk_memcpy( A_Q12_tmp, A_Q12, psDec->LPC_order * sizeof( opus_int16 ) ); + B_Q14 = &psDecCtrl->LTPCoef_Q14[ k * LTP_ORDER ]; + signalType = psDec->indices.signalType; + + Gain_Q10 = silk_RSHIFT( psDecCtrl->Gains_Q16[ k ], 6 ); + inv_gain_Q31 = silk_INVERSE32_varQ( psDecCtrl->Gains_Q16[ k ], 47 ); + + /* Calculate gain adjustment factor */ + if( psDecCtrl->Gains_Q16[ k ] != psDec->prev_gain_Q16 ) { + gain_adj_Q16 = silk_DIV32_varQ( psDec->prev_gain_Q16, psDecCtrl->Gains_Q16[ k ], 16 ); + + /* Scale short term state */ + for( i = 0; i < MAX_LPC_ORDER; i++ ) { + sLPC_Q14[ i ] = silk_SMULWW( gain_adj_Q16, sLPC_Q14[ i ] ); + } + } else { + gain_adj_Q16 = (opus_int32)1 << 16; + } + + /* Save inv_gain */ + silk_assert( inv_gain_Q31 != 0 ); + psDec->prev_gain_Q16 = psDecCtrl->Gains_Q16[ k ]; + + /* Avoid abrupt transition from voiced PLC to unvoiced normal decoding */ + if( psDec->lossCnt && psDec->prevSignalType == TYPE_VOICED && + psDec->indices.signalType != TYPE_VOICED && k < MAX_NB_SUBFR/2 ) { + + silk_memset( B_Q14, 0, LTP_ORDER * sizeof( opus_int16 ) ); + B_Q14[ LTP_ORDER/2 ] = SILK_FIX_CONST( 0.25, 14 ); + + signalType = TYPE_VOICED; + psDecCtrl->pitchL[ k ] = psDec->lagPrev; + } + + if( signalType == TYPE_VOICED ) { + /* Voiced */ + lag = psDecCtrl->pitchL[ k ]; + + /* Re-whitening */ + if( k == 0 || ( k == 2 && NLSF_interpolation_flag ) ) { + /* Rewhiten with new A coefs */ + start_idx = psDec->ltp_mem_length - lag - psDec->LPC_order - LTP_ORDER / 2; + celt_assert( start_idx > 0 ); + + if( k == 2 ) { + silk_memcpy( &psDec->outBuf[ psDec->ltp_mem_length ], xq, 2 * psDec->subfr_length * sizeof( opus_int16 ) ); + } + + silk_LPC_analysis_filter( &sLTP[ start_idx ], &psDec->outBuf[ start_idx + k * psDec->subfr_length ], + A_Q12, psDec->ltp_mem_length - start_idx, psDec->LPC_order, arch ); + + /* After rewhitening the LTP state is unscaled */ + if( k == 0 ) { + /* Do LTP downscaling to reduce inter-packet dependency */ + inv_gain_Q31 = silk_LSHIFT( silk_SMULWB( inv_gain_Q31, psDecCtrl->LTP_scale_Q14 ), 2 ); + } + for( i = 0; i < lag + LTP_ORDER/2; i++ ) { + sLTP_Q15[ sLTP_buf_idx - i - 1 ] = silk_SMULWB( inv_gain_Q31, sLTP[ psDec->ltp_mem_length - i - 1 ] ); + } + } else { + /* Update LTP state when Gain changes */ + if( gain_adj_Q16 != (opus_int32)1 << 16 ) { + for( i = 0; i < lag + LTP_ORDER/2; i++ ) { + sLTP_Q15[ sLTP_buf_idx - i - 1 ] = silk_SMULWW( gain_adj_Q16, sLTP_Q15[ sLTP_buf_idx - i - 1 ] ); + } + } + } + } + + /* Long-term prediction */ + if( signalType == TYPE_VOICED ) { + /* Set up pointer */ + pred_lag_ptr = &sLTP_Q15[ sLTP_buf_idx - lag + LTP_ORDER / 2 ]; + for( i = 0; i < psDec->subfr_length; i++ ) { + /* Unrolled loop */ + /* Avoids introducing a bias because silk_SMLAWB() always rounds to -inf */ + LTP_pred_Q13 = 2; + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ 0 ], B_Q14[ 0 ] ); + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ -1 ], B_Q14[ 1 ] ); + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ -2 ], B_Q14[ 2 ] ); + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ -3 ], B_Q14[ 3 ] ); + LTP_pred_Q13 = silk_SMLAWB( LTP_pred_Q13, pred_lag_ptr[ -4 ], B_Q14[ 4 ] ); + pred_lag_ptr++; + + /* Generate LPC excitation */ + pres_Q14[ i ] = silk_ADD_LSHIFT32( pexc_Q14[ i ], LTP_pred_Q13, 1 ); + + /* Update states */ + sLTP_Q15[ sLTP_buf_idx ] = silk_LSHIFT( pres_Q14[ i ], 1 ); + sLTP_buf_idx++; + } + } else { + pres_Q14 = pexc_Q14; + } + + for( i = 0; i < psDec->subfr_length; i++ ) { + /* Short-term prediction */ + celt_assert( psDec->LPC_order == 10 || psDec->LPC_order == 16 ); + /* Avoids introducing a bias because silk_SMLAWB() always rounds to -inf */ + LPC_pred_Q10 = silk_RSHIFT( psDec->LPC_order, 1 ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 1 ], A_Q12_tmp[ 0 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 2 ], A_Q12_tmp[ 1 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 3 ], A_Q12_tmp[ 2 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 4 ], A_Q12_tmp[ 3 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 5 ], A_Q12_tmp[ 4 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 6 ], A_Q12_tmp[ 5 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 7 ], A_Q12_tmp[ 6 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 8 ], A_Q12_tmp[ 7 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 9 ], A_Q12_tmp[ 8 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 10 ], A_Q12_tmp[ 9 ] ); + if( psDec->LPC_order == 16 ) { + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 11 ], A_Q12_tmp[ 10 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 12 ], A_Q12_tmp[ 11 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 13 ], A_Q12_tmp[ 12 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 14 ], A_Q12_tmp[ 13 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 15 ], A_Q12_tmp[ 14 ] ); + LPC_pred_Q10 = silk_SMLAWB( LPC_pred_Q10, sLPC_Q14[ MAX_LPC_ORDER + i - 16 ], A_Q12_tmp[ 15 ] ); + } + + /* Add prediction to LPC excitation */ + sLPC_Q14[ MAX_LPC_ORDER + i ] = silk_ADD_SAT32( pres_Q14[ i ], silk_LSHIFT_SAT32( LPC_pred_Q10, 4 ) ); + + /* Scale with gain */ + pxq[ i ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( silk_SMULWW( sLPC_Q14[ MAX_LPC_ORDER + i ], Gain_Q10 ), 8 ) ); + } + + /* Update LPC filter state */ + silk_memcpy( sLPC_Q14, &sLPC_Q14[ psDec->subfr_length ], MAX_LPC_ORDER * sizeof( opus_int32 ) ); + pexc_Q14 += psDec->subfr_length; + pxq += psDec->subfr_length; + } + + /* Save LPC state */ + silk_memcpy( psDec->sLPC_Q14_buf, sLPC_Q14, MAX_LPC_ORDER * sizeof( opus_int32 ) ); + RESTORE_STACK; +} diff --git a/port/third_party/opus/silk/decode_frame.c b/port/third_party/opus/silk/decode_frame.c new file mode 100644 index 00000000..9bc4ca2b --- /dev/null +++ b/port/third_party/opus/silk/decode_frame.c @@ -0,0 +1,169 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" +#include "PLC.h" + +#ifdef ENABLE_OSCE +#include "osce.h" +#endif + +/****************/ +/* Decode frame */ +/****************/ +opus_int silk_decode_frame( + silk_decoder_state *psDec, /* I/O Pointer to Silk decoder state */ + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int16 pOut[], /* O Pointer to output speech frame */ + opus_int32 *pN, /* O Pointer to size of output frame */ + opus_int lostFlag, /* I 0: no loss, 1 loss, 2 decode fec */ + opus_int condCoding, /* I The type of conditional coding to use */ +#ifdef ENABLE_DEEP_PLC + LPCNetPLCState *lpcnet, +#endif +#ifdef ENABLE_OSCE + OSCEModel *osce_model, +#endif + int arch /* I Run-time architecture */ +) +{ + VARDECL( silk_decoder_control, psDecCtrl ); + opus_int L, mv_len, ret = 0; + SAVE_STACK; + + L = psDec->frame_length; + ALLOC( psDecCtrl, 1, silk_decoder_control ); + psDecCtrl->LTP_scale_Q14 = 0; + + /* Safety checks */ + celt_assert( L > 0 && L <= MAX_FRAME_LENGTH ); + + if( lostFlag == FLAG_DECODE_NORMAL || + ( lostFlag == FLAG_DECODE_LBRR && psDec->LBRR_flags[ psDec->nFramesDecoded ] == 1 ) ) + { + VARDECL( opus_int16, pulses ); +#ifdef ENABLE_OSCE + opus_int32 ec_start; + ec_start = ec_tell(psRangeDec); +#endif + ALLOC( pulses, (L + SHELL_CODEC_FRAME_LENGTH - 1) & + ~(SHELL_CODEC_FRAME_LENGTH - 1), opus_int16 ); + /*********************************************/ + /* Decode quantization indices of side info */ + /*********************************************/ + silk_decode_indices( psDec, psRangeDec, psDec->nFramesDecoded, lostFlag, condCoding ); + + /*********************************************/ + /* Decode quantization indices of excitation */ + /*********************************************/ + silk_decode_pulses( psRangeDec, pulses, psDec->indices.signalType, + psDec->indices.quantOffsetType, psDec->frame_length ); + + /********************************************/ + /* Decode parameters and pulse signal */ + /********************************************/ + silk_decode_parameters( psDec, psDecCtrl, condCoding ); + + /********************************************************/ + /* Run inverse NSQ */ + /********************************************************/ + silk_decode_core( psDec, psDecCtrl, pOut, pulses, arch ); + + /*************************/ + /* Update output buffer. */ + /*************************/ + celt_assert( psDec->ltp_mem_length >= psDec->frame_length ); + mv_len = psDec->ltp_mem_length - psDec->frame_length; + silk_memmove( psDec->outBuf, &psDec->outBuf[ psDec->frame_length ], mv_len * sizeof(opus_int16) ); + silk_memcpy( &psDec->outBuf[ mv_len ], pOut, psDec->frame_length * sizeof( opus_int16 ) ); + +#ifdef ENABLE_OSCE + /********************************************************/ + /* Run SILK enhancer */ + /********************************************************/ + osce_enhance_frame( osce_model, psDec, psDecCtrl, pOut, ec_tell(psRangeDec) - ec_start, arch ); +#endif + + /********************************************************/ + /* Update PLC state */ + /********************************************************/ + silk_PLC( psDec, psDecCtrl, pOut, 0, +#ifdef ENABLE_DEEP_PLC + lpcnet, +#endif + arch ); + + psDec->lossCnt = 0; + psDec->prevSignalType = psDec->indices.signalType; + celt_assert( psDec->prevSignalType >= 0 && psDec->prevSignalType <= 2 ); + + /* A frame has been decoded without errors */ + psDec->first_frame_after_reset = 0; + } else { + /* Handle packet loss by extrapolation */ + silk_PLC( psDec, psDecCtrl, pOut, 1, +#ifdef ENABLE_DEEP_PLC + lpcnet, +#endif + arch ); + +#ifdef ENABLE_OSCE + osce_reset( &psDec->osce, psDec->osce.method ); +#endif + /*************************/ + /* Update output buffer. */ + /*************************/ + celt_assert( psDec->ltp_mem_length >= psDec->frame_length ); + mv_len = psDec->ltp_mem_length - psDec->frame_length; + silk_memmove( psDec->outBuf, &psDec->outBuf[ psDec->frame_length ], mv_len * sizeof(opus_int16) ); + silk_memcpy( &psDec->outBuf[ mv_len ], pOut, psDec->frame_length * sizeof( opus_int16 ) ); + } + + /************************************************/ + /* Comfort noise generation / estimation */ + /************************************************/ + silk_CNG( psDec, psDecCtrl, pOut, L ); + + /****************************************************************/ + /* Ensure smooth connection of extrapolated and good frames */ + /****************************************************************/ + silk_PLC_glue_frames( psDec, pOut, L ); + + /* Update some decoder state variables */ + psDec->lagPrev = psDecCtrl->pitchL[ psDec->nb_subfr - 1 ]; + + /* Set output frame length */ + *pN = L; + + RESTORE_STACK; + return ret; +} diff --git a/port/third_party/opus/silk/decode_indices.c b/port/third_party/opus/silk/decode_indices.c new file mode 100644 index 00000000..0bb4a997 --- /dev/null +++ b/port/third_party/opus/silk/decode_indices.c @@ -0,0 +1,151 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Decode side-information parameters from payload */ +void silk_decode_indices( + silk_decoder_state *psDec, /* I/O State */ + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int FrameIndex, /* I Frame number */ + opus_int decode_LBRR, /* I Flag indicating LBRR data is being decoded */ + opus_int condCoding /* I The type of conditional coding to use */ +) +{ + opus_int i, k, Ix; + opus_int decode_absolute_lagIndex, delta_lagIndex; + opus_int16 ec_ix[ MAX_LPC_ORDER ]; + opus_uint8 pred_Q8[ MAX_LPC_ORDER ]; + + /*******************************************/ + /* Decode signal type and quantizer offset */ + /*******************************************/ + if( decode_LBRR || psDec->VAD_flags[ FrameIndex ] ) { + Ix = ec_dec_icdf( psRangeDec, silk_type_offset_VAD_iCDF, 8 ) + 2; + } else { + Ix = ec_dec_icdf( psRangeDec, silk_type_offset_no_VAD_iCDF, 8 ); + } + psDec->indices.signalType = (opus_int8)silk_RSHIFT( Ix, 1 ); + psDec->indices.quantOffsetType = (opus_int8)( Ix & 1 ); + + /****************/ + /* Decode gains */ + /****************/ + /* First subframe */ + if( condCoding == CODE_CONDITIONALLY ) { + /* Conditional coding */ + psDec->indices.GainsIndices[ 0 ] = (opus_int8)ec_dec_icdf( psRangeDec, silk_delta_gain_iCDF, 8 ); + } else { + /* Independent coding, in two stages: MSB bits followed by 3 LSBs */ + psDec->indices.GainsIndices[ 0 ] = (opus_int8)silk_LSHIFT( ec_dec_icdf( psRangeDec, silk_gain_iCDF[ psDec->indices.signalType ], 8 ), 3 ); + psDec->indices.GainsIndices[ 0 ] += (opus_int8)ec_dec_icdf( psRangeDec, silk_uniform8_iCDF, 8 ); + } + + /* Remaining subframes */ + for( i = 1; i < psDec->nb_subfr; i++ ) { + psDec->indices.GainsIndices[ i ] = (opus_int8)ec_dec_icdf( psRangeDec, silk_delta_gain_iCDF, 8 ); + } + + /**********************/ + /* Decode LSF Indices */ + /**********************/ + psDec->indices.NLSFIndices[ 0 ] = (opus_int8)ec_dec_icdf( psRangeDec, &psDec->psNLSF_CB->CB1_iCDF[ ( psDec->indices.signalType >> 1 ) * psDec->psNLSF_CB->nVectors ], 8 ); + silk_NLSF_unpack( ec_ix, pred_Q8, psDec->psNLSF_CB, psDec->indices.NLSFIndices[ 0 ] ); + celt_assert( psDec->psNLSF_CB->order == psDec->LPC_order ); + for( i = 0; i < psDec->psNLSF_CB->order; i++ ) { + Ix = ec_dec_icdf( psRangeDec, &psDec->psNLSF_CB->ec_iCDF[ ec_ix[ i ] ], 8 ); + if( Ix == 0 ) { + Ix -= ec_dec_icdf( psRangeDec, silk_NLSF_EXT_iCDF, 8 ); + } else if( Ix == 2 * NLSF_QUANT_MAX_AMPLITUDE ) { + Ix += ec_dec_icdf( psRangeDec, silk_NLSF_EXT_iCDF, 8 ); + } + psDec->indices.NLSFIndices[ i+1 ] = (opus_int8)( Ix - NLSF_QUANT_MAX_AMPLITUDE ); + } + + /* Decode LSF interpolation factor */ + if( psDec->nb_subfr == MAX_NB_SUBFR ) { + psDec->indices.NLSFInterpCoef_Q2 = (opus_int8)ec_dec_icdf( psRangeDec, silk_NLSF_interpolation_factor_iCDF, 8 ); + } else { + psDec->indices.NLSFInterpCoef_Q2 = 4; + } + + if( psDec->indices.signalType == TYPE_VOICED ) + { + /*********************/ + /* Decode pitch lags */ + /*********************/ + /* Get lag index */ + decode_absolute_lagIndex = 1; + if( condCoding == CODE_CONDITIONALLY && psDec->ec_prevSignalType == TYPE_VOICED ) { + /* Decode Delta index */ + delta_lagIndex = (opus_int16)ec_dec_icdf( psRangeDec, silk_pitch_delta_iCDF, 8 ); + if( delta_lagIndex > 0 ) { + delta_lagIndex = delta_lagIndex - 9; + psDec->indices.lagIndex = (opus_int16)( psDec->ec_prevLagIndex + delta_lagIndex ); + decode_absolute_lagIndex = 0; + } + } + if( decode_absolute_lagIndex ) { + /* Absolute decoding */ + psDec->indices.lagIndex = (opus_int16)ec_dec_icdf( psRangeDec, silk_pitch_lag_iCDF, 8 ) * silk_RSHIFT( psDec->fs_kHz, 1 ); + psDec->indices.lagIndex += (opus_int16)ec_dec_icdf( psRangeDec, psDec->pitch_lag_low_bits_iCDF, 8 ); + } + psDec->ec_prevLagIndex = psDec->indices.lagIndex; + + /* Get countour index */ + psDec->indices.contourIndex = (opus_int8)ec_dec_icdf( psRangeDec, psDec->pitch_contour_iCDF, 8 ); + + /********************/ + /* Decode LTP gains */ + /********************/ + /* Decode PERIndex value */ + psDec->indices.PERIndex = (opus_int8)ec_dec_icdf( psRangeDec, silk_LTP_per_index_iCDF, 8 ); + + for( k = 0; k < psDec->nb_subfr; k++ ) { + psDec->indices.LTPIndex[ k ] = (opus_int8)ec_dec_icdf( psRangeDec, silk_LTP_gain_iCDF_ptrs[ psDec->indices.PERIndex ], 8 ); + } + + /**********************/ + /* Decode LTP scaling */ + /**********************/ + if( condCoding == CODE_INDEPENDENTLY ) { + psDec->indices.LTP_scaleIndex = (opus_int8)ec_dec_icdf( psRangeDec, silk_LTPscale_iCDF, 8 ); + } else { + psDec->indices.LTP_scaleIndex = 0; + } + } + psDec->ec_prevSignalType = psDec->indices.signalType; + + /***************/ + /* Decode seed */ + /***************/ + psDec->indices.Seed = (opus_int8)ec_dec_icdf( psRangeDec, silk_uniform4_iCDF, 8 ); +} diff --git a/port/third_party/opus/silk/decode_parameters.c b/port/third_party/opus/silk/decode_parameters.c new file mode 100644 index 00000000..a56a4098 --- /dev/null +++ b/port/third_party/opus/silk/decode_parameters.c @@ -0,0 +1,115 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Decode parameters from payload */ +void silk_decode_parameters( + silk_decoder_state *psDec, /* I/O State */ + silk_decoder_control *psDecCtrl, /* I/O Decoder control */ + opus_int condCoding /* I The type of conditional coding to use */ +) +{ + opus_int i, k, Ix; + opus_int16 pNLSF_Q15[ MAX_LPC_ORDER ], pNLSF0_Q15[ MAX_LPC_ORDER ]; + const opus_int8 *cbk_ptr_Q7; + + /* Dequant Gains */ + silk_gains_dequant( psDecCtrl->Gains_Q16, psDec->indices.GainsIndices, + &psDec->LastGainIndex, condCoding == CODE_CONDITIONALLY, psDec->nb_subfr ); + + /****************/ + /* Decode NLSFs */ + /****************/ + silk_NLSF_decode( pNLSF_Q15, psDec->indices.NLSFIndices, psDec->psNLSF_CB ); + + /* Convert NLSF parameters to AR prediction filter coefficients */ + silk_NLSF2A( psDecCtrl->PredCoef_Q12[ 1 ], pNLSF_Q15, psDec->LPC_order, psDec->arch ); + + /* If just reset, e.g., because internal Fs changed, do not allow interpolation */ + /* improves the case of packet loss in the first frame after a switch */ + if( psDec->first_frame_after_reset == 1 ) { + psDec->indices.NLSFInterpCoef_Q2 = 4; + } + + if( psDec->indices.NLSFInterpCoef_Q2 < 4 ) { + /* Calculation of the interpolated NLSF0 vector from the interpolation factor, */ + /* the previous NLSF1, and the current NLSF1 */ + for( i = 0; i < psDec->LPC_order; i++ ) { + pNLSF0_Q15[ i ] = psDec->prevNLSF_Q15[ i ] + silk_RSHIFT( silk_MUL( psDec->indices.NLSFInterpCoef_Q2, + pNLSF_Q15[ i ] - psDec->prevNLSF_Q15[ i ] ), 2 ); + } + + /* Convert NLSF parameters to AR prediction filter coefficients */ + silk_NLSF2A( psDecCtrl->PredCoef_Q12[ 0 ], pNLSF0_Q15, psDec->LPC_order, psDec->arch ); + } else { + /* Copy LPC coefficients for first half from second half */ + silk_memcpy( psDecCtrl->PredCoef_Q12[ 0 ], psDecCtrl->PredCoef_Q12[ 1 ], psDec->LPC_order * sizeof( opus_int16 ) ); + } + + silk_memcpy( psDec->prevNLSF_Q15, pNLSF_Q15, psDec->LPC_order * sizeof( opus_int16 ) ); + + /* After a packet loss do BWE of LPC coefs */ + if( psDec->lossCnt ) { + silk_bwexpander( psDecCtrl->PredCoef_Q12[ 0 ], psDec->LPC_order, BWE_AFTER_LOSS_Q16 ); + silk_bwexpander( psDecCtrl->PredCoef_Q12[ 1 ], psDec->LPC_order, BWE_AFTER_LOSS_Q16 ); + } + + if( psDec->indices.signalType == TYPE_VOICED ) { + /*********************/ + /* Decode pitch lags */ + /*********************/ + + /* Decode pitch values */ + silk_decode_pitch( psDec->indices.lagIndex, psDec->indices.contourIndex, psDecCtrl->pitchL, psDec->fs_kHz, psDec->nb_subfr ); + + /* Decode Codebook Index */ + cbk_ptr_Q7 = silk_LTP_vq_ptrs_Q7[ psDec->indices.PERIndex ]; /* set pointer to start of codebook */ + + for( k = 0; k < psDec->nb_subfr; k++ ) { + Ix = psDec->indices.LTPIndex[ k ]; + for( i = 0; i < LTP_ORDER; i++ ) { + psDecCtrl->LTPCoef_Q14[ k * LTP_ORDER + i ] = silk_LSHIFT( cbk_ptr_Q7[ Ix * LTP_ORDER + i ], 7 ); + } + } + + /**********************/ + /* Decode LTP scaling */ + /**********************/ + Ix = psDec->indices.LTP_scaleIndex; + psDecCtrl->LTP_scale_Q14 = silk_LTPScales_table_Q14[ Ix ]; + } else { + silk_memset( psDecCtrl->pitchL, 0, psDec->nb_subfr * sizeof( opus_int ) ); + silk_memset( psDecCtrl->LTPCoef_Q14, 0, LTP_ORDER * psDec->nb_subfr * sizeof( opus_int16 ) ); + psDec->indices.PERIndex = 0; + psDecCtrl->LTP_scale_Q14 = 0; + } +} diff --git a/port/third_party/opus/silk/decode_pitch.c b/port/third_party/opus/silk/decode_pitch.c new file mode 100644 index 00000000..fd1b6bf5 --- /dev/null +++ b/port/third_party/opus/silk/decode_pitch.c @@ -0,0 +1,77 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/*********************************************************** +* Pitch analyser function +********************************************************** */ +#include "SigProc_FIX.h" +#include "pitch_est_defines.h" + +void silk_decode_pitch( + opus_int16 lagIndex, /* I */ + opus_int8 contourIndex, /* O */ + opus_int pitch_lags[], /* O 4 pitch values */ + const opus_int Fs_kHz, /* I sampling frequency (kHz) */ + const opus_int nb_subfr /* I number of sub frames */ +) +{ + opus_int lag, k, min_lag, max_lag, cbk_size; + const opus_int8 *Lag_CB_ptr; + + if( Fs_kHz == 8 ) { + if( nb_subfr == PE_MAX_NB_SUBFR ) { + Lag_CB_ptr = &silk_CB_lags_stage2[ 0 ][ 0 ]; + cbk_size = PE_NB_CBKS_STAGE2_EXT; + } else { + celt_assert( nb_subfr == PE_MAX_NB_SUBFR >> 1 ); + Lag_CB_ptr = &silk_CB_lags_stage2_10_ms[ 0 ][ 0 ]; + cbk_size = PE_NB_CBKS_STAGE2_10MS; + } + } else { + if( nb_subfr == PE_MAX_NB_SUBFR ) { + Lag_CB_ptr = &silk_CB_lags_stage3[ 0 ][ 0 ]; + cbk_size = PE_NB_CBKS_STAGE3_MAX; + } else { + celt_assert( nb_subfr == PE_MAX_NB_SUBFR >> 1 ); + Lag_CB_ptr = &silk_CB_lags_stage3_10_ms[ 0 ][ 0 ]; + cbk_size = PE_NB_CBKS_STAGE3_10MS; + } + } + + min_lag = silk_SMULBB( PE_MIN_LAG_MS, Fs_kHz ); + max_lag = silk_SMULBB( PE_MAX_LAG_MS, Fs_kHz ); + lag = min_lag + lagIndex; + + for( k = 0; k < nb_subfr; k++ ) { + pitch_lags[ k ] = lag + matrix_ptr( Lag_CB_ptr, k, contourIndex, cbk_size ); + pitch_lags[ k ] = silk_LIMIT( pitch_lags[ k ], min_lag, max_lag ); + } +} diff --git a/port/third_party/opus/silk/decode_pulses.c b/port/third_party/opus/silk/decode_pulses.c new file mode 100644 index 00000000..a56d2d30 --- /dev/null +++ b/port/third_party/opus/silk/decode_pulses.c @@ -0,0 +1,115 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/*********************************************/ +/* Decode quantization indices of excitation */ +/*********************************************/ +void silk_decode_pulses( + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int16 pulses[], /* O Excitation signal */ + const opus_int signalType, /* I Sigtype */ + const opus_int quantOffsetType, /* I quantOffsetType */ + const opus_int frame_length /* I Frame length */ +) +{ + opus_int i, j, k, iter, abs_q, nLS, RateLevelIndex; + opus_int sum_pulses[ MAX_NB_SHELL_BLOCKS ], nLshifts[ MAX_NB_SHELL_BLOCKS ]; + opus_int16 *pulses_ptr; + const opus_uint8 *cdf_ptr; + + /*********************/ + /* Decode rate level */ + /*********************/ + RateLevelIndex = ec_dec_icdf( psRangeDec, silk_rate_levels_iCDF[ signalType >> 1 ], 8 ); + + /* Calculate number of shell blocks */ + silk_assert( 1 << LOG2_SHELL_CODEC_FRAME_LENGTH == SHELL_CODEC_FRAME_LENGTH ); + iter = silk_RSHIFT( frame_length, LOG2_SHELL_CODEC_FRAME_LENGTH ); + if( iter * SHELL_CODEC_FRAME_LENGTH < frame_length ) { + celt_assert( frame_length == 12 * 10 ); /* Make sure only happens for 10 ms @ 12 kHz */ + iter++; + } + + /***************************************************/ + /* Sum-Weighted-Pulses Decoding */ + /***************************************************/ + cdf_ptr = silk_pulses_per_block_iCDF[ RateLevelIndex ]; + for( i = 0; i < iter; i++ ) { + nLshifts[ i ] = 0; + sum_pulses[ i ] = ec_dec_icdf( psRangeDec, cdf_ptr, 8 ); + + /* LSB indication */ + while( sum_pulses[ i ] == SILK_MAX_PULSES + 1 ) { + nLshifts[ i ]++; + /* When we've already got 10 LSBs, we shift the table to not allow (SILK_MAX_PULSES + 1) */ + sum_pulses[ i ] = ec_dec_icdf( psRangeDec, + silk_pulses_per_block_iCDF[ N_RATE_LEVELS - 1] + ( nLshifts[ i ] == 10 ), 8 ); + } + } + + /***************************************************/ + /* Shell decoding */ + /***************************************************/ + for( i = 0; i < iter; i++ ) { + if( sum_pulses[ i ] > 0 ) { + silk_shell_decoder( &pulses[ silk_SMULBB( i, SHELL_CODEC_FRAME_LENGTH ) ], psRangeDec, sum_pulses[ i ] ); + } else { + silk_memset( &pulses[ silk_SMULBB( i, SHELL_CODEC_FRAME_LENGTH ) ], 0, SHELL_CODEC_FRAME_LENGTH * sizeof( pulses[0] ) ); + } + } + + /***************************************************/ + /* LSB Decoding */ + /***************************************************/ + for( i = 0; i < iter; i++ ) { + if( nLshifts[ i ] > 0 ) { + nLS = nLshifts[ i ]; + pulses_ptr = &pulses[ silk_SMULBB( i, SHELL_CODEC_FRAME_LENGTH ) ]; + for( k = 0; k < SHELL_CODEC_FRAME_LENGTH; k++ ) { + abs_q = pulses_ptr[ k ]; + for( j = 0; j < nLS; j++ ) { + abs_q = silk_LSHIFT( abs_q, 1 ); + abs_q += ec_dec_icdf( psRangeDec, silk_lsb_iCDF, 8 ); + } + pulses_ptr[ k ] = abs_q; + } + /* Mark the number of pulses non-zero for sign decoding. */ + sum_pulses[ i ] |= nLS << 5; + } + } + + /****************************************/ + /* Decode and add signs to pulse signal */ + /****************************************/ + silk_decode_signs( psRangeDec, pulses, frame_length, signalType, quantOffsetType, sum_pulses ); +} diff --git a/port/third_party/opus/silk/decoder_set_fs.c b/port/third_party/opus/silk/decoder_set_fs.c new file mode 100644 index 00000000..d9a13d0f --- /dev/null +++ b/port/third_party/opus/silk/decoder_set_fs.c @@ -0,0 +1,108 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Set decoder sampling rate */ +opus_int silk_decoder_set_fs( + silk_decoder_state *psDec, /* I/O Decoder state pointer */ + opus_int fs_kHz, /* I Sampling frequency (kHz) */ + opus_int32 fs_API_Hz /* I API Sampling frequency (Hz) */ +) +{ + opus_int frame_length, ret = 0; + + celt_assert( fs_kHz == 8 || fs_kHz == 12 || fs_kHz == 16 ); + celt_assert( psDec->nb_subfr == MAX_NB_SUBFR || psDec->nb_subfr == MAX_NB_SUBFR/2 ); + + /* New (sub)frame length */ + psDec->subfr_length = silk_SMULBB( SUB_FRAME_LENGTH_MS, fs_kHz ); + frame_length = silk_SMULBB( psDec->nb_subfr, psDec->subfr_length ); + + /* Initialize resampler when switching internal or external sampling frequency */ + if( psDec->fs_kHz != fs_kHz || psDec->fs_API_hz != fs_API_Hz ) { + /* Initialize the resampler for dec_API.c preparing resampling from fs_kHz to API_fs_Hz */ + ret += silk_resampler_init( &psDec->resampler_state, silk_SMULBB( fs_kHz, 1000 ), fs_API_Hz, 0 ); + + psDec->fs_API_hz = fs_API_Hz; + } + + if( psDec->fs_kHz != fs_kHz || frame_length != psDec->frame_length ) { + if( fs_kHz == 8 ) { + if( psDec->nb_subfr == MAX_NB_SUBFR ) { + psDec->pitch_contour_iCDF = silk_pitch_contour_NB_iCDF; + } else { + psDec->pitch_contour_iCDF = silk_pitch_contour_10_ms_NB_iCDF; + } + } else { + if( psDec->nb_subfr == MAX_NB_SUBFR ) { + psDec->pitch_contour_iCDF = silk_pitch_contour_iCDF; + } else { + psDec->pitch_contour_iCDF = silk_pitch_contour_10_ms_iCDF; + } + } + if( psDec->fs_kHz != fs_kHz ) { + psDec->ltp_mem_length = silk_SMULBB( LTP_MEM_LENGTH_MS, fs_kHz ); + if( fs_kHz == 8 || fs_kHz == 12 ) { + psDec->LPC_order = MIN_LPC_ORDER; + psDec->psNLSF_CB = &silk_NLSF_CB_NB_MB; + } else { + psDec->LPC_order = MAX_LPC_ORDER; + psDec->psNLSF_CB = &silk_NLSF_CB_WB; + } + if( fs_kHz == 16 ) { + psDec->pitch_lag_low_bits_iCDF = silk_uniform8_iCDF; + } else if( fs_kHz == 12 ) { + psDec->pitch_lag_low_bits_iCDF = silk_uniform6_iCDF; + } else if( fs_kHz == 8 ) { + psDec->pitch_lag_low_bits_iCDF = silk_uniform4_iCDF; + } else { + /* unsupported sampling rate */ + celt_assert( 0 ); + } + psDec->first_frame_after_reset = 1; + psDec->lagPrev = 100; + psDec->LastGainIndex = 10; + psDec->prevSignalType = TYPE_NO_VOICE_ACTIVITY; + silk_memset( psDec->outBuf, 0, sizeof(psDec->outBuf)); + silk_memset( psDec->sLPC_Q14_buf, 0, sizeof(psDec->sLPC_Q14_buf) ); + } + + psDec->fs_kHz = fs_kHz; + psDec->frame_length = frame_length; + } + + /* Check that settings are valid */ + celt_assert( psDec->frame_length > 0 && psDec->frame_length <= MAX_FRAME_LENGTH ); + + return ret; +} + diff --git a/port/third_party/opus/silk/define.h b/port/third_party/opus/silk/define.h new file mode 100644 index 00000000..491c86f3 --- /dev/null +++ b/port/third_party/opus/silk/define.h @@ -0,0 +1,235 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_DEFINE_H +#define SILK_DEFINE_H + +#include "errors.h" +#include "typedef.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Max number of encoder channels (1/2) */ +#define ENCODER_NUM_CHANNELS 2 +/* Number of decoder channels (1/2) */ +#define DECODER_NUM_CHANNELS 2 + +#define MAX_FRAMES_PER_PACKET 3 + +/* Limits on bitrate */ +#define MIN_TARGET_RATE_BPS 5000 +#define MAX_TARGET_RATE_BPS 80000 + +/* LBRR thresholds */ +#define LBRR_NB_MIN_RATE_BPS 12000 +#define LBRR_MB_MIN_RATE_BPS 14000 +#define LBRR_WB_MIN_RATE_BPS 16000 + +/* DTX settings */ +#define NB_SPEECH_FRAMES_BEFORE_DTX 10 /* eq 200 ms */ +#define MAX_CONSECUTIVE_DTX 20 /* eq 400 ms */ +#define DTX_ACTIVITY_THRESHOLD 0.1f + +/* VAD decision */ +#define VAD_NO_DECISION -1 +#define VAD_NO_ACTIVITY 0 +#define VAD_ACTIVITY 1 + +/* Maximum sampling frequency */ +#define MAX_FS_KHZ 16 +#define MAX_API_FS_KHZ 48 + +/* Signal types */ +#define TYPE_NO_VOICE_ACTIVITY 0 +#define TYPE_UNVOICED 1 +#define TYPE_VOICED 2 + +/* Conditional coding types */ +#define CODE_INDEPENDENTLY 0 +#define CODE_INDEPENDENTLY_NO_LTP_SCALING 1 +#define CODE_CONDITIONALLY 2 + +/* Settings for stereo processing */ +#define STEREO_QUANT_TAB_SIZE 16 +#define STEREO_QUANT_SUB_STEPS 5 +#define STEREO_INTERP_LEN_MS 8 /* must be even */ +#define STEREO_RATIO_SMOOTH_COEF 0.01 /* smoothing coef for signal norms and stereo width */ + +/* Range of pitch lag estimates */ +#define PITCH_EST_MIN_LAG_MS 2 /* 2 ms -> 500 Hz */ +#define PITCH_EST_MAX_LAG_MS 18 /* 18 ms -> 56 Hz */ + +/* Maximum number of subframes */ +#define MAX_NB_SUBFR 4 + +/* Number of samples per frame */ +#define LTP_MEM_LENGTH_MS 20 +#define SUB_FRAME_LENGTH_MS 5 +#define MAX_SUB_FRAME_LENGTH ( SUB_FRAME_LENGTH_MS * MAX_FS_KHZ ) +#define MAX_FRAME_LENGTH_MS ( SUB_FRAME_LENGTH_MS * MAX_NB_SUBFR ) +#define MAX_FRAME_LENGTH ( MAX_FRAME_LENGTH_MS * MAX_FS_KHZ ) + +/* Milliseconds of lookahead for pitch analysis */ +#define LA_PITCH_MS 2 +#define LA_PITCH_MAX ( LA_PITCH_MS * MAX_FS_KHZ ) + +/* Order of LPC used in find pitch */ +#define MAX_FIND_PITCH_LPC_ORDER 16 + +/* Length of LPC window used in find pitch */ +#define FIND_PITCH_LPC_WIN_MS ( 20 + (LA_PITCH_MS << 1) ) +#define FIND_PITCH_LPC_WIN_MS_2_SF ( 10 + (LA_PITCH_MS << 1) ) +#define FIND_PITCH_LPC_WIN_MAX ( FIND_PITCH_LPC_WIN_MS * MAX_FS_KHZ ) + +/* Milliseconds of lookahead for noise shape analysis */ +#define LA_SHAPE_MS 5 +#define LA_SHAPE_MAX ( LA_SHAPE_MS * MAX_FS_KHZ ) + +/* Maximum length of LPC window used in noise shape analysis */ +#define SHAPE_LPC_WIN_MAX ( 15 * MAX_FS_KHZ ) + +/* dB level of lowest gain quantization level */ +#define MIN_QGAIN_DB 2 +/* dB level of highest gain quantization level */ +#define MAX_QGAIN_DB 88 +/* Number of gain quantization levels */ +#define N_LEVELS_QGAIN 64 +/* Max increase in gain quantization index */ +#define MAX_DELTA_GAIN_QUANT 36 +/* Max decrease in gain quantization index */ +#define MIN_DELTA_GAIN_QUANT -4 + +/* Quantization offsets (multiples of 4) */ +#define OFFSET_VL_Q10 32 +#define OFFSET_VH_Q10 100 +#define OFFSET_UVL_Q10 100 +#define OFFSET_UVH_Q10 240 + +#define QUANT_LEVEL_ADJUST_Q10 80 + +/* Maximum numbers of iterations used to stabilize an LPC vector */ +#define MAX_LPC_STABILIZE_ITERATIONS 16 +#define MAX_PREDICTION_POWER_GAIN 1e4f +#define MAX_PREDICTION_POWER_GAIN_AFTER_RESET 1e2f + +#define MAX_LPC_ORDER 16 +#define MIN_LPC_ORDER 10 + +/* Find Pred Coef defines */ +#define LTP_ORDER 5 + +/* LTP quantization settings */ +#define NB_LTP_CBKS 3 + +/* Flag to use harmonic noise shaping */ +#define USE_HARM_SHAPING 1 + +/* Max LPC order of noise shaping filters */ +#define MAX_SHAPE_LPC_ORDER 24 + +#define HARM_SHAPE_FIR_TAPS 3 + +/* Maximum number of delayed decision states */ +#define MAX_DEL_DEC_STATES 4 + +#define LTP_BUF_LENGTH 512 +#define LTP_MASK ( LTP_BUF_LENGTH - 1 ) + +#define DECISION_DELAY 40 + +/* Number of subframes for excitation entropy coding */ +#define SHELL_CODEC_FRAME_LENGTH 16 +#define LOG2_SHELL_CODEC_FRAME_LENGTH 4 +#define MAX_NB_SHELL_BLOCKS ( MAX_FRAME_LENGTH / SHELL_CODEC_FRAME_LENGTH ) + +/* Number of rate levels, for entropy coding of excitation */ +#define N_RATE_LEVELS 10 + +/* Maximum sum of pulses per shell coding frame */ +#define SILK_MAX_PULSES 16 + +#define MAX_MATRIX_SIZE MAX_LPC_ORDER /* Max of LPC Order and LTP order */ + +# define NSQ_LPC_BUF_LENGTH MAX_LPC_ORDER + +/***************************/ +/* Voice activity detector */ +/***************************/ +#define VAD_N_BANDS 4 + +#define VAD_INTERNAL_SUBFRAMES_LOG2 2 +#define VAD_INTERNAL_SUBFRAMES ( 1 << VAD_INTERNAL_SUBFRAMES_LOG2 ) + +#define VAD_NOISE_LEVEL_SMOOTH_COEF_Q16 1024 /* Must be < 4096 */ +#define VAD_NOISE_LEVELS_BIAS 50 + +/* Sigmoid settings */ +#define VAD_NEGATIVE_OFFSET_Q5 128 /* sigmoid is 0 at -128 */ +#define VAD_SNR_FACTOR_Q16 45000 + +/* smoothing for SNR measurement */ +#define VAD_SNR_SMOOTH_COEF_Q18 4096 + +/* Size of the piecewise linear cosine approximation table for the LSFs */ +#define LSF_COS_TAB_SZ_FIX 128 + +/******************/ +/* NLSF quantizer */ +/******************/ +#define NLSF_W_Q 2 +#define NLSF_VQ_MAX_VECTORS 32 +#define NLSF_QUANT_MAX_AMPLITUDE 4 +#define NLSF_QUANT_MAX_AMPLITUDE_EXT 10 +#define NLSF_QUANT_LEVEL_ADJ 0.1 +#define NLSF_QUANT_DEL_DEC_STATES_LOG2 2 +#define NLSF_QUANT_DEL_DEC_STATES ( 1 << NLSF_QUANT_DEL_DEC_STATES_LOG2 ) + +/* Transition filtering for mode switching */ +#define TRANSITION_TIME_MS 5120 /* 5120 = 64 * FRAME_LENGTH_MS * ( TRANSITION_INT_NUM - 1 ) = 64*(20*4)*/ +#define TRANSITION_NB 3 /* Hardcoded in tables */ +#define TRANSITION_NA 2 /* Hardcoded in tables */ +#define TRANSITION_INT_NUM 5 /* Hardcoded in tables */ +#define TRANSITION_FRAMES ( TRANSITION_TIME_MS / MAX_FRAME_LENGTH_MS ) +#define TRANSITION_INT_STEPS ( TRANSITION_FRAMES / ( TRANSITION_INT_NUM - 1 ) ) + +/* BWE factors to apply after packet loss */ +#define BWE_AFTER_LOSS_Q16 63570 + +/* Defines for CN generation */ +#define CNG_BUF_MASK_MAX 255 /* 2^floor(log2(MAX_FRAME_LENGTH))-1 */ +#define CNG_GAIN_SMTH_Q16 4634 /* 0.25^(1/4) */ +#define CNG_GAIN_SMTH_THRESHOLD_Q16 46396 /* -3 dB */ +#define CNG_NLSF_SMTH_Q16 16348 /* 0.25 */ + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/port/third_party/opus/silk/enc_API.c b/port/third_party/opus/silk/enc_API.c new file mode 100644 index 00000000..369caddd --- /dev/null +++ b/port/third_party/opus/silk/enc_API.c @@ -0,0 +1,591 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include "define.h" +#include "API.h" +#include "control.h" +#include "typedef.h" +#include "stack_alloc.h" +#include "structs.h" +#include "tuning_parameters.h" +#ifdef FIXED_POINT +#include "main_FIX.h" +#else +#include "main_FLP.h" +#endif + +#ifdef ENABLE_DRED +#include "dred_encoder.h" +#endif + +/***************************************/ +/* Read control structure from encoder */ +/***************************************/ +static opus_int silk_QueryEncoder( /* O Returns error code */ + const void *encState, /* I State */ + silk_EncControlStruct *encStatus /* O Encoder Status */ +); + +/****************************************/ +/* Encoder functions */ +/****************************************/ + +opus_int silk_Get_Encoder_Size( /* O Returns error code */ + opus_int *encSizeBytes /* O Number of bytes in SILK encoder state */ +) +{ + opus_int ret = SILK_NO_ERROR; + + *encSizeBytes = sizeof( silk_encoder ); + + return ret; +} + +/*************************/ +/* Init or Reset encoder */ +/*************************/ +opus_int silk_InitEncoder( /* O Returns error code */ + void *encState, /* I/O State */ + int arch, /* I Run-time architecture */ + silk_EncControlStruct *encStatus /* O Encoder Status */ +) +{ + silk_encoder *psEnc; + opus_int n, ret = SILK_NO_ERROR; + + psEnc = (silk_encoder *)encState; + + /* Reset encoder */ + silk_memset( psEnc, 0, sizeof( silk_encoder ) ); + for( n = 0; n < ENCODER_NUM_CHANNELS; n++ ) { + if( ret += silk_init_encoder( &psEnc->state_Fxx[ n ], arch ) ) { + celt_assert( 0 ); + } + } + + psEnc->nChannelsAPI = 1; + psEnc->nChannelsInternal = 1; + + /* Read control structure */ + if( ret += silk_QueryEncoder( encState, encStatus ) ) { + celt_assert( 0 ); + } + + return ret; +} + +/***************************************/ +/* Read control structure from encoder */ +/***************************************/ +static opus_int silk_QueryEncoder( /* O Returns error code */ + const void *encState, /* I State */ + silk_EncControlStruct *encStatus /* O Encoder Status */ +) +{ + opus_int ret = SILK_NO_ERROR; + silk_encoder_state_Fxx *state_Fxx; + silk_encoder *psEnc = (silk_encoder *)encState; + + state_Fxx = psEnc->state_Fxx; + + encStatus->nChannelsAPI = psEnc->nChannelsAPI; + encStatus->nChannelsInternal = psEnc->nChannelsInternal; + encStatus->API_sampleRate = state_Fxx[ 0 ].sCmn.API_fs_Hz; + encStatus->maxInternalSampleRate = state_Fxx[ 0 ].sCmn.maxInternal_fs_Hz; + encStatus->minInternalSampleRate = state_Fxx[ 0 ].sCmn.minInternal_fs_Hz; + encStatus->desiredInternalSampleRate = state_Fxx[ 0 ].sCmn.desiredInternal_fs_Hz; + encStatus->payloadSize_ms = state_Fxx[ 0 ].sCmn.PacketSize_ms; + encStatus->bitRate = state_Fxx[ 0 ].sCmn.TargetRate_bps; + encStatus->packetLossPercentage = state_Fxx[ 0 ].sCmn.PacketLoss_perc; + encStatus->complexity = state_Fxx[ 0 ].sCmn.Complexity; + encStatus->useInBandFEC = state_Fxx[ 0 ].sCmn.useInBandFEC; + encStatus->useDTX = state_Fxx[ 0 ].sCmn.useDTX; + encStatus->useCBR = state_Fxx[ 0 ].sCmn.useCBR; + encStatus->internalSampleRate = silk_SMULBB( state_Fxx[ 0 ].sCmn.fs_kHz, 1000 ); + encStatus->allowBandwidthSwitch = state_Fxx[ 0 ].sCmn.allow_bandwidth_switch; + encStatus->inWBmodeWithoutVariableLP = state_Fxx[ 0 ].sCmn.fs_kHz == 16 && state_Fxx[ 0 ].sCmn.sLP.mode == 0; + + return ret; +} + + +/**************************/ +/* Encode frame with Silk */ +/**************************/ +/* Note: if prefillFlag is set, the input must contain 10 ms of audio, irrespective of what */ +/* encControl->payloadSize_ms is set to */ +opus_int silk_Encode( /* O Returns error code */ + void *encState, /* I/O State */ + silk_EncControlStruct *encControl, /* I Control status */ + const opus_int16 *samplesIn, /* I Speech sample input vector */ + opus_int nSamplesIn, /* I Number of samples in input vector */ + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + opus_int32 *nBytesOut, /* I/O Number of bytes in payload (input: Max bytes) */ + const opus_int prefillFlag, /* I Flag to indicate prefilling buffers no coding */ + opus_int activity /* I Decision of Opus voice activity detector */ +) +{ + opus_int n, i, nBits, flags, tmp_payloadSize_ms = 0, tmp_complexity = 0, ret = 0; + opus_int nSamplesToBuffer, nSamplesToBufferMax, nBlocksOf10ms; + opus_int nSamplesFromInput = 0, nSamplesFromInputMax; + opus_int speech_act_thr_for_switch_Q8; + opus_int32 TargetRate_bps, MStargetRates_bps[ 2 ], channelRate_bps, LBRR_symbol, sum; + silk_encoder *psEnc = ( silk_encoder * )encState; + VARDECL( opus_int16, buf ); + opus_int transition, curr_block, tot_blocks; + SAVE_STACK; + + if (encControl->reducedDependency) + { + psEnc->state_Fxx[0].sCmn.first_frame_after_reset = 1; + psEnc->state_Fxx[1].sCmn.first_frame_after_reset = 1; + } + psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded = psEnc->state_Fxx[ 1 ].sCmn.nFramesEncoded = 0; + + /* Check values in encoder control structure */ + if( ( ret = check_control_input( encControl ) ) != 0 ) { + celt_assert( 0 ); + RESTORE_STACK; + return ret; + } + + encControl->switchReady = 0; + + if( encControl->nChannelsInternal > psEnc->nChannelsInternal ) { + /* Mono -> Stereo transition: init state of second channel and stereo state */ + ret += silk_init_encoder( &psEnc->state_Fxx[ 1 ], psEnc->state_Fxx[ 0 ].sCmn.arch ); + silk_memset( psEnc->sStereo.pred_prev_Q13, 0, sizeof( psEnc->sStereo.pred_prev_Q13 ) ); + silk_memset( psEnc->sStereo.sSide, 0, sizeof( psEnc->sStereo.sSide ) ); + psEnc->sStereo.mid_side_amp_Q0[ 0 ] = 0; + psEnc->sStereo.mid_side_amp_Q0[ 1 ] = 1; + psEnc->sStereo.mid_side_amp_Q0[ 2 ] = 0; + psEnc->sStereo.mid_side_amp_Q0[ 3 ] = 1; + psEnc->sStereo.width_prev_Q14 = 0; + psEnc->sStereo.smth_width_Q14 = SILK_FIX_CONST( 1, 14 ); + if( psEnc->nChannelsAPI == 2 ) { + silk_memcpy( &psEnc->state_Fxx[ 1 ].sCmn.resampler_state, &psEnc->state_Fxx[ 0 ].sCmn.resampler_state, sizeof( silk_resampler_state_struct ) ); + silk_memcpy( &psEnc->state_Fxx[ 1 ].sCmn.In_HP_State, &psEnc->state_Fxx[ 0 ].sCmn.In_HP_State, sizeof( psEnc->state_Fxx[ 1 ].sCmn.In_HP_State ) ); + } + } + + transition = (encControl->payloadSize_ms != psEnc->state_Fxx[ 0 ].sCmn.PacketSize_ms) || (psEnc->nChannelsInternal != encControl->nChannelsInternal); + + psEnc->nChannelsAPI = encControl->nChannelsAPI; + psEnc->nChannelsInternal = encControl->nChannelsInternal; + + nBlocksOf10ms = silk_DIV32( 100 * nSamplesIn, encControl->API_sampleRate ); + tot_blocks = ( nBlocksOf10ms > 1 ) ? nBlocksOf10ms >> 1 : 1; + curr_block = 0; + if( prefillFlag ) { + silk_LP_state save_LP; + /* Only accept input length of 10 ms */ + if( nBlocksOf10ms != 1 ) { + celt_assert( 0 ); + RESTORE_STACK; + return SILK_ENC_INPUT_INVALID_NO_OF_SAMPLES; + } + if ( prefillFlag == 2 ) { + save_LP = psEnc->state_Fxx[ 0 ].sCmn.sLP; + /* Save the sampling rate so the bandwidth switching code can keep handling transitions. */ + save_LP.saved_fs_kHz = psEnc->state_Fxx[ 0 ].sCmn.fs_kHz; + } + /* Reset Encoder */ + for( n = 0; n < encControl->nChannelsInternal; n++ ) { + ret = silk_init_encoder( &psEnc->state_Fxx[ n ], psEnc->state_Fxx[ n ].sCmn.arch ); + /* Restore the variable LP state. */ + if ( prefillFlag == 2 ) { + psEnc->state_Fxx[ n ].sCmn.sLP = save_LP; + } + celt_assert( !ret ); + } + tmp_payloadSize_ms = encControl->payloadSize_ms; + encControl->payloadSize_ms = 10; + tmp_complexity = encControl->complexity; + encControl->complexity = 0; + for( n = 0; n < encControl->nChannelsInternal; n++ ) { + psEnc->state_Fxx[ n ].sCmn.controlled_since_last_payload = 0; + psEnc->state_Fxx[ n ].sCmn.prefillFlag = 1; + } + } else { + /* Only accept input lengths that are a multiple of 10 ms */ + if( nBlocksOf10ms * encControl->API_sampleRate != 100 * nSamplesIn || nSamplesIn < 0 ) { + celt_assert( 0 ); + RESTORE_STACK; + return SILK_ENC_INPUT_INVALID_NO_OF_SAMPLES; + } + /* Make sure no more than one packet can be produced */ + if( 1000 * (opus_int32)nSamplesIn > encControl->payloadSize_ms * encControl->API_sampleRate ) { + celt_assert( 0 ); + RESTORE_STACK; + return SILK_ENC_INPUT_INVALID_NO_OF_SAMPLES; + } + } + + for( n = 0; n < encControl->nChannelsInternal; n++ ) { + /* Force the side channel to the same rate as the mid */ + opus_int force_fs_kHz = (n==1) ? psEnc->state_Fxx[0].sCmn.fs_kHz : 0; + if( ( ret = silk_control_encoder( &psEnc->state_Fxx[ n ], encControl, psEnc->allowBandwidthSwitch, n, force_fs_kHz ) ) != 0 ) { + silk_assert( 0 ); + RESTORE_STACK; + return ret; + } + if( psEnc->state_Fxx[n].sCmn.first_frame_after_reset || transition ) { + for( i = 0; i < psEnc->state_Fxx[ 0 ].sCmn.nFramesPerPacket; i++ ) { + psEnc->state_Fxx[ n ].sCmn.LBRR_flags[ i ] = 0; + } + } + psEnc->state_Fxx[ n ].sCmn.inDTX = psEnc->state_Fxx[ n ].sCmn.useDTX; + } + celt_assert( encControl->nChannelsInternal == 1 || psEnc->state_Fxx[ 0 ].sCmn.fs_kHz == psEnc->state_Fxx[ 1 ].sCmn.fs_kHz ); + + /* Input buffering/resampling and encoding */ + nSamplesToBufferMax = + 10 * nBlocksOf10ms * psEnc->state_Fxx[ 0 ].sCmn.fs_kHz; + nSamplesFromInputMax = + silk_DIV32_16( nSamplesToBufferMax * + psEnc->state_Fxx[ 0 ].sCmn.API_fs_Hz, + psEnc->state_Fxx[ 0 ].sCmn.fs_kHz * 1000 ); + ALLOC( buf, nSamplesFromInputMax, opus_int16 ); + while( 1 ) { + int curr_nBitsUsedLBRR = 0; + nSamplesToBuffer = psEnc->state_Fxx[ 0 ].sCmn.frame_length - psEnc->state_Fxx[ 0 ].sCmn.inputBufIx; + nSamplesToBuffer = silk_min( nSamplesToBuffer, nSamplesToBufferMax ); + nSamplesFromInput = silk_DIV32_16( nSamplesToBuffer * psEnc->state_Fxx[ 0 ].sCmn.API_fs_Hz, psEnc->state_Fxx[ 0 ].sCmn.fs_kHz * 1000 ); + /* Resample and write to buffer */ + if( encControl->nChannelsAPI == 2 && encControl->nChannelsInternal == 2 ) { + opus_int id = psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded; + for( n = 0; n < nSamplesFromInput; n++ ) { + buf[ n ] = samplesIn[ 2 * n ]; + } + /* Making sure to start both resamplers from the same state when switching from mono to stereo */ + if( psEnc->nPrevChannelsInternal == 1 && id==0 ) { + silk_memcpy( &psEnc->state_Fxx[ 1 ].sCmn.resampler_state, &psEnc->state_Fxx[ 0 ].sCmn.resampler_state, sizeof(psEnc->state_Fxx[ 1 ].sCmn.resampler_state)); + } + + ret += silk_resampler( &psEnc->state_Fxx[ 0 ].sCmn.resampler_state, + &psEnc->state_Fxx[ 0 ].sCmn.inputBuf[ psEnc->state_Fxx[ 0 ].sCmn.inputBufIx + 2 ], buf, nSamplesFromInput ); + psEnc->state_Fxx[ 0 ].sCmn.inputBufIx += nSamplesToBuffer; + + nSamplesToBuffer = psEnc->state_Fxx[ 1 ].sCmn.frame_length - psEnc->state_Fxx[ 1 ].sCmn.inputBufIx; + nSamplesToBuffer = silk_min( nSamplesToBuffer, 10 * nBlocksOf10ms * psEnc->state_Fxx[ 1 ].sCmn.fs_kHz ); + for( n = 0; n < nSamplesFromInput; n++ ) { + buf[ n ] = samplesIn[ 2 * n + 1 ]; + } + ret += silk_resampler( &psEnc->state_Fxx[ 1 ].sCmn.resampler_state, + &psEnc->state_Fxx[ 1 ].sCmn.inputBuf[ psEnc->state_Fxx[ 1 ].sCmn.inputBufIx + 2 ], buf, nSamplesFromInput ); + + psEnc->state_Fxx[ 1 ].sCmn.inputBufIx += nSamplesToBuffer; + } else if( encControl->nChannelsAPI == 2 && encControl->nChannelsInternal == 1 ) { + /* Combine left and right channels before resampling */ + for( n = 0; n < nSamplesFromInput; n++ ) { + sum = samplesIn[ 2 * n ] + samplesIn[ 2 * n + 1 ]; + buf[ n ] = (opus_int16)silk_RSHIFT_ROUND( sum, 1 ); + } + ret += silk_resampler( &psEnc->state_Fxx[ 0 ].sCmn.resampler_state, + &psEnc->state_Fxx[ 0 ].sCmn.inputBuf[ psEnc->state_Fxx[ 0 ].sCmn.inputBufIx + 2 ], buf, nSamplesFromInput ); + /* On the first mono frame, average the results for the two resampler states */ + if( psEnc->nPrevChannelsInternal == 2 && psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded == 0 ) { + ret += silk_resampler( &psEnc->state_Fxx[ 1 ].sCmn.resampler_state, + &psEnc->state_Fxx[ 1 ].sCmn.inputBuf[ psEnc->state_Fxx[ 1 ].sCmn.inputBufIx + 2 ], buf, nSamplesFromInput ); + for( n = 0; n < psEnc->state_Fxx[ 0 ].sCmn.frame_length; n++ ) { + psEnc->state_Fxx[ 0 ].sCmn.inputBuf[ psEnc->state_Fxx[ 0 ].sCmn.inputBufIx+n+2 ] = + silk_RSHIFT(psEnc->state_Fxx[ 0 ].sCmn.inputBuf[ psEnc->state_Fxx[ 0 ].sCmn.inputBufIx+n+2 ] + + psEnc->state_Fxx[ 1 ].sCmn.inputBuf[ psEnc->state_Fxx[ 1 ].sCmn.inputBufIx+n+2 ], 1); + } + } + psEnc->state_Fxx[ 0 ].sCmn.inputBufIx += nSamplesToBuffer; + } else { + celt_assert( encControl->nChannelsAPI == 1 && encControl->nChannelsInternal == 1 ); + silk_memcpy(buf, samplesIn, nSamplesFromInput*sizeof(opus_int16)); + ret += silk_resampler( &psEnc->state_Fxx[ 0 ].sCmn.resampler_state, + &psEnc->state_Fxx[ 0 ].sCmn.inputBuf[ psEnc->state_Fxx[ 0 ].sCmn.inputBufIx + 2 ], buf, nSamplesFromInput ); + psEnc->state_Fxx[ 0 ].sCmn.inputBufIx += nSamplesToBuffer; + } + + samplesIn += nSamplesFromInput * encControl->nChannelsAPI; + nSamplesIn -= nSamplesFromInput; + + /* Default */ + psEnc->allowBandwidthSwitch = 0; + + /* Silk encoder */ + if( psEnc->state_Fxx[ 0 ].sCmn.inputBufIx >= psEnc->state_Fxx[ 0 ].sCmn.frame_length ) { + /* Enough data in input buffer, so encode */ + celt_assert( psEnc->state_Fxx[ 0 ].sCmn.inputBufIx == psEnc->state_Fxx[ 0 ].sCmn.frame_length ); + celt_assert( encControl->nChannelsInternal == 1 || psEnc->state_Fxx[ 1 ].sCmn.inputBufIx == psEnc->state_Fxx[ 1 ].sCmn.frame_length ); + + /* Deal with LBRR data */ + if( psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded == 0 && !prefillFlag ) { + /* Create space at start of payload for VAD and FEC flags */ + opus_uint8 iCDF[ 2 ] = { 0, 0 }; + iCDF[ 0 ] = 256 - silk_RSHIFT( 256, ( psEnc->state_Fxx[ 0 ].sCmn.nFramesPerPacket + 1 ) * encControl->nChannelsInternal ); + ec_enc_icdf( psRangeEnc, 0, iCDF, 8 ); + curr_nBitsUsedLBRR = ec_tell( psRangeEnc ); + + /* Encode any LBRR data from previous packet */ + /* Encode LBRR flags */ + for( n = 0; n < encControl->nChannelsInternal; n++ ) { + LBRR_symbol = 0; + for( i = 0; i < psEnc->state_Fxx[ n ].sCmn.nFramesPerPacket; i++ ) { + LBRR_symbol |= silk_LSHIFT( psEnc->state_Fxx[ n ].sCmn.LBRR_flags[ i ], i ); + } + psEnc->state_Fxx[ n ].sCmn.LBRR_flag = LBRR_symbol > 0 ? 1 : 0; + if( LBRR_symbol && psEnc->state_Fxx[ n ].sCmn.nFramesPerPacket > 1 ) { + ec_enc_icdf( psRangeEnc, LBRR_symbol - 1, silk_LBRR_flags_iCDF_ptr[ psEnc->state_Fxx[ n ].sCmn.nFramesPerPacket - 2 ], 8 ); + } + } + + /* Code LBRR indices and excitation signals */ + for( i = 0; i < psEnc->state_Fxx[ 0 ].sCmn.nFramesPerPacket; i++ ) { + for( n = 0; n < encControl->nChannelsInternal; n++ ) { + if( psEnc->state_Fxx[ n ].sCmn.LBRR_flags[ i ] ) { + opus_int condCoding; + + if( encControl->nChannelsInternal == 2 && n == 0 ) { + silk_stereo_encode_pred( psRangeEnc, psEnc->sStereo.predIx[ i ] ); + /* For LBRR data there's no need to code the mid-only flag if the side-channel LBRR flag is set */ + if( psEnc->state_Fxx[ 1 ].sCmn.LBRR_flags[ i ] == 0 ) { + silk_stereo_encode_mid_only( psRangeEnc, psEnc->sStereo.mid_only_flags[ i ] ); + } + } + /* Use conditional coding if previous frame available */ + if( i > 0 && psEnc->state_Fxx[ n ].sCmn.LBRR_flags[ i - 1 ] ) { + condCoding = CODE_CONDITIONALLY; + } else { + condCoding = CODE_INDEPENDENTLY; + } + silk_encode_indices( &psEnc->state_Fxx[ n ].sCmn, psRangeEnc, i, 1, condCoding ); + silk_encode_pulses( psRangeEnc, psEnc->state_Fxx[ n ].sCmn.indices_LBRR[i].signalType, psEnc->state_Fxx[ n ].sCmn.indices_LBRR[i].quantOffsetType, + psEnc->state_Fxx[ n ].sCmn.pulses_LBRR[ i ], psEnc->state_Fxx[ n ].sCmn.frame_length ); + } + } + } + + /* Reset LBRR flags */ + for( n = 0; n < encControl->nChannelsInternal; n++ ) { + silk_memset( psEnc->state_Fxx[ n ].sCmn.LBRR_flags, 0, sizeof( psEnc->state_Fxx[ n ].sCmn.LBRR_flags ) ); + } + curr_nBitsUsedLBRR = ec_tell( psRangeEnc ) - curr_nBitsUsedLBRR; + } + + silk_HP_variable_cutoff( psEnc->state_Fxx ); + + /* Total target bits for packet */ + nBits = silk_DIV32_16( silk_MUL( encControl->bitRate, encControl->payloadSize_ms ), 1000 ); + /* Subtract bits used for LBRR */ + if( !prefillFlag ) { + /* psEnc->nBitsUsedLBRR is an exponential moving average of the LBRR usage, + except that for the first LBRR frame it does no averaging and for the first + frame after after LBRR, it goes back to zero immediately. */ + if ( curr_nBitsUsedLBRR < 10 ) { + psEnc->nBitsUsedLBRR = 0; + } else if ( psEnc->nBitsUsedLBRR < 10) { + psEnc->nBitsUsedLBRR = curr_nBitsUsedLBRR; + } else { + psEnc->nBitsUsedLBRR = ( psEnc->nBitsUsedLBRR + curr_nBitsUsedLBRR ) / 2; + } + nBits -= psEnc->nBitsUsedLBRR; + } + /* Divide by number of uncoded frames left in packet */ + nBits = silk_DIV32_16( nBits, psEnc->state_Fxx[ 0 ].sCmn.nFramesPerPacket ); + /* Convert to bits/second */ + if( encControl->payloadSize_ms == 10 ) { + TargetRate_bps = silk_SMULBB( nBits, 100 ); + } else { + TargetRate_bps = silk_SMULBB( nBits, 50 ); + } + /* Subtract fraction of bits in excess of target in previous frames and packets */ + TargetRate_bps -= silk_DIV32_16( silk_MUL( psEnc->nBitsExceeded, 1000 ), BITRESERVOIR_DECAY_TIME_MS ); + if( !prefillFlag && psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded > 0 ) { + /* Compare actual vs target bits so far in this packet */ + opus_int32 bitsBalance = ec_tell( psRangeEnc ) - psEnc->nBitsUsedLBRR - nBits * psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded; + TargetRate_bps -= silk_DIV32_16( silk_MUL( bitsBalance, 1000 ), BITRESERVOIR_DECAY_TIME_MS ); + } + /* Never exceed input bitrate */ + TargetRate_bps = silk_LIMIT( TargetRate_bps, encControl->bitRate, 5000 ); + + /* Convert Left/Right to Mid/Side */ + if( encControl->nChannelsInternal == 2 ) { + silk_stereo_LR_to_MS( &psEnc->sStereo, &psEnc->state_Fxx[ 0 ].sCmn.inputBuf[ 2 ], &psEnc->state_Fxx[ 1 ].sCmn.inputBuf[ 2 ], + psEnc->sStereo.predIx[ psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded ], &psEnc->sStereo.mid_only_flags[ psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded ], + MStargetRates_bps, TargetRate_bps, psEnc->state_Fxx[ 0 ].sCmn.speech_activity_Q8, encControl->toMono, + psEnc->state_Fxx[ 0 ].sCmn.fs_kHz, psEnc->state_Fxx[ 0 ].sCmn.frame_length ); + if( psEnc->sStereo.mid_only_flags[ psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded ] == 0 ) { + /* Reset side channel encoder memory for first frame with side coding */ + if( psEnc->prev_decode_only_middle == 1 ) { + silk_memset( &psEnc->state_Fxx[ 1 ].sShape, 0, sizeof( psEnc->state_Fxx[ 1 ].sShape ) ); + silk_memset( &psEnc->state_Fxx[ 1 ].sCmn.sNSQ, 0, sizeof( psEnc->state_Fxx[ 1 ].sCmn.sNSQ ) ); + silk_memset( psEnc->state_Fxx[ 1 ].sCmn.prev_NLSFq_Q15, 0, sizeof( psEnc->state_Fxx[ 1 ].sCmn.prev_NLSFq_Q15 ) ); + silk_memset( &psEnc->state_Fxx[ 1 ].sCmn.sLP.In_LP_State, 0, sizeof( psEnc->state_Fxx[ 1 ].sCmn.sLP.In_LP_State ) ); + psEnc->state_Fxx[ 1 ].sCmn.prevLag = 100; + psEnc->state_Fxx[ 1 ].sCmn.sNSQ.lagPrev = 100; + psEnc->state_Fxx[ 1 ].sShape.LastGainIndex = 10; + psEnc->state_Fxx[ 1 ].sCmn.prevSignalType = TYPE_NO_VOICE_ACTIVITY; + psEnc->state_Fxx[ 1 ].sCmn.sNSQ.prev_gain_Q16 = 65536; + psEnc->state_Fxx[ 1 ].sCmn.first_frame_after_reset = 1; + } + silk_encode_do_VAD_Fxx( &psEnc->state_Fxx[ 1 ], activity ); + } else { + psEnc->state_Fxx[ 1 ].sCmn.VAD_flags[ psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded ] = 0; + } + if( !prefillFlag ) { + silk_stereo_encode_pred( psRangeEnc, psEnc->sStereo.predIx[ psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded ] ); + if( psEnc->state_Fxx[ 1 ].sCmn.VAD_flags[ psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded ] == 0 ) { + silk_stereo_encode_mid_only( psRangeEnc, psEnc->sStereo.mid_only_flags[ psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded ] ); + } + } + } else { + /* Buffering */ + silk_memcpy( psEnc->state_Fxx[ 0 ].sCmn.inputBuf, psEnc->sStereo.sMid, 2 * sizeof( opus_int16 ) ); + silk_memcpy( psEnc->sStereo.sMid, &psEnc->state_Fxx[ 0 ].sCmn.inputBuf[ psEnc->state_Fxx[ 0 ].sCmn.frame_length ], 2 * sizeof( opus_int16 ) ); + } + silk_encode_do_VAD_Fxx( &psEnc->state_Fxx[ 0 ], activity ); + + /* Encode */ + for( n = 0; n < encControl->nChannelsInternal; n++ ) { + opus_int maxBits, useCBR; + + /* Handling rate constraints */ + maxBits = encControl->maxBits; + if( tot_blocks == 2 && curr_block == 0 ) { + maxBits = maxBits * 3 / 5; + } else if( tot_blocks == 3 ) { + if( curr_block == 0 ) { + maxBits = maxBits * 2 / 5; + } else if( curr_block == 1 ) { + maxBits = maxBits * 3 / 4; + } + } + useCBR = encControl->useCBR && curr_block == tot_blocks - 1; + + if( encControl->nChannelsInternal == 1 ) { + channelRate_bps = TargetRate_bps; + } else { + channelRate_bps = MStargetRates_bps[ n ]; + if( n == 0 && MStargetRates_bps[ 1 ] > 0 ) { + useCBR = 0; + /* Give mid up to 1/2 of the max bits for that frame */ + maxBits -= encControl->maxBits / ( tot_blocks * 2 ); + } + } + + if( channelRate_bps > 0 ) { + opus_int condCoding; + + silk_control_SNR( &psEnc->state_Fxx[ n ].sCmn, channelRate_bps ); + + /* Use independent coding if no previous frame available */ + if( psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded - n <= 0 ) { + condCoding = CODE_INDEPENDENTLY; + } else if( n > 0 && psEnc->prev_decode_only_middle ) { + /* If we skipped a side frame in this packet, we don't + need LTP scaling; the LTP state is well-defined. */ + condCoding = CODE_INDEPENDENTLY_NO_LTP_SCALING; + } else { + condCoding = CODE_CONDITIONALLY; + } + if( ( ret = silk_encode_frame_Fxx( &psEnc->state_Fxx[ n ], nBytesOut, psRangeEnc, condCoding, maxBits, useCBR ) ) != 0 ) { + silk_assert( 0 ); + } + } + psEnc->state_Fxx[ n ].sCmn.controlled_since_last_payload = 0; + psEnc->state_Fxx[ n ].sCmn.inputBufIx = 0; + psEnc->state_Fxx[ n ].sCmn.nFramesEncoded++; + } + psEnc->prev_decode_only_middle = psEnc->sStereo.mid_only_flags[ psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded - 1 ]; + + /* Insert VAD and FEC flags at beginning of bitstream */ + if( *nBytesOut > 0 && psEnc->state_Fxx[ 0 ].sCmn.nFramesEncoded == psEnc->state_Fxx[ 0 ].sCmn.nFramesPerPacket) { + flags = 0; + for( n = 0; n < encControl->nChannelsInternal; n++ ) { + for( i = 0; i < psEnc->state_Fxx[ n ].sCmn.nFramesPerPacket; i++ ) { + flags = silk_LSHIFT( flags, 1 ); + flags |= psEnc->state_Fxx[ n ].sCmn.VAD_flags[ i ]; + } + flags = silk_LSHIFT( flags, 1 ); + flags |= psEnc->state_Fxx[ n ].sCmn.LBRR_flag; + } + if( !prefillFlag ) { + ec_enc_patch_initial_bits( psRangeEnc, flags, ( psEnc->state_Fxx[ 0 ].sCmn.nFramesPerPacket + 1 ) * encControl->nChannelsInternal ); + } + + /* Return zero bytes if all channels DTXed */ + if( psEnc->state_Fxx[ 0 ].sCmn.inDTX && ( encControl->nChannelsInternal == 1 || psEnc->state_Fxx[ 1 ].sCmn.inDTX ) ) { + *nBytesOut = 0; + } + + psEnc->nBitsExceeded += *nBytesOut * 8; + psEnc->nBitsExceeded -= silk_DIV32_16( silk_MUL( encControl->bitRate, encControl->payloadSize_ms ), 1000 ); + psEnc->nBitsExceeded = silk_LIMIT( psEnc->nBitsExceeded, 0, 10000 ); + + /* Update flag indicating if bandwidth switching is allowed */ + speech_act_thr_for_switch_Q8 = silk_SMLAWB( SILK_FIX_CONST( SPEECH_ACTIVITY_DTX_THRES, 8 ), + SILK_FIX_CONST( ( 1 - SPEECH_ACTIVITY_DTX_THRES ) / MAX_BANDWIDTH_SWITCH_DELAY_MS, 16 + 8 ), psEnc->timeSinceSwitchAllowed_ms ); + if( psEnc->state_Fxx[ 0 ].sCmn.speech_activity_Q8 < speech_act_thr_for_switch_Q8 ) { + psEnc->allowBandwidthSwitch = 1; + psEnc->timeSinceSwitchAllowed_ms = 0; + } else { + psEnc->allowBandwidthSwitch = 0; + psEnc->timeSinceSwitchAllowed_ms += encControl->payloadSize_ms; + } + } + + if( nSamplesIn == 0 ) { + break; + } + } else { + break; + } + curr_block++; + } + + psEnc->nPrevChannelsInternal = encControl->nChannelsInternal; + + encControl->allowBandwidthSwitch = psEnc->allowBandwidthSwitch; + encControl->inWBmodeWithoutVariableLP = psEnc->state_Fxx[ 0 ].sCmn.fs_kHz == 16 && psEnc->state_Fxx[ 0 ].sCmn.sLP.mode == 0; + encControl->internalSampleRate = silk_SMULBB( psEnc->state_Fxx[ 0 ].sCmn.fs_kHz, 1000 ); + encControl->stereoWidth_Q14 = encControl->toMono ? 0 : psEnc->sStereo.smth_width_Q14; + if( prefillFlag ) { + encControl->payloadSize_ms = tmp_payloadSize_ms; + encControl->complexity = tmp_complexity; + for( n = 0; n < encControl->nChannelsInternal; n++ ) { + psEnc->state_Fxx[ n ].sCmn.controlled_since_last_payload = 0; + psEnc->state_Fxx[ n ].sCmn.prefillFlag = 0; + } + } + + encControl->signalType = psEnc->state_Fxx[0].sCmn.indices.signalType; + encControl->offset = silk_Quantization_Offsets_Q10 + [ psEnc->state_Fxx[0].sCmn.indices.signalType >> 1 ] + [ psEnc->state_Fxx[0].sCmn.indices.quantOffsetType ]; + RESTORE_STACK; + return ret; +} + diff --git a/port/third_party/opus/silk/encode_indices.c b/port/third_party/opus/silk/encode_indices.c new file mode 100644 index 00000000..4bcbc334 --- /dev/null +++ b/port/third_party/opus/silk/encode_indices.c @@ -0,0 +1,181 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Encode side-information parameters to payload */ +void silk_encode_indices( + silk_encoder_state *psEncC, /* I/O Encoder state */ + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + opus_int FrameIndex, /* I Frame number */ + opus_int encode_LBRR, /* I Flag indicating LBRR data is being encoded */ + opus_int condCoding /* I The type of conditional coding to use */ +) +{ + opus_int i, k, typeOffset; + opus_int encode_absolute_lagIndex, delta_lagIndex; + opus_int16 ec_ix[ MAX_LPC_ORDER ]; + opus_uint8 pred_Q8[ MAX_LPC_ORDER ]; + const SideInfoIndices *psIndices; + + if( encode_LBRR ) { + psIndices = &psEncC->indices_LBRR[ FrameIndex ]; + } else { + psIndices = &psEncC->indices; + } + + /*******************************************/ + /* Encode signal type and quantizer offset */ + /*******************************************/ + typeOffset = 2 * psIndices->signalType + psIndices->quantOffsetType; + celt_assert( typeOffset >= 0 && typeOffset < 6 ); + celt_assert( encode_LBRR == 0 || typeOffset >= 2 ); + if( encode_LBRR || typeOffset >= 2 ) { + ec_enc_icdf( psRangeEnc, typeOffset - 2, silk_type_offset_VAD_iCDF, 8 ); + } else { + ec_enc_icdf( psRangeEnc, typeOffset, silk_type_offset_no_VAD_iCDF, 8 ); + } + + /****************/ + /* Encode gains */ + /****************/ + /* first subframe */ + if( condCoding == CODE_CONDITIONALLY ) { + /* conditional coding */ + silk_assert( psIndices->GainsIndices[ 0 ] >= 0 && psIndices->GainsIndices[ 0 ] < MAX_DELTA_GAIN_QUANT - MIN_DELTA_GAIN_QUANT + 1 ); + ec_enc_icdf( psRangeEnc, psIndices->GainsIndices[ 0 ], silk_delta_gain_iCDF, 8 ); + } else { + /* independent coding, in two stages: MSB bits followed by 3 LSBs */ + silk_assert( psIndices->GainsIndices[ 0 ] >= 0 && psIndices->GainsIndices[ 0 ] < N_LEVELS_QGAIN ); + ec_enc_icdf( psRangeEnc, silk_RSHIFT( psIndices->GainsIndices[ 0 ], 3 ), silk_gain_iCDF[ psIndices->signalType ], 8 ); + ec_enc_icdf( psRangeEnc, psIndices->GainsIndices[ 0 ] & 7, silk_uniform8_iCDF, 8 ); + } + + /* remaining subframes */ + for( i = 1; i < psEncC->nb_subfr; i++ ) { + silk_assert( psIndices->GainsIndices[ i ] >= 0 && psIndices->GainsIndices[ i ] < MAX_DELTA_GAIN_QUANT - MIN_DELTA_GAIN_QUANT + 1 ); + ec_enc_icdf( psRangeEnc, psIndices->GainsIndices[ i ], silk_delta_gain_iCDF, 8 ); + } + + /****************/ + /* Encode NLSFs */ + /****************/ + ec_enc_icdf( psRangeEnc, psIndices->NLSFIndices[ 0 ], &psEncC->psNLSF_CB->CB1_iCDF[ ( psIndices->signalType >> 1 ) * psEncC->psNLSF_CB->nVectors ], 8 ); + silk_NLSF_unpack( ec_ix, pred_Q8, psEncC->psNLSF_CB, psIndices->NLSFIndices[ 0 ] ); + celt_assert( psEncC->psNLSF_CB->order == psEncC->predictLPCOrder ); + for( i = 0; i < psEncC->psNLSF_CB->order; i++ ) { + if( psIndices->NLSFIndices[ i+1 ] >= NLSF_QUANT_MAX_AMPLITUDE ) { + ec_enc_icdf( psRangeEnc, 2 * NLSF_QUANT_MAX_AMPLITUDE, &psEncC->psNLSF_CB->ec_iCDF[ ec_ix[ i ] ], 8 ); + ec_enc_icdf( psRangeEnc, psIndices->NLSFIndices[ i+1 ] - NLSF_QUANT_MAX_AMPLITUDE, silk_NLSF_EXT_iCDF, 8 ); + } else if( psIndices->NLSFIndices[ i+1 ] <= -NLSF_QUANT_MAX_AMPLITUDE ) { + ec_enc_icdf( psRangeEnc, 0, &psEncC->psNLSF_CB->ec_iCDF[ ec_ix[ i ] ], 8 ); + ec_enc_icdf( psRangeEnc, -psIndices->NLSFIndices[ i+1 ] - NLSF_QUANT_MAX_AMPLITUDE, silk_NLSF_EXT_iCDF, 8 ); + } else { + ec_enc_icdf( psRangeEnc, psIndices->NLSFIndices[ i+1 ] + NLSF_QUANT_MAX_AMPLITUDE, &psEncC->psNLSF_CB->ec_iCDF[ ec_ix[ i ] ], 8 ); + } + } + + /* Encode NLSF interpolation factor */ + if( psEncC->nb_subfr == MAX_NB_SUBFR ) { + silk_assert( psIndices->NLSFInterpCoef_Q2 >= 0 && psIndices->NLSFInterpCoef_Q2 < 5 ); + ec_enc_icdf( psRangeEnc, psIndices->NLSFInterpCoef_Q2, silk_NLSF_interpolation_factor_iCDF, 8 ); + } + + if( psIndices->signalType == TYPE_VOICED ) + { + /*********************/ + /* Encode pitch lags */ + /*********************/ + /* lag index */ + encode_absolute_lagIndex = 1; + if( condCoding == CODE_CONDITIONALLY && psEncC->ec_prevSignalType == TYPE_VOICED ) { + /* Delta Encoding */ + delta_lagIndex = psIndices->lagIndex - psEncC->ec_prevLagIndex; + if( delta_lagIndex < -8 || delta_lagIndex > 11 ) { + delta_lagIndex = 0; + } else { + delta_lagIndex = delta_lagIndex + 9; + encode_absolute_lagIndex = 0; /* Only use delta */ + } + silk_assert( delta_lagIndex >= 0 && delta_lagIndex < 21 ); + ec_enc_icdf( psRangeEnc, delta_lagIndex, silk_pitch_delta_iCDF, 8 ); + } + if( encode_absolute_lagIndex ) { + /* Absolute encoding */ + opus_int32 pitch_high_bits, pitch_low_bits; + pitch_high_bits = silk_DIV32_16( psIndices->lagIndex, silk_RSHIFT( psEncC->fs_kHz, 1 ) ); + pitch_low_bits = psIndices->lagIndex - silk_SMULBB( pitch_high_bits, silk_RSHIFT( psEncC->fs_kHz, 1 ) ); + silk_assert( pitch_low_bits < psEncC->fs_kHz / 2 ); + silk_assert( pitch_high_bits < 32 ); + ec_enc_icdf( psRangeEnc, pitch_high_bits, silk_pitch_lag_iCDF, 8 ); + ec_enc_icdf( psRangeEnc, pitch_low_bits, psEncC->pitch_lag_low_bits_iCDF, 8 ); + } + psEncC->ec_prevLagIndex = psIndices->lagIndex; + + /* Countour index */ + silk_assert( psIndices->contourIndex >= 0 ); + silk_assert( ( psIndices->contourIndex < 34 && psEncC->fs_kHz > 8 && psEncC->nb_subfr == 4 ) || + ( psIndices->contourIndex < 11 && psEncC->fs_kHz == 8 && psEncC->nb_subfr == 4 ) || + ( psIndices->contourIndex < 12 && psEncC->fs_kHz > 8 && psEncC->nb_subfr == 2 ) || + ( psIndices->contourIndex < 3 && psEncC->fs_kHz == 8 && psEncC->nb_subfr == 2 ) ); + ec_enc_icdf( psRangeEnc, psIndices->contourIndex, psEncC->pitch_contour_iCDF, 8 ); + + /********************/ + /* Encode LTP gains */ + /********************/ + /* PERIndex value */ + silk_assert( psIndices->PERIndex >= 0 && psIndices->PERIndex < 3 ); + ec_enc_icdf( psRangeEnc, psIndices->PERIndex, silk_LTP_per_index_iCDF, 8 ); + + /* Codebook Indices */ + for( k = 0; k < psEncC->nb_subfr; k++ ) { + silk_assert( psIndices->LTPIndex[ k ] >= 0 && psIndices->LTPIndex[ k ] < ( 8 << psIndices->PERIndex ) ); + ec_enc_icdf( psRangeEnc, psIndices->LTPIndex[ k ], silk_LTP_gain_iCDF_ptrs[ psIndices->PERIndex ], 8 ); + } + + /**********************/ + /* Encode LTP scaling */ + /**********************/ + if( condCoding == CODE_INDEPENDENTLY ) { + silk_assert( psIndices->LTP_scaleIndex >= 0 && psIndices->LTP_scaleIndex < 3 ); + ec_enc_icdf( psRangeEnc, psIndices->LTP_scaleIndex, silk_LTPscale_iCDF, 8 ); + } + silk_assert( !condCoding || psIndices->LTP_scaleIndex == 0 ); + } + + psEncC->ec_prevSignalType = psIndices->signalType; + + /***************/ + /* Encode seed */ + /***************/ + silk_assert( psIndices->Seed >= 0 && psIndices->Seed < 4 ); + ec_enc_icdf( psRangeEnc, psIndices->Seed, silk_uniform4_iCDF, 8 ); +} diff --git a/port/third_party/opus/silk/encode_pulses.c b/port/third_party/opus/silk/encode_pulses.c new file mode 100644 index 00000000..8a199913 --- /dev/null +++ b/port/third_party/opus/silk/encode_pulses.c @@ -0,0 +1,206 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" + +/*********************************************/ +/* Encode quantization indices of excitation */ +/*********************************************/ + +static OPUS_INLINE opus_int combine_and_check( /* return ok */ + opus_int *pulses_comb, /* O */ + const opus_int *pulses_in, /* I */ + opus_int max_pulses, /* I max value for sum of pulses */ + opus_int len /* I number of output values */ +) +{ + opus_int k, sum; + + for( k = 0; k < len; k++ ) { + sum = pulses_in[ 2 * k ] + pulses_in[ 2 * k + 1 ]; + if( sum > max_pulses ) { + return 1; + } + pulses_comb[ k ] = sum; + } + + return 0; +} + +/* Encode quantization indices of excitation */ +void silk_encode_pulses( + ec_enc *psRangeEnc, /* I/O compressor data structure */ + const opus_int signalType, /* I Signal type */ + const opus_int quantOffsetType, /* I quantOffsetType */ + opus_int8 pulses[], /* I quantization indices */ + const opus_int frame_length /* I Frame length */ +) +{ + opus_int i, k, j, iter, bit, nLS, scale_down, RateLevelIndex = 0; + opus_int32 abs_q, minSumBits_Q5, sumBits_Q5; + VARDECL( opus_int, abs_pulses ); + VARDECL( opus_int, sum_pulses ); + VARDECL( opus_int, nRshifts ); + opus_int pulses_comb[ 8 ]; + opus_int *abs_pulses_ptr; + const opus_int8 *pulses_ptr; + const opus_uint8 *cdf_ptr; + const opus_uint8 *nBits_ptr; + SAVE_STACK; + + silk_memset( pulses_comb, 0, 8 * sizeof( opus_int ) ); /* Fixing Valgrind reported problem*/ + + /****************************/ + /* Prepare for shell coding */ + /****************************/ + /* Calculate number of shell blocks */ + silk_assert( 1 << LOG2_SHELL_CODEC_FRAME_LENGTH == SHELL_CODEC_FRAME_LENGTH ); + iter = silk_RSHIFT( frame_length, LOG2_SHELL_CODEC_FRAME_LENGTH ); + if( iter * SHELL_CODEC_FRAME_LENGTH < frame_length ) { + celt_assert( frame_length == 12 * 10 ); /* Make sure only happens for 10 ms @ 12 kHz */ + iter++; + silk_memset( &pulses[ frame_length ], 0, SHELL_CODEC_FRAME_LENGTH * sizeof(opus_int8)); + } + + /* Take the absolute value of the pulses */ + ALLOC( abs_pulses, iter * SHELL_CODEC_FRAME_LENGTH, opus_int ); + silk_assert( !( SHELL_CODEC_FRAME_LENGTH & 3 ) ); + for( i = 0; i < iter * SHELL_CODEC_FRAME_LENGTH; i+=4 ) { + abs_pulses[i+0] = ( opus_int )silk_abs( pulses[ i + 0 ] ); + abs_pulses[i+1] = ( opus_int )silk_abs( pulses[ i + 1 ] ); + abs_pulses[i+2] = ( opus_int )silk_abs( pulses[ i + 2 ] ); + abs_pulses[i+3] = ( opus_int )silk_abs( pulses[ i + 3 ] ); + } + + /* Calc sum pulses per shell code frame */ + ALLOC( sum_pulses, iter, opus_int ); + ALLOC( nRshifts, iter, opus_int ); + abs_pulses_ptr = abs_pulses; + for( i = 0; i < iter; i++ ) { + nRshifts[ i ] = 0; + + while( 1 ) { + /* 1+1 -> 2 */ + scale_down = combine_and_check( pulses_comb, abs_pulses_ptr, silk_max_pulses_table[ 0 ], 8 ); + /* 2+2 -> 4 */ + scale_down += combine_and_check( pulses_comb, pulses_comb, silk_max_pulses_table[ 1 ], 4 ); + /* 4+4 -> 8 */ + scale_down += combine_and_check( pulses_comb, pulses_comb, silk_max_pulses_table[ 2 ], 2 ); + /* 8+8 -> 16 */ + scale_down += combine_and_check( &sum_pulses[ i ], pulses_comb, silk_max_pulses_table[ 3 ], 1 ); + + if( scale_down ) { + /* We need to downscale the quantization signal */ + nRshifts[ i ]++; + for( k = 0; k < SHELL_CODEC_FRAME_LENGTH; k++ ) { + abs_pulses_ptr[ k ] = silk_RSHIFT( abs_pulses_ptr[ k ], 1 ); + } + } else { + /* Jump out of while(1) loop and go to next shell coding frame */ + break; + } + } + abs_pulses_ptr += SHELL_CODEC_FRAME_LENGTH; + } + + /**************/ + /* Rate level */ + /**************/ + /* find rate level that leads to fewest bits for coding of pulses per block info */ + minSumBits_Q5 = silk_int32_MAX; + for( k = 0; k < N_RATE_LEVELS - 1; k++ ) { + nBits_ptr = silk_pulses_per_block_BITS_Q5[ k ]; + sumBits_Q5 = silk_rate_levels_BITS_Q5[ signalType >> 1 ][ k ]; + for( i = 0; i < iter; i++ ) { + if( nRshifts[ i ] > 0 ) { + sumBits_Q5 += nBits_ptr[ SILK_MAX_PULSES + 1 ]; + } else { + sumBits_Q5 += nBits_ptr[ sum_pulses[ i ] ]; + } + } + if( sumBits_Q5 < minSumBits_Q5 ) { + minSumBits_Q5 = sumBits_Q5; + RateLevelIndex = k; + } + } + ec_enc_icdf( psRangeEnc, RateLevelIndex, silk_rate_levels_iCDF[ signalType >> 1 ], 8 ); + + /***************************************************/ + /* Sum-Weighted-Pulses Encoding */ + /***************************************************/ + cdf_ptr = silk_pulses_per_block_iCDF[ RateLevelIndex ]; + for( i = 0; i < iter; i++ ) { + if( nRshifts[ i ] == 0 ) { + ec_enc_icdf( psRangeEnc, sum_pulses[ i ], cdf_ptr, 8 ); + } else { + ec_enc_icdf( psRangeEnc, SILK_MAX_PULSES + 1, cdf_ptr, 8 ); + for( k = 0; k < nRshifts[ i ] - 1; k++ ) { + ec_enc_icdf( psRangeEnc, SILK_MAX_PULSES + 1, silk_pulses_per_block_iCDF[ N_RATE_LEVELS - 1 ], 8 ); + } + ec_enc_icdf( psRangeEnc, sum_pulses[ i ], silk_pulses_per_block_iCDF[ N_RATE_LEVELS - 1 ], 8 ); + } + } + + /******************/ + /* Shell Encoding */ + /******************/ + for( i = 0; i < iter; i++ ) { + if( sum_pulses[ i ] > 0 ) { + silk_shell_encoder( psRangeEnc, &abs_pulses[ i * SHELL_CODEC_FRAME_LENGTH ] ); + } + } + + /****************/ + /* LSB Encoding */ + /****************/ + for( i = 0; i < iter; i++ ) { + if( nRshifts[ i ] > 0 ) { + pulses_ptr = &pulses[ i * SHELL_CODEC_FRAME_LENGTH ]; + nLS = nRshifts[ i ] - 1; + for( k = 0; k < SHELL_CODEC_FRAME_LENGTH; k++ ) { + abs_q = (opus_int8)silk_abs( pulses_ptr[ k ] ); + for( j = nLS; j > 0; j-- ) { + bit = silk_RSHIFT( abs_q, j ) & 1; + ec_enc_icdf( psRangeEnc, bit, silk_lsb_iCDF, 8 ); + } + bit = abs_q & 1; + ec_enc_icdf( psRangeEnc, bit, silk_lsb_iCDF, 8 ); + } + } + } + + /****************/ + /* Encode signs */ + /****************/ + silk_encode_signs( psRangeEnc, pulses, frame_length, signalType, quantOffsetType, sum_pulses ); + RESTORE_STACK; +} diff --git a/port/third_party/opus/silk/errors.h b/port/third_party/opus/silk/errors.h new file mode 100644 index 00000000..45070800 --- /dev/null +++ b/port/third_party/opus/silk/errors.h @@ -0,0 +1,98 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_ERRORS_H +#define SILK_ERRORS_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +/******************/ +/* Error messages */ +/******************/ +#define SILK_NO_ERROR 0 + +/**************************/ +/* Encoder error messages */ +/**************************/ + +/* Input length is not a multiple of 10 ms, or length is longer than the packet length */ +#define SILK_ENC_INPUT_INVALID_NO_OF_SAMPLES -101 + +/* Sampling frequency not 8000, 12000 or 16000 Hertz */ +#define SILK_ENC_FS_NOT_SUPPORTED -102 + +/* Packet size not 10, 20, 40, or 60 ms */ +#define SILK_ENC_PACKET_SIZE_NOT_SUPPORTED -103 + +/* Allocated payload buffer too short */ +#define SILK_ENC_PAYLOAD_BUF_TOO_SHORT -104 + +/* Loss rate not between 0 and 100 percent */ +#define SILK_ENC_INVALID_LOSS_RATE -105 + +/* Complexity setting not valid, use 0...10 */ +#define SILK_ENC_INVALID_COMPLEXITY_SETTING -106 + +/* Inband FEC setting not valid, use 0 or 1 */ +#define SILK_ENC_INVALID_INBAND_FEC_SETTING -107 + +/* DTX setting not valid, use 0 or 1 */ +#define SILK_ENC_INVALID_DTX_SETTING -108 + +/* CBR setting not valid, use 0 or 1 */ +#define SILK_ENC_INVALID_CBR_SETTING -109 + +/* Internal encoder error */ +#define SILK_ENC_INTERNAL_ERROR -110 + +/* Internal encoder error */ +#define SILK_ENC_INVALID_NUMBER_OF_CHANNELS_ERROR -111 + +/**************************/ +/* Decoder error messages */ +/**************************/ + +/* Output sampling frequency lower than internal decoded sampling frequency */ +#define SILK_DEC_INVALID_SAMPLING_FREQUENCY -200 + +/* Payload size exceeded the maximum allowed 1024 bytes */ +#define SILK_DEC_PAYLOAD_TOO_LARGE -201 + +/* Payload has bit errors */ +#define SILK_DEC_PAYLOAD_ERROR -202 + +/* Payload has bit errors */ +#define SILK_DEC_INVALID_FRAME_SIZE -203 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/port/third_party/opus/silk/float/LPC_analysis_filter_FLP.c b/port/third_party/opus/silk/float/LPC_analysis_filter_FLP.c new file mode 100644 index 00000000..0e1a1fed --- /dev/null +++ b/port/third_party/opus/silk/float/LPC_analysis_filter_FLP.c @@ -0,0 +1,249 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "main_FLP.h" + +/************************************************/ +/* LPC analysis filter */ +/* NB! State is kept internally and the */ +/* filter always starts with zero state */ +/* first Order output samples are set to zero */ +/************************************************/ + +/* 16th order LPC analysis filter, does not write first 16 samples */ +static OPUS_INLINE void silk_LPC_analysis_filter16_FLP( + silk_float r_LPC[], /* O LPC residual signal */ + const silk_float PredCoef[], /* I LPC coefficients */ + const silk_float s[], /* I Input signal */ + const opus_int length /* I Length of input signal */ +) +{ + opus_int ix; + silk_float LPC_pred; + const silk_float *s_ptr; + + for( ix = 16; ix < length; ix++ ) { + s_ptr = &s[ix - 1]; + + /* short-term prediction */ + LPC_pred = s_ptr[ 0 ] * PredCoef[ 0 ] + + s_ptr[ -1 ] * PredCoef[ 1 ] + + s_ptr[ -2 ] * PredCoef[ 2 ] + + s_ptr[ -3 ] * PredCoef[ 3 ] + + s_ptr[ -4 ] * PredCoef[ 4 ] + + s_ptr[ -5 ] * PredCoef[ 5 ] + + s_ptr[ -6 ] * PredCoef[ 6 ] + + s_ptr[ -7 ] * PredCoef[ 7 ] + + s_ptr[ -8 ] * PredCoef[ 8 ] + + s_ptr[ -9 ] * PredCoef[ 9 ] + + s_ptr[ -10 ] * PredCoef[ 10 ] + + s_ptr[ -11 ] * PredCoef[ 11 ] + + s_ptr[ -12 ] * PredCoef[ 12 ] + + s_ptr[ -13 ] * PredCoef[ 13 ] + + s_ptr[ -14 ] * PredCoef[ 14 ] + + s_ptr[ -15 ] * PredCoef[ 15 ]; + + /* prediction error */ + r_LPC[ix] = s_ptr[ 1 ] - LPC_pred; + } +} + +/* 12th order LPC analysis filter, does not write first 12 samples */ +static OPUS_INLINE void silk_LPC_analysis_filter12_FLP( + silk_float r_LPC[], /* O LPC residual signal */ + const silk_float PredCoef[], /* I LPC coefficients */ + const silk_float s[], /* I Input signal */ + const opus_int length /* I Length of input signal */ +) +{ + opus_int ix; + silk_float LPC_pred; + const silk_float *s_ptr; + + for( ix = 12; ix < length; ix++ ) { + s_ptr = &s[ix - 1]; + + /* short-term prediction */ + LPC_pred = s_ptr[ 0 ] * PredCoef[ 0 ] + + s_ptr[ -1 ] * PredCoef[ 1 ] + + s_ptr[ -2 ] * PredCoef[ 2 ] + + s_ptr[ -3 ] * PredCoef[ 3 ] + + s_ptr[ -4 ] * PredCoef[ 4 ] + + s_ptr[ -5 ] * PredCoef[ 5 ] + + s_ptr[ -6 ] * PredCoef[ 6 ] + + s_ptr[ -7 ] * PredCoef[ 7 ] + + s_ptr[ -8 ] * PredCoef[ 8 ] + + s_ptr[ -9 ] * PredCoef[ 9 ] + + s_ptr[ -10 ] * PredCoef[ 10 ] + + s_ptr[ -11 ] * PredCoef[ 11 ]; + + /* prediction error */ + r_LPC[ix] = s_ptr[ 1 ] - LPC_pred; + } +} + +/* 10th order LPC analysis filter, does not write first 10 samples */ +static OPUS_INLINE void silk_LPC_analysis_filter10_FLP( + silk_float r_LPC[], /* O LPC residual signal */ + const silk_float PredCoef[], /* I LPC coefficients */ + const silk_float s[], /* I Input signal */ + const opus_int length /* I Length of input signal */ +) +{ + opus_int ix; + silk_float LPC_pred; + const silk_float *s_ptr; + + for( ix = 10; ix < length; ix++ ) { + s_ptr = &s[ix - 1]; + + /* short-term prediction */ + LPC_pred = s_ptr[ 0 ] * PredCoef[ 0 ] + + s_ptr[ -1 ] * PredCoef[ 1 ] + + s_ptr[ -2 ] * PredCoef[ 2 ] + + s_ptr[ -3 ] * PredCoef[ 3 ] + + s_ptr[ -4 ] * PredCoef[ 4 ] + + s_ptr[ -5 ] * PredCoef[ 5 ] + + s_ptr[ -6 ] * PredCoef[ 6 ] + + s_ptr[ -7 ] * PredCoef[ 7 ] + + s_ptr[ -8 ] * PredCoef[ 8 ] + + s_ptr[ -9 ] * PredCoef[ 9 ]; + + /* prediction error */ + r_LPC[ix] = s_ptr[ 1 ] - LPC_pred; + } +} + +/* 8th order LPC analysis filter, does not write first 8 samples */ +static OPUS_INLINE void silk_LPC_analysis_filter8_FLP( + silk_float r_LPC[], /* O LPC residual signal */ + const silk_float PredCoef[], /* I LPC coefficients */ + const silk_float s[], /* I Input signal */ + const opus_int length /* I Length of input signal */ +) +{ + opus_int ix; + silk_float LPC_pred; + const silk_float *s_ptr; + + for( ix = 8; ix < length; ix++ ) { + s_ptr = &s[ix - 1]; + + /* short-term prediction */ + LPC_pred = s_ptr[ 0 ] * PredCoef[ 0 ] + + s_ptr[ -1 ] * PredCoef[ 1 ] + + s_ptr[ -2 ] * PredCoef[ 2 ] + + s_ptr[ -3 ] * PredCoef[ 3 ] + + s_ptr[ -4 ] * PredCoef[ 4 ] + + s_ptr[ -5 ] * PredCoef[ 5 ] + + s_ptr[ -6 ] * PredCoef[ 6 ] + + s_ptr[ -7 ] * PredCoef[ 7 ]; + + /* prediction error */ + r_LPC[ix] = s_ptr[ 1 ] - LPC_pred; + } +} + +/* 6th order LPC analysis filter, does not write first 6 samples */ +static OPUS_INLINE void silk_LPC_analysis_filter6_FLP( + silk_float r_LPC[], /* O LPC residual signal */ + const silk_float PredCoef[], /* I LPC coefficients */ + const silk_float s[], /* I Input signal */ + const opus_int length /* I Length of input signal */ +) +{ + opus_int ix; + silk_float LPC_pred; + const silk_float *s_ptr; + + for( ix = 6; ix < length; ix++ ) { + s_ptr = &s[ix - 1]; + + /* short-term prediction */ + LPC_pred = s_ptr[ 0 ] * PredCoef[ 0 ] + + s_ptr[ -1 ] * PredCoef[ 1 ] + + s_ptr[ -2 ] * PredCoef[ 2 ] + + s_ptr[ -3 ] * PredCoef[ 3 ] + + s_ptr[ -4 ] * PredCoef[ 4 ] + + s_ptr[ -5 ] * PredCoef[ 5 ]; + + /* prediction error */ + r_LPC[ix] = s_ptr[ 1 ] - LPC_pred; + } +} + +/************************************************/ +/* LPC analysis filter */ +/* NB! State is kept internally and the */ +/* filter always starts with zero state */ +/* first Order output samples are set to zero */ +/************************************************/ +void silk_LPC_analysis_filter_FLP( + silk_float r_LPC[], /* O LPC residual signal */ + const silk_float PredCoef[], /* I LPC coefficients */ + const silk_float s[], /* I Input signal */ + const opus_int length, /* I Length of input signal */ + const opus_int Order /* I LPC order */ +) +{ + celt_assert( Order <= length ); + + switch( Order ) { + case 6: + silk_LPC_analysis_filter6_FLP( r_LPC, PredCoef, s, length ); + break; + + case 8: + silk_LPC_analysis_filter8_FLP( r_LPC, PredCoef, s, length ); + break; + + case 10: + silk_LPC_analysis_filter10_FLP( r_LPC, PredCoef, s, length ); + break; + + case 12: + silk_LPC_analysis_filter12_FLP( r_LPC, PredCoef, s, length ); + break; + + case 16: + silk_LPC_analysis_filter16_FLP( r_LPC, PredCoef, s, length ); + break; + + default: + celt_assert( 0 ); + break; + } + + /* Set first Order output samples to zero */ + silk_memset( r_LPC, 0, Order * sizeof( silk_float ) ); +} + diff --git a/port/third_party/opus/silk/float/LPC_inv_pred_gain_FLP.c b/port/third_party/opus/silk/float/LPC_inv_pred_gain_FLP.c new file mode 100644 index 00000000..2be2122d --- /dev/null +++ b/port/third_party/opus/silk/float/LPC_inv_pred_gain_FLP.c @@ -0,0 +1,73 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "SigProc_FLP.h" +#include "define.h" + +/* compute inverse of LPC prediction gain, and */ +/* test if LPC coefficients are stable (all poles within unit circle) */ +/* this code is based on silk_a2k_FLP() */ +silk_float silk_LPC_inverse_pred_gain_FLP( /* O return inverse prediction gain, energy domain */ + const silk_float *A, /* I prediction coefficients [order] */ + opus_int32 order /* I prediction order */ +) +{ + opus_int k, n; + double invGain, rc, rc_mult1, rc_mult2, tmp1, tmp2; + silk_float Atmp[ SILK_MAX_ORDER_LPC ]; + + silk_memcpy( Atmp, A, order * sizeof(silk_float) ); + + invGain = 1.0; + for( k = order - 1; k > 0; k-- ) { + rc = -Atmp[ k ]; + rc_mult1 = 1.0f - rc * rc; + invGain *= rc_mult1; + if( invGain * MAX_PREDICTION_POWER_GAIN < 1.0f ) { + return 0.0f; + } + rc_mult2 = 1.0f / rc_mult1; + for( n = 0; n < (k + 1) >> 1; n++ ) { + tmp1 = Atmp[ n ]; + tmp2 = Atmp[ k - n - 1 ]; + Atmp[ n ] = (silk_float)( ( tmp1 - tmp2 * rc ) * rc_mult2 ); + Atmp[ k - n - 1 ] = (silk_float)( ( tmp2 - tmp1 * rc ) * rc_mult2 ); + } + } + rc = -Atmp[ 0 ]; + rc_mult1 = 1.0f - rc * rc; + invGain *= rc_mult1; + if( invGain * MAX_PREDICTION_POWER_GAIN < 1.0f ) { + return 0.0f; + } + return (silk_float)invGain; +} diff --git a/port/third_party/opus/silk/float/LTP_analysis_filter_FLP.c b/port/third_party/opus/silk/float/LTP_analysis_filter_FLP.c new file mode 100644 index 00000000..849b7c1c --- /dev/null +++ b/port/third_party/opus/silk/float/LTP_analysis_filter_FLP.c @@ -0,0 +1,75 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" + +void silk_LTP_analysis_filter_FLP( + silk_float *LTP_res, /* O LTP res MAX_NB_SUBFR*(pre_lgth+subfr_lngth) */ + const silk_float *x, /* I Input signal, with preceding samples */ + const silk_float B[ LTP_ORDER * MAX_NB_SUBFR ], /* I LTP coefficients for each subframe */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lags */ + const silk_float invGains[ MAX_NB_SUBFR ], /* I Inverse quantization gains */ + const opus_int subfr_length, /* I Length of each subframe */ + const opus_int nb_subfr, /* I number of subframes */ + const opus_int pre_length /* I Preceding samples for each subframe */ +) +{ + const silk_float *x_ptr, *x_lag_ptr; + silk_float Btmp[ LTP_ORDER ]; + silk_float *LTP_res_ptr; + silk_float inv_gain; + opus_int k, i, j; + + x_ptr = x; + LTP_res_ptr = LTP_res; + for( k = 0; k < nb_subfr; k++ ) { + x_lag_ptr = x_ptr - pitchL[ k ]; + inv_gain = invGains[ k ]; + for( i = 0; i < LTP_ORDER; i++ ) { + Btmp[ i ] = B[ k * LTP_ORDER + i ]; + } + + /* LTP analysis FIR filter */ + for( i = 0; i < subfr_length + pre_length; i++ ) { + LTP_res_ptr[ i ] = x_ptr[ i ]; + /* Subtract long-term prediction */ + for( j = 0; j < LTP_ORDER; j++ ) { + LTP_res_ptr[ i ] -= Btmp[ j ] * x_lag_ptr[ LTP_ORDER / 2 - j ]; + } + LTP_res_ptr[ i ] *= inv_gain; + x_lag_ptr++; + } + + /* Update pointers */ + LTP_res_ptr += subfr_length + pre_length; + x_ptr += subfr_length; + } +} diff --git a/port/third_party/opus/silk/float/LTP_scale_ctrl_FLP.c b/port/third_party/opus/silk/float/LTP_scale_ctrl_FLP.c new file mode 100644 index 00000000..6f30ff09 --- /dev/null +++ b/port/third_party/opus/silk/float/LTP_scale_ctrl_FLP.c @@ -0,0 +1,58 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" + +void silk_LTP_scale_ctrl_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + opus_int condCoding /* I The type of conditional coding to use */ +) +{ + opus_int round_loss; + + if( condCoding == CODE_INDEPENDENTLY ) { + /* Only scale if first frame in packet */ + round_loss = psEnc->sCmn.PacketLoss_perc * psEnc->sCmn.nFramesPerPacket; + if ( psEnc->sCmn.LBRR_flag ) { + /* LBRR reduces the effective loss. In practice, it does not square the loss because + losses aren't independent, but that still seems to work best. We also never go below 2%. */ + round_loss = 2 + silk_SMULBB( round_loss, round_loss) / 100; + } + psEnc->sCmn.indices.LTP_scaleIndex = silk_SMULBB( psEncCtrl->LTPredCodGain, round_loss ) > silk_log2lin( 2900 - psEnc->sCmn.SNR_dB_Q7 ); + psEnc->sCmn.indices.LTP_scaleIndex += silk_SMULBB( psEncCtrl->LTPredCodGain, round_loss ) > silk_log2lin( 3900 - psEnc->sCmn.SNR_dB_Q7 ); + } else { + /* Default is minimum scaling */ + psEnc->sCmn.indices.LTP_scaleIndex = 0; + } + + psEncCtrl->LTP_scale = (silk_float)silk_LTPScales_table_Q14[ psEnc->sCmn.indices.LTP_scaleIndex ] / 16384.0f; +} diff --git a/port/third_party/opus/silk/float/SigProc_FLP.h b/port/third_party/opus/silk/float/SigProc_FLP.h new file mode 100644 index 00000000..ff9281b8 --- /dev/null +++ b/port/third_party/opus/silk/float/SigProc_FLP.h @@ -0,0 +1,205 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_SIGPROC_FLP_H +#define SILK_SIGPROC_FLP_H + +#include "SigProc_FIX.h" +#include "float_cast.h" +#include "main.h" +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/********************************************************************/ +/* SIGNAL PROCESSING FUNCTIONS */ +/********************************************************************/ + +/* Chirp (bw expand) LP AR filter */ +void silk_bwexpander_FLP( + silk_float *ar, /* I/O AR filter to be expanded (without leading 1) */ + const opus_int d, /* I length of ar */ + const silk_float chirp /* I chirp factor (typically in range (0..1) ) */ +); + +/* compute inverse of LPC prediction gain, and */ +/* test if LPC coefficients are stable (all poles within unit circle) */ +/* this code is based on silk_FLP_a2k() */ +silk_float silk_LPC_inverse_pred_gain_FLP( /* O return inverse prediction gain, energy domain */ + const silk_float *A, /* I prediction coefficients [order] */ + opus_int32 order /* I prediction order */ +); + +silk_float silk_schur_FLP( /* O returns residual energy */ + silk_float refl_coef[], /* O reflection coefficients (length order) */ + const silk_float auto_corr[], /* I autocorrelation sequence (length order+1) */ + opus_int order /* I order */ +); + +void silk_k2a_FLP( + silk_float *A, /* O prediction coefficients [order] */ + const silk_float *rc, /* I reflection coefficients [order] */ + opus_int32 order /* I prediction order */ +); + +/* compute autocorrelation */ +void silk_autocorrelation_FLP( + silk_float *results, /* O result (length correlationCount) */ + const silk_float *inputData, /* I input data to correlate */ + opus_int inputDataSize, /* I length of input */ + opus_int correlationCount, /* I number of correlation taps to compute */ + int arch +); + +opus_int silk_pitch_analysis_core_FLP( /* O Voicing estimate: 0 voiced, 1 unvoiced */ + const silk_float *frame, /* I Signal of length PE_FRAME_LENGTH_MS*Fs_kHz */ + opus_int *pitch_out, /* O Pitch lag values [nb_subfr] */ + opus_int16 *lagIndex, /* O Lag Index */ + opus_int8 *contourIndex, /* O Pitch contour Index */ + silk_float *LTPCorr, /* I/O Normalized correlation; input: value from previous frame */ + opus_int prevLag, /* I Last lag of previous frame; set to zero is unvoiced */ + const silk_float search_thres1, /* I First stage threshold for lag candidates 0 - 1 */ + const silk_float search_thres2, /* I Final threshold for lag candidates 0 - 1 */ + const opus_int Fs_kHz, /* I sample frequency (kHz) */ + const opus_int complexity, /* I Complexity setting, 0-2, where 2 is highest */ + const opus_int nb_subfr, /* I Number of 5 ms subframes */ + int arch /* I Run-time architecture */ +); + +void silk_insertion_sort_decreasing_FLP( + silk_float *a, /* I/O Unsorted / Sorted vector */ + opus_int *idx, /* O Index vector for the sorted elements */ + const opus_int L, /* I Vector length */ + const opus_int K /* I Number of correctly sorted positions */ +); + +/* Compute reflection coefficients from input signal */ +silk_float silk_burg_modified_FLP( /* O returns residual energy */ + silk_float A[], /* O prediction coefficients (length order) */ + const silk_float x[], /* I input signal, length: nb_subfr*(D+L_sub) */ + const silk_float minInvGain, /* I minimum inverse prediction gain */ + const opus_int subfr_length, /* I input signal subframe length (incl. D preceding samples) */ + const opus_int nb_subfr, /* I number of subframes stacked in x */ + const opus_int D, /* I order */ + int arch +); + +/* multiply a vector by a constant */ +void silk_scale_vector_FLP( + silk_float *data1, + silk_float gain, + opus_int dataSize +); + +/* copy and multiply a vector by a constant */ +void silk_scale_copy_vector_FLP( + silk_float *data_out, + const silk_float *data_in, + silk_float gain, + opus_int dataSize +); + +/* inner product of two silk_float arrays, with result as double */ +double silk_inner_product_FLP_c( + const silk_float *data1, + const silk_float *data2, + opus_int dataSize +); + +#ifndef OVERRIDE_inner_product_FLP +#define silk_inner_product_FLP(data1, data2, dataSize, arch) ((void)arch,silk_inner_product_FLP_c(data1, data2, dataSize)) +#endif + + +/* sum of squares of a silk_float array, with result as double */ +double silk_energy_FLP( + const silk_float *data, + opus_int dataSize +); + +/********************************************************************/ +/* MACROS */ +/********************************************************************/ + +#define PI (3.1415926536f) + +#define silk_min_float( a, b ) (((a) < (b)) ? (a) : (b)) +#define silk_max_float( a, b ) (((a) > (b)) ? (a) : (b)) +#define silk_abs_float( a ) ((silk_float)fabs(a)) + +/* sigmoid function */ +static OPUS_INLINE silk_float silk_sigmoid( silk_float x ) +{ + return (silk_float)(1.0 / (1.0 + exp(-x))); +} + +/* floating-point to integer conversion (rounding) */ +static OPUS_INLINE opus_int32 silk_float2int( silk_float x ) +{ + return (opus_int32)float2int( x ); +} + +/* floating-point to integer conversion (rounding) */ +static OPUS_INLINE void silk_float2short_array( + opus_int16 *out, + const silk_float *in, + opus_int32 length +) +{ + opus_int32 k; + for( k = length - 1; k >= 0; k-- ) { + out[k] = silk_SAT16( (opus_int32)float2int( in[k] ) ); + } +} + +/* integer to floating-point conversion */ +static OPUS_INLINE void silk_short2float_array( + silk_float *out, + const opus_int16 *in, + opus_int32 length +) +{ + opus_int32 k; + for( k = length - 1; k >= 0; k-- ) { + out[k] = (silk_float)in[k]; + } +} + +/* using log2() helps the fixed-point conversion */ +static OPUS_INLINE silk_float silk_log2( double x ) +{ + return ( silk_float )( 3.32192809488736 * log10( x ) ); +} + +#ifdef __cplusplus +} +#endif + +#endif /* SILK_SIGPROC_FLP_H */ diff --git a/port/third_party/opus/silk/float/apply_sine_window_FLP.c b/port/third_party/opus/silk/float/apply_sine_window_FLP.c new file mode 100644 index 00000000..e49e7179 --- /dev/null +++ b/port/third_party/opus/silk/float/apply_sine_window_FLP.c @@ -0,0 +1,81 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" + +/* Apply sine window to signal vector */ +/* Window types: */ +/* 1 -> sine window from 0 to pi/2 */ +/* 2 -> sine window from pi/2 to pi */ +void silk_apply_sine_window_FLP( + silk_float px_win[], /* O Pointer to windowed signal */ + const silk_float px[], /* I Pointer to input signal */ + const opus_int win_type, /* I Selects a window type */ + const opus_int length /* I Window length, multiple of 4 */ +) +{ + opus_int k; + silk_float freq, c, S0, S1; + + celt_assert( win_type == 1 || win_type == 2 ); + + /* Length must be multiple of 4 */ + celt_assert( ( length & 3 ) == 0 ); + + freq = PI / ( length + 1 ); + + /* Approximation of 2 * cos(f) */ + c = 2.0f - freq * freq; + + /* Initialize state */ + if( win_type < 2 ) { + /* Start from 0 */ + S0 = 0.0f; + /* Approximation of sin(f) */ + S1 = freq; + } else { + /* Start from 1 */ + S0 = 1.0f; + /* Approximation of cos(f) */ + S1 = 0.5f * c; + } + + /* Uses the recursive equation: sin(n*f) = 2 * cos(f) * sin((n-1)*f) - sin((n-2)*f) */ + /* 4 samples at a time */ + for( k = 0; k < length; k += 4 ) { + px_win[ k + 0 ] = px[ k + 0 ] * 0.5f * ( S0 + S1 ); + px_win[ k + 1 ] = px[ k + 1 ] * S1; + S0 = c * S1 - S0; + px_win[ k + 2 ] = px[ k + 2 ] * 0.5f * ( S1 + S0 ); + px_win[ k + 3 ] = px[ k + 3 ] * S0; + S1 = c * S0 - S1; + } +} diff --git a/port/third_party/opus/silk/float/autocorrelation_FLP.c b/port/third_party/opus/silk/float/autocorrelation_FLP.c new file mode 100644 index 00000000..4253b26e --- /dev/null +++ b/port/third_party/opus/silk/float/autocorrelation_FLP.c @@ -0,0 +1,53 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "typedef.h" +#include "SigProc_FLP.h" + +/* compute autocorrelation */ +void silk_autocorrelation_FLP( + silk_float *results, /* O result (length correlationCount) */ + const silk_float *inputData, /* I input data to correlate */ + opus_int inputDataSize, /* I length of input */ + opus_int correlationCount, /* I number of correlation taps to compute */ + int arch +) +{ + opus_int i; + + if( correlationCount > inputDataSize ) { + correlationCount = inputDataSize; + } + + for( i = 0; i < correlationCount; i++ ) { + results[ i ] = (silk_float)silk_inner_product_FLP( inputData, inputData + i, inputDataSize - i, arch ); + } +} diff --git a/port/third_party/opus/silk/float/burg_modified_FLP.c b/port/third_party/opus/silk/float/burg_modified_FLP.c new file mode 100644 index 00000000..f5bef5dd --- /dev/null +++ b/port/third_party/opus/silk/float/burg_modified_FLP.c @@ -0,0 +1,187 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FLP.h" +#include "tuning_parameters.h" +#include "define.h" + +#define MAX_FRAME_SIZE 384 /* subfr_length * nb_subfr = ( 0.005 * 16000 + 16 ) * 4 = 384*/ + +/* Compute reflection coefficients from input signal */ +silk_float silk_burg_modified_FLP( /* O returns residual energy */ + silk_float A[], /* O prediction coefficients (length order) */ + const silk_float x[], /* I input signal, length: nb_subfr*(D+L_sub) */ + const silk_float minInvGain, /* I minimum inverse prediction gain */ + const opus_int subfr_length, /* I input signal subframe length (incl. D preceding samples) */ + const opus_int nb_subfr, /* I number of subframes stacked in x */ + const opus_int D, /* I order */ + int arch +) +{ + opus_int k, n, s, reached_max_gain; + double C0, invGain, num, nrg_f, nrg_b, rc, Atmp, tmp1, tmp2; + const silk_float *x_ptr; + double C_first_row[ SILK_MAX_ORDER_LPC ], C_last_row[ SILK_MAX_ORDER_LPC ]; + double CAf[ SILK_MAX_ORDER_LPC + 1 ], CAb[ SILK_MAX_ORDER_LPC + 1 ]; + double Af[ SILK_MAX_ORDER_LPC ]; + + celt_assert( subfr_length * nb_subfr <= MAX_FRAME_SIZE ); + + /* Compute autocorrelations, added over subframes */ + C0 = silk_energy_FLP( x, nb_subfr * subfr_length ); + silk_memset( C_first_row, 0, SILK_MAX_ORDER_LPC * sizeof( double ) ); + for( s = 0; s < nb_subfr; s++ ) { + x_ptr = x + s * subfr_length; + for( n = 1; n < D + 1; n++ ) { + C_first_row[ n - 1 ] += silk_inner_product_FLP( x_ptr, x_ptr + n, subfr_length - n, arch ); + } + } + silk_memcpy( C_last_row, C_first_row, SILK_MAX_ORDER_LPC * sizeof( double ) ); + + /* Initialize */ + CAb[ 0 ] = CAf[ 0 ] = C0 + FIND_LPC_COND_FAC * C0 + 1e-9f; + invGain = 1.0f; + reached_max_gain = 0; + for( n = 0; n < D; n++ ) { + /* Update first row of correlation matrix (without first element) */ + /* Update last row of correlation matrix (without last element, stored in reversed order) */ + /* Update C * Af */ + /* Update C * flipud(Af) (stored in reversed order) */ + for( s = 0; s < nb_subfr; s++ ) { + x_ptr = x + s * subfr_length; + tmp1 = x_ptr[ n ]; + tmp2 = x_ptr[ subfr_length - n - 1 ]; + for( k = 0; k < n; k++ ) { + C_first_row[ k ] -= x_ptr[ n ] * x_ptr[ n - k - 1 ]; + C_last_row[ k ] -= x_ptr[ subfr_length - n - 1 ] * x_ptr[ subfr_length - n + k ]; + Atmp = Af[ k ]; + tmp1 += x_ptr[ n - k - 1 ] * Atmp; + tmp2 += x_ptr[ subfr_length - n + k ] * Atmp; + } + for( k = 0; k <= n; k++ ) { + CAf[ k ] -= tmp1 * x_ptr[ n - k ]; + CAb[ k ] -= tmp2 * x_ptr[ subfr_length - n + k - 1 ]; + } + } + tmp1 = C_first_row[ n ]; + tmp2 = C_last_row[ n ]; + for( k = 0; k < n; k++ ) { + Atmp = Af[ k ]; + tmp1 += C_last_row[ n - k - 1 ] * Atmp; + tmp2 += C_first_row[ n - k - 1 ] * Atmp; + } + CAf[ n + 1 ] = tmp1; + CAb[ n + 1 ] = tmp2; + + /* Calculate nominator and denominator for the next order reflection (parcor) coefficient */ + num = CAb[ n + 1 ]; + nrg_b = CAb[ 0 ]; + nrg_f = CAf[ 0 ]; + for( k = 0; k < n; k++ ) { + Atmp = Af[ k ]; + num += CAb[ n - k ] * Atmp; + nrg_b += CAb[ k + 1 ] * Atmp; + nrg_f += CAf[ k + 1 ] * Atmp; + } + silk_assert( nrg_f > 0.0 ); + silk_assert( nrg_b > 0.0 ); + + /* Calculate the next order reflection (parcor) coefficient */ + rc = -2.0 * num / ( nrg_f + nrg_b ); + silk_assert( rc > -1.0 && rc < 1.0 ); + + /* Update inverse prediction gain */ + tmp1 = invGain * ( 1.0 - rc * rc ); + if( tmp1 <= minInvGain ) { + /* Max prediction gain exceeded; set reflection coefficient such that max prediction gain is exactly hit */ + rc = sqrt( 1.0 - minInvGain / invGain ); + if( num > 0 ) { + /* Ensure adjusted reflection coefficients has the original sign */ + rc = -rc; + } + invGain = minInvGain; + reached_max_gain = 1; + } else { + invGain = tmp1; + } + + /* Update the AR coefficients */ + for( k = 0; k < (n + 1) >> 1; k++ ) { + tmp1 = Af[ k ]; + tmp2 = Af[ n - k - 1 ]; + Af[ k ] = tmp1 + rc * tmp2; + Af[ n - k - 1 ] = tmp2 + rc * tmp1; + } + Af[ n ] = rc; + + if( reached_max_gain ) { + /* Reached max prediction gain; set remaining coefficients to zero and exit loop */ + for( k = n + 1; k < D; k++ ) { + Af[ k ] = 0.0; + } + break; + } + + /* Update C * Af and C * Ab */ + for( k = 0; k <= n + 1; k++ ) { + tmp1 = CAf[ k ]; + CAf[ k ] += rc * CAb[ n - k + 1 ]; + CAb[ n - k + 1 ] += rc * tmp1; + } + } + + if( reached_max_gain ) { + /* Convert to silk_float */ + for( k = 0; k < D; k++ ) { + A[ k ] = (silk_float)( -Af[ k ] ); + } + /* Subtract energy of preceding samples from C0 */ + for( s = 0; s < nb_subfr; s++ ) { + C0 -= silk_energy_FLP( x + s * subfr_length, D ); + } + /* Approximate residual energy */ + nrg_f = C0 * invGain; + } else { + /* Compute residual energy and store coefficients as silk_float */ + nrg_f = CAf[ 0 ]; + tmp1 = 1.0; + for( k = 0; k < D; k++ ) { + Atmp = Af[ k ]; + nrg_f += CAf[ k + 1 ] * Atmp; + tmp1 += Atmp * Atmp; + A[ k ] = (silk_float)(-Atmp); + } + nrg_f -= FIND_LPC_COND_FAC * C0 * tmp1; + } + + /* Return residual energy */ + return (silk_float)nrg_f; +} diff --git a/port/third_party/opus/silk/float/bwexpander_FLP.c b/port/third_party/opus/silk/float/bwexpander_FLP.c new file mode 100644 index 00000000..d55a4d79 --- /dev/null +++ b/port/third_party/opus/silk/float/bwexpander_FLP.c @@ -0,0 +1,49 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FLP.h" + +/* Chirp (bw expand) LP AR filter */ +void silk_bwexpander_FLP( + silk_float *ar, /* I/O AR filter to be expanded (without leading 1) */ + const opus_int d, /* I length of ar */ + const silk_float chirp /* I chirp factor (typically in range (0..1) ) */ +) +{ + opus_int i; + silk_float cfac = chirp; + + for( i = 0; i < d - 1; i++ ) { + ar[ i ] *= cfac; + cfac *= chirp; + } + ar[ d - 1 ] *= cfac; +} diff --git a/port/third_party/opus/silk/float/corrMatrix_FLP.c b/port/third_party/opus/silk/float/corrMatrix_FLP.c new file mode 100644 index 00000000..eef6e8aa --- /dev/null +++ b/port/third_party/opus/silk/float/corrMatrix_FLP.c @@ -0,0 +1,95 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/********************************************************************** + * Correlation matrix computations for LS estimate. + **********************************************************************/ + +#include "main_FLP.h" + +/* Calculates correlation vector X'*t */ +void silk_corrVector_FLP( + const silk_float *x, /* I x vector [L+order-1] used to create X */ + const silk_float *t, /* I Target vector [L] */ + const opus_int L, /* I Length of vecors */ + const opus_int Order, /* I Max lag for correlation */ + silk_float *Xt, /* O X'*t correlation vector [order] */ + int arch +) +{ + opus_int lag; + const silk_float *ptr1; + + ptr1 = &x[ Order - 1 ]; /* Points to first sample of column 0 of X: X[:,0] */ + for( lag = 0; lag < Order; lag++ ) { + /* Calculate X[:,lag]'*t */ + Xt[ lag ] = (silk_float)silk_inner_product_FLP( ptr1, t, L, arch ); + ptr1--; /* Next column of X */ + } +} + +/* Calculates correlation matrix X'*X */ +void silk_corrMatrix_FLP( + const silk_float *x, /* I x vector [ L+order-1 ] used to create X */ + const opus_int L, /* I Length of vectors */ + const opus_int Order, /* I Max lag for correlation */ + silk_float *XX, /* O X'*X correlation matrix [order x order] */ + int arch +) +{ + opus_int j, lag; + double energy; + const silk_float *ptr1, *ptr2; + + ptr1 = &x[ Order - 1 ]; /* First sample of column 0 of X */ + energy = silk_energy_FLP( ptr1, L ); /* X[:,0]'*X[:,0] */ + matrix_ptr( XX, 0, 0, Order ) = ( silk_float )energy; + for( j = 1; j < Order; j++ ) { + /* Calculate X[:,j]'*X[:,j] */ + energy += ptr1[ -j ] * ptr1[ -j ] - ptr1[ L - j ] * ptr1[ L - j ]; + matrix_ptr( XX, j, j, Order ) = ( silk_float )energy; + } + + ptr2 = &x[ Order - 2 ]; /* First sample of column 1 of X */ + for( lag = 1; lag < Order; lag++ ) { + /* Calculate X[:,0]'*X[:,lag] */ + energy = silk_inner_product_FLP( ptr1, ptr2, L, arch ); + matrix_ptr( XX, lag, 0, Order ) = ( silk_float )energy; + matrix_ptr( XX, 0, lag, Order ) = ( silk_float )energy; + /* Calculate X[:,j]'*X[:,j + lag] */ + for( j = 1; j < ( Order - lag ); j++ ) { + energy += ptr1[ -j ] * ptr2[ -j ] - ptr1[ L - j ] * ptr2[ L - j ]; + matrix_ptr( XX, lag + j, j, Order ) = ( silk_float )energy; + matrix_ptr( XX, j, lag + j, Order ) = ( silk_float )energy; + } + ptr2--; /* Next column of X */ + } +} diff --git a/port/third_party/opus/silk/float/encode_frame_FLP.c b/port/third_party/opus/silk/float/encode_frame_FLP.c new file mode 100644 index 00000000..8a327c56 --- /dev/null +++ b/port/third_party/opus/silk/float/encode_frame_FLP.c @@ -0,0 +1,432 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "main_FLP.h" +#include "tuning_parameters.h" + +/* Low Bitrate Redundancy (LBRR) encoding. Reuse all parameters but encode with lower bitrate */ +static OPUS_INLINE void silk_LBRR_encode_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + const silk_float xfw[], /* I Input signal */ + opus_int condCoding /* I The type of conditional coding used so far for this frame */ +); + +void silk_encode_do_VAD_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + opus_int activity /* I Decision of Opus voice activity detector */ +) +{ + const opus_int activity_threshold = SILK_FIX_CONST( SPEECH_ACTIVITY_DTX_THRES, 8 ); + + /****************************/ + /* Voice Activity Detection */ + /****************************/ + silk_VAD_GetSA_Q8( &psEnc->sCmn, psEnc->sCmn.inputBuf + 1, psEnc->sCmn.arch ); + /* If Opus VAD is inactive and Silk VAD is active: lower Silk VAD to just under the threshold */ + if( activity == VAD_NO_ACTIVITY && psEnc->sCmn.speech_activity_Q8 >= activity_threshold ) { + psEnc->sCmn.speech_activity_Q8 = activity_threshold - 1; + } + + /**************************************************/ + /* Convert speech activity into VAD and DTX flags */ + /**************************************************/ + if( psEnc->sCmn.speech_activity_Q8 < activity_threshold ) { + psEnc->sCmn.indices.signalType = TYPE_NO_VOICE_ACTIVITY; + psEnc->sCmn.noSpeechCounter++; + if( psEnc->sCmn.noSpeechCounter <= NB_SPEECH_FRAMES_BEFORE_DTX ) { + psEnc->sCmn.inDTX = 0; + } else if( psEnc->sCmn.noSpeechCounter > MAX_CONSECUTIVE_DTX + NB_SPEECH_FRAMES_BEFORE_DTX ) { + psEnc->sCmn.noSpeechCounter = NB_SPEECH_FRAMES_BEFORE_DTX; + psEnc->sCmn.inDTX = 0; + } + psEnc->sCmn.VAD_flags[ psEnc->sCmn.nFramesEncoded ] = 0; + } else { + psEnc->sCmn.noSpeechCounter = 0; + psEnc->sCmn.inDTX = 0; + psEnc->sCmn.indices.signalType = TYPE_UNVOICED; + psEnc->sCmn.VAD_flags[ psEnc->sCmn.nFramesEncoded ] = 1; + } +} + +/****************/ +/* Encode frame */ +/****************/ +opus_int silk_encode_frame_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + opus_int32 *pnBytesOut, /* O Number of payload bytes; */ + ec_enc *psRangeEnc, /* I/O compressor data structure */ + opus_int condCoding, /* I The type of conditional coding to use */ + opus_int maxBits, /* I If > 0: maximum number of output bits */ + opus_int useCBR /* I Flag to force constant-bitrate operation */ +) +{ + silk_encoder_control_FLP sEncCtrl; + opus_int i, iter, maxIter, found_upper, found_lower, ret = 0; + silk_float *x_frame, *res_pitch_frame; + silk_float res_pitch[ 2 * MAX_FRAME_LENGTH + LA_PITCH_MAX ]; + ec_enc sRangeEnc_copy, sRangeEnc_copy2; + silk_nsq_state sNSQ_copy, sNSQ_copy2; + opus_int32 seed_copy, nBits, nBits_lower, nBits_upper, gainMult_lower, gainMult_upper; + opus_int32 gainsID, gainsID_lower, gainsID_upper; + opus_int16 gainMult_Q8; + opus_int16 ec_prevLagIndex_copy; + opus_int ec_prevSignalType_copy; + opus_int8 LastGainIndex_copy2; + opus_int32 pGains_Q16[ MAX_NB_SUBFR ]; + opus_uint8 ec_buf_copy[ 1275 ]; + opus_int gain_lock[ MAX_NB_SUBFR ] = {0}; + opus_int16 best_gain_mult[ MAX_NB_SUBFR ]; + opus_int best_sum[ MAX_NB_SUBFR ]; + opus_int bits_margin; + + /* For CBR, 5 bits below budget is close enough. For VBR, allow up to 25% below the cap if we initially busted the budget. */ + bits_margin = useCBR ? 5 : maxBits/4; + /* This is totally unnecessary but many compilers (including gcc) are too dumb to realise it */ + LastGainIndex_copy2 = nBits_lower = nBits_upper = gainMult_lower = gainMult_upper = 0; + + psEnc->sCmn.indices.Seed = psEnc->sCmn.frameCounter++ & 3; + + /**************************************************************/ + /* Set up Input Pointers, and insert frame in input buffer */ + /**************************************************************/ + /* pointers aligned with start of frame to encode */ + x_frame = psEnc->x_buf + psEnc->sCmn.ltp_mem_length; /* start of frame to encode */ + res_pitch_frame = res_pitch + psEnc->sCmn.ltp_mem_length; /* start of pitch LPC residual frame */ + + /***************************************/ + /* Ensure smooth bandwidth transitions */ + /***************************************/ + silk_LP_variable_cutoff( &psEnc->sCmn.sLP, psEnc->sCmn.inputBuf + 1, psEnc->sCmn.frame_length ); + + /*******************************************/ + /* Copy new frame to front of input buffer */ + /*******************************************/ + silk_short2float_array( x_frame + LA_SHAPE_MS * psEnc->sCmn.fs_kHz, psEnc->sCmn.inputBuf + 1, psEnc->sCmn.frame_length ); + + /* Add tiny signal to avoid high CPU load from denormalized floating point numbers */ + for( i = 0; i < 8; i++ ) { + x_frame[ LA_SHAPE_MS * psEnc->sCmn.fs_kHz + i * ( psEnc->sCmn.frame_length >> 3 ) ] += ( 1 - ( i & 2 ) ) * 1e-6f; + } + + if( !psEnc->sCmn.prefillFlag ) { + /*****************************************/ + /* Find pitch lags, initial LPC analysis */ + /*****************************************/ + silk_find_pitch_lags_FLP( psEnc, &sEncCtrl, res_pitch, x_frame, psEnc->sCmn.arch ); + + /************************/ + /* Noise shape analysis */ + /************************/ + silk_noise_shape_analysis_FLP( psEnc, &sEncCtrl, res_pitch_frame, x_frame ); + + /***************************************************/ + /* Find linear prediction coefficients (LPC + LTP) */ + /***************************************************/ + silk_find_pred_coefs_FLP( psEnc, &sEncCtrl, res_pitch_frame, x_frame, condCoding ); + + /****************************************/ + /* Process gains */ + /****************************************/ + silk_process_gains_FLP( psEnc, &sEncCtrl, condCoding ); + + /****************************************/ + /* Low Bitrate Redundant Encoding */ + /****************************************/ + silk_LBRR_encode_FLP( psEnc, &sEncCtrl, x_frame, condCoding ); + + /* Loop over quantizer and entroy coding to control bitrate */ + maxIter = 6; + gainMult_Q8 = SILK_FIX_CONST( 1, 8 ); + found_lower = 0; + found_upper = 0; + gainsID = silk_gains_ID( psEnc->sCmn.indices.GainsIndices, psEnc->sCmn.nb_subfr ); + gainsID_lower = -1; + gainsID_upper = -1; + /* Copy part of the input state */ + silk_memcpy( &sRangeEnc_copy, psRangeEnc, sizeof( ec_enc ) ); + silk_memcpy( &sNSQ_copy, &psEnc->sCmn.sNSQ, sizeof( silk_nsq_state ) ); + seed_copy = psEnc->sCmn.indices.Seed; + ec_prevLagIndex_copy = psEnc->sCmn.ec_prevLagIndex; + ec_prevSignalType_copy = psEnc->sCmn.ec_prevSignalType; + for( iter = 0; ; iter++ ) { + if( gainsID == gainsID_lower ) { + nBits = nBits_lower; + } else if( gainsID == gainsID_upper ) { + nBits = nBits_upper; + } else { + /* Restore part of the input state */ + if( iter > 0 ) { + silk_memcpy( psRangeEnc, &sRangeEnc_copy, sizeof( ec_enc ) ); + silk_memcpy( &psEnc->sCmn.sNSQ, &sNSQ_copy, sizeof( silk_nsq_state ) ); + psEnc->sCmn.indices.Seed = seed_copy; + psEnc->sCmn.ec_prevLagIndex = ec_prevLagIndex_copy; + psEnc->sCmn.ec_prevSignalType = ec_prevSignalType_copy; + } + + /*****************************************/ + /* Noise shaping quantization */ + /*****************************************/ + silk_NSQ_wrapper_FLP( psEnc, &sEncCtrl, &psEnc->sCmn.indices, &psEnc->sCmn.sNSQ, psEnc->sCmn.pulses, x_frame ); + + if ( iter == maxIter && !found_lower ) { + silk_memcpy( &sRangeEnc_copy2, psRangeEnc, sizeof( ec_enc ) ); + } + + /****************************************/ + /* Encode Parameters */ + /****************************************/ + silk_encode_indices( &psEnc->sCmn, psRangeEnc, psEnc->sCmn.nFramesEncoded, 0, condCoding ); + + /****************************************/ + /* Encode Excitation Signal */ + /****************************************/ + silk_encode_pulses( psRangeEnc, psEnc->sCmn.indices.signalType, psEnc->sCmn.indices.quantOffsetType, + psEnc->sCmn.pulses, psEnc->sCmn.frame_length ); + + nBits = ec_tell( psRangeEnc ); + + /* If we still bust after the last iteration, do some damage control. */ + if ( iter == maxIter && !found_lower && nBits > maxBits ) { + silk_memcpy( psRangeEnc, &sRangeEnc_copy2, sizeof( ec_enc ) ); + + /* Keep gains the same as the last frame. */ + psEnc->sShape.LastGainIndex = sEncCtrl.lastGainIndexPrev; + for ( i = 0; i < psEnc->sCmn.nb_subfr; i++ ) { + psEnc->sCmn.indices.GainsIndices[ i ] = 4; + } + if (condCoding != CODE_CONDITIONALLY) { + psEnc->sCmn.indices.GainsIndices[ 0 ] = sEncCtrl.lastGainIndexPrev; + } + psEnc->sCmn.ec_prevLagIndex = ec_prevLagIndex_copy; + psEnc->sCmn.ec_prevSignalType = ec_prevSignalType_copy; + /* Clear all pulses. */ + for ( i = 0; i < psEnc->sCmn.frame_length; i++ ) { + psEnc->sCmn.pulses[ i ] = 0; + } + + silk_encode_indices( &psEnc->sCmn, psRangeEnc, psEnc->sCmn.nFramesEncoded, 0, condCoding ); + + silk_encode_pulses( psRangeEnc, psEnc->sCmn.indices.signalType, psEnc->sCmn.indices.quantOffsetType, + psEnc->sCmn.pulses, psEnc->sCmn.frame_length ); + + nBits = ec_tell( psRangeEnc ); + } + + if( useCBR == 0 && iter == 0 && nBits <= maxBits ) { + break; + } + } + + if( iter == maxIter ) { + if( found_lower && ( gainsID == gainsID_lower || nBits > maxBits ) ) { + /* Restore output state from earlier iteration that did meet the bitrate budget */ + silk_memcpy( psRangeEnc, &sRangeEnc_copy2, sizeof( ec_enc ) ); + celt_assert( sRangeEnc_copy2.offs <= 1275 ); + silk_memcpy( psRangeEnc->buf, ec_buf_copy, sRangeEnc_copy2.offs ); + silk_memcpy( &psEnc->sCmn.sNSQ, &sNSQ_copy2, sizeof( silk_nsq_state ) ); + psEnc->sShape.LastGainIndex = LastGainIndex_copy2; + } + break; + } + + if( nBits > maxBits ) { + if( found_lower == 0 && iter >= 2 ) { + /* Adjust the quantizer's rate/distortion tradeoff and discard previous "upper" results */ + sEncCtrl.Lambda = silk_max_float(sEncCtrl.Lambda*1.5f, 1.5f); + /* Reducing dithering can help us hit the target. */ + psEnc->sCmn.indices.quantOffsetType = 0; + found_upper = 0; + gainsID_upper = -1; + } else { + found_upper = 1; + nBits_upper = nBits; + gainMult_upper = gainMult_Q8; + gainsID_upper = gainsID; + } + } else if( nBits < maxBits - bits_margin ) { + found_lower = 1; + nBits_lower = nBits; + gainMult_lower = gainMult_Q8; + if( gainsID != gainsID_lower ) { + gainsID_lower = gainsID; + /* Copy part of the output state */ + silk_memcpy( &sRangeEnc_copy2, psRangeEnc, sizeof( ec_enc ) ); + celt_assert( psRangeEnc->offs <= 1275 ); + silk_memcpy( ec_buf_copy, psRangeEnc->buf, psRangeEnc->offs ); + silk_memcpy( &sNSQ_copy2, &psEnc->sCmn.sNSQ, sizeof( silk_nsq_state ) ); + LastGainIndex_copy2 = psEnc->sShape.LastGainIndex; + } + } else { + /* Close enough */ + break; + } + + if ( !found_lower && nBits > maxBits ) { + int j; + for ( i = 0; i < psEnc->sCmn.nb_subfr; i++ ) { + int sum=0; + for ( j = i*psEnc->sCmn.subfr_length; j < (i+1)*psEnc->sCmn.subfr_length; j++ ) { + sum += abs( psEnc->sCmn.pulses[j] ); + } + if ( iter == 0 || (sum < best_sum[i] && !gain_lock[i]) ) { + best_sum[i] = sum; + best_gain_mult[i] = gainMult_Q8; + } else { + gain_lock[i] = 1; + } + } + } + if( ( found_lower & found_upper ) == 0 ) { + /* Adjust gain according to high-rate rate/distortion curve */ + if( nBits > maxBits ) { + gainMult_Q8 = silk_min_32( 1024, gainMult_Q8*3/2 ); + } else { + gainMult_Q8 = silk_max_32( 64, gainMult_Q8*4/5 ); + } + } else { + /* Adjust gain by interpolating */ + gainMult_Q8 = gainMult_lower + ( ( gainMult_upper - gainMult_lower ) * ( maxBits - nBits_lower ) ) / ( nBits_upper - nBits_lower ); + /* New gain multplier must be between 25% and 75% of old range (note that gainMult_upper < gainMult_lower) */ + if( gainMult_Q8 > silk_ADD_RSHIFT32( gainMult_lower, gainMult_upper - gainMult_lower, 2 ) ) { + gainMult_Q8 = silk_ADD_RSHIFT32( gainMult_lower, gainMult_upper - gainMult_lower, 2 ); + } else + if( gainMult_Q8 < silk_SUB_RSHIFT32( gainMult_upper, gainMult_upper - gainMult_lower, 2 ) ) { + gainMult_Q8 = silk_SUB_RSHIFT32( gainMult_upper, gainMult_upper - gainMult_lower, 2 ); + } + } + + for( i = 0; i < psEnc->sCmn.nb_subfr; i++ ) { + opus_int16 tmp; + if ( gain_lock[i] ) { + tmp = best_gain_mult[i]; + } else { + tmp = gainMult_Q8; + } + pGains_Q16[ i ] = silk_LSHIFT_SAT32( silk_SMULWB( sEncCtrl.GainsUnq_Q16[ i ], tmp ), 8 ); + } + + /* Quantize gains */ + psEnc->sShape.LastGainIndex = sEncCtrl.lastGainIndexPrev; + silk_gains_quant( psEnc->sCmn.indices.GainsIndices, pGains_Q16, + &psEnc->sShape.LastGainIndex, condCoding == CODE_CONDITIONALLY, psEnc->sCmn.nb_subfr ); + + /* Unique identifier of gains vector */ + gainsID = silk_gains_ID( psEnc->sCmn.indices.GainsIndices, psEnc->sCmn.nb_subfr ); + + /* Overwrite unquantized gains with quantized gains and convert back to Q0 from Q16 */ + for( i = 0; i < psEnc->sCmn.nb_subfr; i++ ) { + sEncCtrl.Gains[ i ] = pGains_Q16[ i ] / 65536.0f; + } + } + } + + /* Update input buffer */ + silk_memmove( psEnc->x_buf, &psEnc->x_buf[ psEnc->sCmn.frame_length ], + ( psEnc->sCmn.ltp_mem_length + LA_SHAPE_MS * psEnc->sCmn.fs_kHz ) * sizeof( silk_float ) ); + + /* Exit without entropy coding */ + if( psEnc->sCmn.prefillFlag ) { + /* No payload */ + *pnBytesOut = 0; + return ret; + } + + /* Parameters needed for next frame */ + psEnc->sCmn.prevLag = sEncCtrl.pitchL[ psEnc->sCmn.nb_subfr - 1 ]; + psEnc->sCmn.prevSignalType = psEnc->sCmn.indices.signalType; + + /****************************************/ + /* Finalize payload */ + /****************************************/ + psEnc->sCmn.first_frame_after_reset = 0; + /* Payload size */ + *pnBytesOut = silk_RSHIFT( ec_tell( psRangeEnc ) + 7, 3 ); + + return ret; +} + +/* Low-Bitrate Redundancy (LBRR) encoding. Reuse all parameters but encode excitation at lower bitrate */ +static OPUS_INLINE void silk_LBRR_encode_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + const silk_float xfw[], /* I Input signal */ + opus_int condCoding /* I The type of conditional coding used so far for this frame */ +) +{ + opus_int k; + opus_int32 Gains_Q16[ MAX_NB_SUBFR ]; + silk_float TempGains[ MAX_NB_SUBFR ]; + SideInfoIndices *psIndices_LBRR = &psEnc->sCmn.indices_LBRR[ psEnc->sCmn.nFramesEncoded ]; + silk_nsq_state sNSQ_LBRR; + + /*******************************************/ + /* Control use of inband LBRR */ + /*******************************************/ + if( psEnc->sCmn.LBRR_enabled && psEnc->sCmn.speech_activity_Q8 > SILK_FIX_CONST( LBRR_SPEECH_ACTIVITY_THRES, 8 ) ) { + psEnc->sCmn.LBRR_flags[ psEnc->sCmn.nFramesEncoded ] = 1; + + /* Copy noise shaping quantizer state and quantization indices from regular encoding */ + silk_memcpy( &sNSQ_LBRR, &psEnc->sCmn.sNSQ, sizeof( silk_nsq_state ) ); + silk_memcpy( psIndices_LBRR, &psEnc->sCmn.indices, sizeof( SideInfoIndices ) ); + + /* Save original gains */ + silk_memcpy( TempGains, psEncCtrl->Gains, psEnc->sCmn.nb_subfr * sizeof( silk_float ) ); + + if( psEnc->sCmn.nFramesEncoded == 0 || psEnc->sCmn.LBRR_flags[ psEnc->sCmn.nFramesEncoded - 1 ] == 0 ) { + /* First frame in packet or previous frame not LBRR coded */ + psEnc->sCmn.LBRRprevLastGainIndex = psEnc->sShape.LastGainIndex; + + /* Increase Gains to get target LBRR rate */ + psIndices_LBRR->GainsIndices[ 0 ] += psEnc->sCmn.LBRR_GainIncreases; + psIndices_LBRR->GainsIndices[ 0 ] = silk_min_int( psIndices_LBRR->GainsIndices[ 0 ], N_LEVELS_QGAIN - 1 ); + } + + /* Decode to get gains in sync with decoder */ + silk_gains_dequant( Gains_Q16, psIndices_LBRR->GainsIndices, + &psEnc->sCmn.LBRRprevLastGainIndex, condCoding == CODE_CONDITIONALLY, psEnc->sCmn.nb_subfr ); + + /* Overwrite unquantized gains with quantized gains and convert back to Q0 from Q16 */ + for( k = 0; k < psEnc->sCmn.nb_subfr; k++ ) { + psEncCtrl->Gains[ k ] = Gains_Q16[ k ] * ( 1.0f / 65536.0f ); + } + + /*****************************************/ + /* Noise shaping quantization */ + /*****************************************/ + silk_NSQ_wrapper_FLP( psEnc, psEncCtrl, psIndices_LBRR, &sNSQ_LBRR, + psEnc->sCmn.pulses_LBRR[ psEnc->sCmn.nFramesEncoded ], xfw ); + + /* Restore original gains */ + silk_memcpy( psEncCtrl->Gains, TempGains, psEnc->sCmn.nb_subfr * sizeof( silk_float ) ); + } +} diff --git a/port/third_party/opus/silk/float/energy_FLP.c b/port/third_party/opus/silk/float/energy_FLP.c new file mode 100644 index 00000000..7bc7173c --- /dev/null +++ b/port/third_party/opus/silk/float/energy_FLP.c @@ -0,0 +1,59 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FLP.h" + +/* sum of squares of a silk_float array, with result as double */ +double silk_energy_FLP( + const silk_float *data, + opus_int dataSize +) +{ + opus_int i; + double result; + + /* 4x unrolled loop */ + result = 0.0; + for( i = 0; i < dataSize - 3; i += 4 ) { + result += data[ i + 0 ] * (double)data[ i + 0 ] + + data[ i + 1 ] * (double)data[ i + 1 ] + + data[ i + 2 ] * (double)data[ i + 2 ] + + data[ i + 3 ] * (double)data[ i + 3 ]; + } + + /* add any remaining products */ + for( ; i < dataSize; i++ ) { + result += data[ i ] * (double)data[ i ]; + } + + silk_assert( result >= 0.0 ); + return result; +} diff --git a/port/third_party/opus/silk/float/find_LPC_FLP.c b/port/third_party/opus/silk/float/find_LPC_FLP.c new file mode 100644 index 00000000..6ccd711d --- /dev/null +++ b/port/third_party/opus/silk/float/find_LPC_FLP.c @@ -0,0 +1,105 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "define.h" +#include "main_FLP.h" +#include "tuning_parameters.h" + +/* LPC analysis */ +void silk_find_LPC_FLP( + silk_encoder_state *psEncC, /* I/O Encoder state */ + opus_int16 NLSF_Q15[], /* O NLSFs */ + const silk_float x[], /* I Input signal */ + const silk_float minInvGain, /* I Inverse of max prediction gain */ + int arch +) +{ + opus_int k, subfr_length; + silk_float a[ MAX_LPC_ORDER ]; + + /* Used only for NLSF interpolation */ + silk_float res_nrg, res_nrg_2nd, res_nrg_interp; + opus_int16 NLSF0_Q15[ MAX_LPC_ORDER ]; + silk_float a_tmp[ MAX_LPC_ORDER ]; + silk_float LPC_res[ MAX_FRAME_LENGTH + MAX_NB_SUBFR * MAX_LPC_ORDER ]; + + subfr_length = psEncC->subfr_length + psEncC->predictLPCOrder; + + /* Default: No interpolation */ + psEncC->indices.NLSFInterpCoef_Q2 = 4; + + /* Burg AR analysis for the full frame */ + res_nrg = silk_burg_modified_FLP( a, x, minInvGain, subfr_length, psEncC->nb_subfr, psEncC->predictLPCOrder, arch ); + + if( psEncC->useInterpolatedNLSFs && !psEncC->first_frame_after_reset && psEncC->nb_subfr == MAX_NB_SUBFR ) { + /* Optimal solution for last 10 ms; subtract residual energy here, as that's easier than */ + /* adding it to the residual energy of the first 10 ms in each iteration of the search below */ + res_nrg -= silk_burg_modified_FLP( a_tmp, x + ( MAX_NB_SUBFR / 2 ) * subfr_length, minInvGain, subfr_length, MAX_NB_SUBFR / 2, psEncC->predictLPCOrder, arch ); + + /* Convert to NLSFs */ + silk_A2NLSF_FLP( NLSF_Q15, a_tmp, psEncC->predictLPCOrder ); + + /* Search over interpolation indices to find the one with lowest residual energy */ + res_nrg_2nd = silk_float_MAX; + for( k = 3; k >= 0; k-- ) { + /* Interpolate NLSFs for first half */ + silk_interpolate( NLSF0_Q15, psEncC->prev_NLSFq_Q15, NLSF_Q15, k, psEncC->predictLPCOrder ); + + /* Convert to LPC for residual energy evaluation */ + silk_NLSF2A_FLP( a_tmp, NLSF0_Q15, psEncC->predictLPCOrder, psEncC->arch ); + + /* Calculate residual energy with LSF interpolation */ + silk_LPC_analysis_filter_FLP( LPC_res, a_tmp, x, 2 * subfr_length, psEncC->predictLPCOrder ); + res_nrg_interp = (silk_float)( + silk_energy_FLP( LPC_res + psEncC->predictLPCOrder, subfr_length - psEncC->predictLPCOrder ) + + silk_energy_FLP( LPC_res + psEncC->predictLPCOrder + subfr_length, subfr_length - psEncC->predictLPCOrder ) ); + + /* Determine whether current interpolated NLSFs are best so far */ + if( res_nrg_interp < res_nrg ) { + /* Interpolation has lower residual energy */ + res_nrg = res_nrg_interp; + psEncC->indices.NLSFInterpCoef_Q2 = (opus_int8)k; + } else if( res_nrg_interp > res_nrg_2nd ) { + /* No reason to continue iterating - residual energies will continue to climb */ + break; + } + res_nrg_2nd = res_nrg_interp; + } + } + + if( psEncC->indices.NLSFInterpCoef_Q2 == 4 ) { + /* NLSF interpolation is currently inactive, calculate NLSFs from full frame AR coefficients */ + silk_A2NLSF_FLP( NLSF_Q15, a, psEncC->predictLPCOrder ); + } + + celt_assert( psEncC->indices.NLSFInterpCoef_Q2 == 4 || + ( psEncC->useInterpolatedNLSFs && !psEncC->first_frame_after_reset && psEncC->nb_subfr == MAX_NB_SUBFR ) ); +} diff --git a/port/third_party/opus/silk/float/find_LTP_FLP.c b/port/third_party/opus/silk/float/find_LTP_FLP.c new file mode 100644 index 00000000..90aeeac0 --- /dev/null +++ b/port/third_party/opus/silk/float/find_LTP_FLP.c @@ -0,0 +1,65 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" +#include "tuning_parameters.h" + +void silk_find_LTP_FLP( + silk_float XX[ MAX_NB_SUBFR * LTP_ORDER * LTP_ORDER ], /* O Weight for LTP quantization */ + silk_float xX[ MAX_NB_SUBFR * LTP_ORDER ], /* O Weight for LTP quantization */ + const silk_float r_ptr[], /* I LPC residual */ + const opus_int lag[ MAX_NB_SUBFR ], /* I LTP lags */ + const opus_int subfr_length, /* I Subframe length */ + const opus_int nb_subfr, /* I number of subframes */ + int arch +) +{ + opus_int k; + silk_float *xX_ptr, *XX_ptr; + const silk_float *lag_ptr; + silk_float xx, temp; + + xX_ptr = xX; + XX_ptr = XX; + for( k = 0; k < nb_subfr; k++ ) { + lag_ptr = r_ptr - ( lag[ k ] + LTP_ORDER / 2 ); + silk_corrMatrix_FLP( lag_ptr, subfr_length, LTP_ORDER, XX_ptr, arch ); + silk_corrVector_FLP( lag_ptr, r_ptr, subfr_length, LTP_ORDER, xX_ptr, arch ); + xx = ( silk_float )silk_energy_FLP( r_ptr, subfr_length + LTP_ORDER ); + temp = 1.0f / silk_max( xx, LTP_CORR_INV_MAX * 0.5f * ( XX_ptr[ 0 ] + XX_ptr[ 24 ] ) + 1.0f ); + silk_scale_vector_FLP( XX_ptr, temp, LTP_ORDER * LTP_ORDER ); + silk_scale_vector_FLP( xX_ptr, temp, LTP_ORDER ); + + r_ptr += subfr_length; + XX_ptr += LTP_ORDER * LTP_ORDER; + xX_ptr += LTP_ORDER; + } +} diff --git a/port/third_party/opus/silk/float/find_pitch_lags_FLP.c b/port/third_party/opus/silk/float/find_pitch_lags_FLP.c new file mode 100644 index 00000000..1f6bd599 --- /dev/null +++ b/port/third_party/opus/silk/float/find_pitch_lags_FLP.c @@ -0,0 +1,132 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "main_FLP.h" +#include "tuning_parameters.h" + +void silk_find_pitch_lags_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + silk_float res[], /* O Residual */ + const silk_float x[], /* I Speech signal */ + int arch /* I Run-time architecture */ +) +{ + opus_int buf_len; + silk_float thrhld, res_nrg; + const silk_float *x_buf_ptr, *x_buf; + silk_float auto_corr[ MAX_FIND_PITCH_LPC_ORDER + 1 ]; + silk_float A[ MAX_FIND_PITCH_LPC_ORDER ]; + silk_float refl_coef[ MAX_FIND_PITCH_LPC_ORDER ]; + silk_float Wsig[ FIND_PITCH_LPC_WIN_MAX ]; + silk_float *Wsig_ptr; + + /******************************************/ + /* Set up buffer lengths etc based on Fs */ + /******************************************/ + buf_len = psEnc->sCmn.la_pitch + psEnc->sCmn.frame_length + psEnc->sCmn.ltp_mem_length; + + /* Safety check */ + celt_assert( buf_len >= psEnc->sCmn.pitch_LPC_win_length ); + + x_buf = x - psEnc->sCmn.ltp_mem_length; + + /******************************************/ + /* Estimate LPC AR coeficients */ + /******************************************/ + + /* Calculate windowed signal */ + + /* First LA_LTP samples */ + x_buf_ptr = x_buf + buf_len - psEnc->sCmn.pitch_LPC_win_length; + Wsig_ptr = Wsig; + silk_apply_sine_window_FLP( Wsig_ptr, x_buf_ptr, 1, psEnc->sCmn.la_pitch ); + + /* Middle non-windowed samples */ + Wsig_ptr += psEnc->sCmn.la_pitch; + x_buf_ptr += psEnc->sCmn.la_pitch; + silk_memcpy( Wsig_ptr, x_buf_ptr, ( psEnc->sCmn.pitch_LPC_win_length - ( psEnc->sCmn.la_pitch << 1 ) ) * sizeof( silk_float ) ); + + /* Last LA_LTP samples */ + Wsig_ptr += psEnc->sCmn.pitch_LPC_win_length - ( psEnc->sCmn.la_pitch << 1 ); + x_buf_ptr += psEnc->sCmn.pitch_LPC_win_length - ( psEnc->sCmn.la_pitch << 1 ); + silk_apply_sine_window_FLP( Wsig_ptr, x_buf_ptr, 2, psEnc->sCmn.la_pitch ); + + /* Calculate autocorrelation sequence */ + silk_autocorrelation_FLP( auto_corr, Wsig, psEnc->sCmn.pitch_LPC_win_length, psEnc->sCmn.pitchEstimationLPCOrder + 1, arch ); + + /* Add white noise, as a fraction of the energy */ + auto_corr[ 0 ] += auto_corr[ 0 ] * FIND_PITCH_WHITE_NOISE_FRACTION + 1; + + /* Calculate the reflection coefficients using Schur */ + res_nrg = silk_schur_FLP( refl_coef, auto_corr, psEnc->sCmn.pitchEstimationLPCOrder ); + + /* Prediction gain */ + psEncCtrl->predGain = auto_corr[ 0 ] / silk_max_float( res_nrg, 1.0f ); + + /* Convert reflection coefficients to prediction coefficients */ + silk_k2a_FLP( A, refl_coef, psEnc->sCmn.pitchEstimationLPCOrder ); + + /* Bandwidth expansion */ + silk_bwexpander_FLP( A, psEnc->sCmn.pitchEstimationLPCOrder, FIND_PITCH_BANDWIDTH_EXPANSION ); + + /*****************************************/ + /* LPC analysis filtering */ + /*****************************************/ + silk_LPC_analysis_filter_FLP( res, A, x_buf, buf_len, psEnc->sCmn.pitchEstimationLPCOrder ); + + if( psEnc->sCmn.indices.signalType != TYPE_NO_VOICE_ACTIVITY && psEnc->sCmn.first_frame_after_reset == 0 ) { + /* Threshold for pitch estimator */ + thrhld = 0.6f; + thrhld -= 0.004f * psEnc->sCmn.pitchEstimationLPCOrder; + thrhld -= 0.1f * psEnc->sCmn.speech_activity_Q8 * ( 1.0f / 256.0f ); + thrhld -= 0.15f * (psEnc->sCmn.prevSignalType >> 1); + thrhld -= 0.1f * psEnc->sCmn.input_tilt_Q15 * ( 1.0f / 32768.0f ); + + /*****************************************/ + /* Call Pitch estimator */ + /*****************************************/ + if( silk_pitch_analysis_core_FLP( res, psEncCtrl->pitchL, &psEnc->sCmn.indices.lagIndex, + &psEnc->sCmn.indices.contourIndex, &psEnc->LTPCorr, psEnc->sCmn.prevLag, psEnc->sCmn.pitchEstimationThreshold_Q16 / 65536.0f, + thrhld, psEnc->sCmn.fs_kHz, psEnc->sCmn.pitchEstimationComplexity, psEnc->sCmn.nb_subfr, arch ) == 0 ) + { + psEnc->sCmn.indices.signalType = TYPE_VOICED; + } else { + psEnc->sCmn.indices.signalType = TYPE_UNVOICED; + } + } else { + silk_memset( psEncCtrl->pitchL, 0, sizeof( psEncCtrl->pitchL ) ); + psEnc->sCmn.indices.lagIndex = 0; + psEnc->sCmn.indices.contourIndex = 0; + psEnc->LTPCorr = 0; + } +} diff --git a/port/third_party/opus/silk/float/find_pred_coefs_FLP.c b/port/third_party/opus/silk/float/find_pred_coefs_FLP.c new file mode 100644 index 00000000..f3c54cf4 --- /dev/null +++ b/port/third_party/opus/silk/float/find_pred_coefs_FLP.c @@ -0,0 +1,117 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" + +/* Find LPC and LTP coefficients */ +void silk_find_pred_coefs_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + const silk_float res_pitch[], /* I Residual from pitch analysis */ + const silk_float x[], /* I Speech signal */ + opus_int condCoding /* I The type of conditional coding to use */ +) +{ + opus_int i; + silk_float XXLTP[ MAX_NB_SUBFR * LTP_ORDER * LTP_ORDER ]; + silk_float xXLTP[ MAX_NB_SUBFR * LTP_ORDER ]; + silk_float invGains[ MAX_NB_SUBFR ]; + /* Set to NLSF_Q15 to zero so we don't copy junk to the state. */ + opus_int16 NLSF_Q15[ MAX_LPC_ORDER ]={0}; + const silk_float *x_ptr; + silk_float *x_pre_ptr, LPC_in_pre[ MAX_NB_SUBFR * MAX_LPC_ORDER + MAX_FRAME_LENGTH ]; + silk_float minInvGain; + + /* Weighting for weighted least squares */ + for( i = 0; i < psEnc->sCmn.nb_subfr; i++ ) { + silk_assert( psEncCtrl->Gains[ i ] > 0.0f ); + invGains[ i ] = 1.0f / psEncCtrl->Gains[ i ]; + } + + if( psEnc->sCmn.indices.signalType == TYPE_VOICED ) { + /**********/ + /* VOICED */ + /**********/ + celt_assert( psEnc->sCmn.ltp_mem_length - psEnc->sCmn.predictLPCOrder >= psEncCtrl->pitchL[ 0 ] + LTP_ORDER / 2 ); + + /* LTP analysis */ + silk_find_LTP_FLP( XXLTP, xXLTP, res_pitch, psEncCtrl->pitchL, psEnc->sCmn.subfr_length, psEnc->sCmn.nb_subfr, psEnc->sCmn.arch ); + + /* Quantize LTP gain parameters */ + silk_quant_LTP_gains_FLP( psEncCtrl->LTPCoef, psEnc->sCmn.indices.LTPIndex, &psEnc->sCmn.indices.PERIndex, + &psEnc->sCmn.sum_log_gain_Q7, &psEncCtrl->LTPredCodGain, XXLTP, xXLTP, psEnc->sCmn.subfr_length, psEnc->sCmn.nb_subfr, psEnc->sCmn.arch ); + + /* Control LTP scaling */ + silk_LTP_scale_ctrl_FLP( psEnc, psEncCtrl, condCoding ); + + /* Create LTP residual */ + silk_LTP_analysis_filter_FLP( LPC_in_pre, x - psEnc->sCmn.predictLPCOrder, psEncCtrl->LTPCoef, + psEncCtrl->pitchL, invGains, psEnc->sCmn.subfr_length, psEnc->sCmn.nb_subfr, psEnc->sCmn.predictLPCOrder ); + } else { + /************/ + /* UNVOICED */ + /************/ + /* Create signal with prepended subframes, scaled by inverse gains */ + x_ptr = x - psEnc->sCmn.predictLPCOrder; + x_pre_ptr = LPC_in_pre; + for( i = 0; i < psEnc->sCmn.nb_subfr; i++ ) { + silk_scale_copy_vector_FLP( x_pre_ptr, x_ptr, invGains[ i ], + psEnc->sCmn.subfr_length + psEnc->sCmn.predictLPCOrder ); + x_pre_ptr += psEnc->sCmn.subfr_length + psEnc->sCmn.predictLPCOrder; + x_ptr += psEnc->sCmn.subfr_length; + } + silk_memset( psEncCtrl->LTPCoef, 0, psEnc->sCmn.nb_subfr * LTP_ORDER * sizeof( silk_float ) ); + psEncCtrl->LTPredCodGain = 0.0f; + psEnc->sCmn.sum_log_gain_Q7 = 0; + } + + /* Limit on total predictive coding gain */ + if( psEnc->sCmn.first_frame_after_reset ) { + minInvGain = 1.0f / MAX_PREDICTION_POWER_GAIN_AFTER_RESET; + } else { + minInvGain = (silk_float)pow( 2, psEncCtrl->LTPredCodGain / 3 ) / MAX_PREDICTION_POWER_GAIN; + minInvGain /= 0.25f + 0.75f * psEncCtrl->coding_quality; + } + + /* LPC_in_pre contains the LTP-filtered input for voiced, and the unfiltered input for unvoiced */ + silk_find_LPC_FLP( &psEnc->sCmn, NLSF_Q15, LPC_in_pre, minInvGain, psEnc->sCmn.arch ); + + /* Quantize LSFs */ + silk_process_NLSFs_FLP( &psEnc->sCmn, psEncCtrl->PredCoef, NLSF_Q15, psEnc->sCmn.prev_NLSFq_Q15 ); + + /* Calculate residual energy using quantized LPC coefficients */ + silk_residual_energy_FLP( psEncCtrl->ResNrg, LPC_in_pre, psEncCtrl->PredCoef, psEncCtrl->Gains, + psEnc->sCmn.subfr_length, psEnc->sCmn.nb_subfr, psEnc->sCmn.predictLPCOrder ); + + /* Copy to prediction struct for use in next frame for interpolation */ + silk_memcpy( psEnc->sCmn.prev_NLSFq_Q15, NLSF_Q15, sizeof( psEnc->sCmn.prev_NLSFq_Q15 ) ); +} + diff --git a/port/third_party/opus/silk/float/inner_product_FLP.c b/port/third_party/opus/silk/float/inner_product_FLP.c new file mode 100644 index 00000000..88b160ab --- /dev/null +++ b/port/third_party/opus/silk/float/inner_product_FLP.c @@ -0,0 +1,59 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FLP.h" + +/* inner product of two silk_float arrays, with result as double */ +double silk_inner_product_FLP_c( + const silk_float *data1, + const silk_float *data2, + opus_int dataSize +) +{ + opus_int i; + double result; + + /* 4x unrolled loop */ + result = 0.0; + for( i = 0; i < dataSize - 3; i += 4 ) { + result += data1[ i + 0 ] * (double)data2[ i + 0 ] + + data1[ i + 1 ] * (double)data2[ i + 1 ] + + data1[ i + 2 ] * (double)data2[ i + 2 ] + + data1[ i + 3 ] * (double)data2[ i + 3 ]; + } + + /* add any remaining products */ + for( ; i < dataSize; i++ ) { + result += data1[ i ] * (double)data2[ i ]; + } + + return result; +} diff --git a/port/third_party/opus/silk/float/k2a_FLP.c b/port/third_party/opus/silk/float/k2a_FLP.c new file mode 100644 index 00000000..1448008d --- /dev/null +++ b/port/third_party/opus/silk/float/k2a_FLP.c @@ -0,0 +1,54 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FLP.h" + +/* step up function, converts reflection coefficients to prediction coefficients */ +void silk_k2a_FLP( + silk_float *A, /* O prediction coefficients [order] */ + const silk_float *rc, /* I reflection coefficients [order] */ + opus_int32 order /* I prediction order */ +) +{ + opus_int k, n; + silk_float rck, tmp1, tmp2; + + for( k = 0; k < order; k++ ) { + rck = rc[ k ]; + for( n = 0; n < (k + 1) >> 1; n++ ) { + tmp1 = A[ n ]; + tmp2 = A[ k - n - 1 ]; + A[ n ] = tmp1 + tmp2 * rck; + A[ k - n - 1 ] = tmp2 + tmp1 * rck; + } + A[ k ] = -rck; + } +} diff --git a/port/third_party/opus/silk/float/main_FLP.h b/port/third_party/opus/silk/float/main_FLP.h new file mode 100644 index 00000000..2e4435cc --- /dev/null +++ b/port/third_party/opus/silk/float/main_FLP.h @@ -0,0 +1,290 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_MAIN_FLP_H +#define SILK_MAIN_FLP_H + +#include "SigProc_FLP.h" +#include "SigProc_FIX.h" +#include "structs_FLP.h" +#include "main.h" +#include "define.h" +#include "debug.h" +#include "entenc.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define silk_encoder_state_Fxx silk_encoder_state_FLP +#define silk_encode_do_VAD_Fxx silk_encode_do_VAD_FLP +#define silk_encode_frame_Fxx silk_encode_frame_FLP + +/*********************/ +/* Encoder Functions */ +/*********************/ + +/* High-pass filter with cutoff frequency adaptation based on pitch lag statistics */ +void silk_HP_variable_cutoff( + silk_encoder_state_Fxx state_Fxx[] /* I/O Encoder states */ +); + +/* Encoder main function */ +void silk_encode_do_VAD_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + opus_int activity /* I Decision of Opus voice activity detector */ +); + +/* Encoder main function */ +opus_int silk_encode_frame_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + opus_int32 *pnBytesOut, /* O Number of payload bytes; */ + ec_enc *psRangeEnc, /* I/O compressor data structure */ + opus_int condCoding, /* I The type of conditional coding to use */ + opus_int maxBits, /* I If > 0: maximum number of output bits */ + opus_int useCBR /* I Flag to force constant-bitrate operation */ +); + +/* Initializes the Silk encoder state */ +opus_int silk_init_encoder( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + int arch /* I Run-tim architecture */ +); + +/* Control the Silk encoder */ +opus_int silk_control_encoder( + silk_encoder_state_FLP *psEnc, /* I/O Pointer to Silk encoder state FLP */ + silk_EncControlStruct *encControl, /* I Control structure */ + const opus_int allow_bw_switch, /* I Flag to allow switching audio bandwidth */ + const opus_int channelNb, /* I Channel number */ + const opus_int force_fs_kHz +); + +/**************************/ +/* Noise shaping analysis */ +/**************************/ +/* Compute noise shaping coefficients and initial gain values */ +void silk_noise_shape_analysis_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + const silk_float *pitch_res, /* I LPC residual from pitch analysis */ + const silk_float *x /* I Input signal [frame_length + la_shape] */ +); + +/* Autocorrelations for a warped frequency axis */ +void silk_warped_autocorrelation_FLP( + silk_float *corr, /* O Result [order + 1] */ + const silk_float *input, /* I Input data to correlate */ + const silk_float warping, /* I Warping coefficient */ + const opus_int length, /* I Length of input */ + const opus_int order /* I Correlation order (even) */ +); + +/* Calculation of LTP state scaling */ +void silk_LTP_scale_ctrl_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + opus_int condCoding /* I The type of conditional coding to use */ +); + +/**********************************************/ +/* Prediction Analysis */ +/**********************************************/ +/* Find pitch lags */ +void silk_find_pitch_lags_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + silk_float res[], /* O Residual */ + const silk_float x[], /* I Speech signal */ + int arch /* I Run-time architecture */ +); + +/* Find LPC and LTP coefficients */ +void silk_find_pred_coefs_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + const silk_float res_pitch[], /* I Residual from pitch analysis */ + const silk_float x[], /* I Speech signal */ + opus_int condCoding /* I The type of conditional coding to use */ +); + +/* LPC analysis */ +void silk_find_LPC_FLP( + silk_encoder_state *psEncC, /* I/O Encoder state */ + opus_int16 NLSF_Q15[], /* O NLSFs */ + const silk_float x[], /* I Input signal */ + const silk_float minInvGain, /* I Prediction gain from LTP (dB) */ + int arch +); + +/* LTP analysis */ +void silk_find_LTP_FLP( + silk_float XX[ MAX_NB_SUBFR * LTP_ORDER * LTP_ORDER ], /* O Weight for LTP quantization */ + silk_float xX[ MAX_NB_SUBFR * LTP_ORDER ], /* O Weight for LTP quantization */ + const silk_float r_ptr[], /* I LPC residual */ + const opus_int lag[ MAX_NB_SUBFR ], /* I LTP lags */ + const opus_int subfr_length, /* I Subframe length */ + const opus_int nb_subfr, /* I number of subframes */ + int arch +); + +void silk_LTP_analysis_filter_FLP( + silk_float *LTP_res, /* O LTP res MAX_NB_SUBFR*(pre_lgth+subfr_lngth) */ + const silk_float *x, /* I Input signal, with preceding samples */ + const silk_float B[ LTP_ORDER * MAX_NB_SUBFR ], /* I LTP coefficients for each subframe */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lags */ + const silk_float invGains[ MAX_NB_SUBFR ], /* I Inverse quantization gains */ + const opus_int subfr_length, /* I Length of each subframe */ + const opus_int nb_subfr, /* I number of subframes */ + const opus_int pre_length /* I Preceding samples for each subframe */ +); + +/* Calculates residual energies of input subframes where all subframes have LPC_order */ +/* of preceding samples */ +void silk_residual_energy_FLP( + silk_float nrgs[ MAX_NB_SUBFR ], /* O Residual energy per subframe */ + const silk_float x[], /* I Input signal */ + silk_float a[ 2 ][ MAX_LPC_ORDER ], /* I AR coefs for each frame half */ + const silk_float gains[], /* I Quantization gains */ + const opus_int subfr_length, /* I Subframe length */ + const opus_int nb_subfr, /* I number of subframes */ + const opus_int LPC_order /* I LPC order */ +); + +/* 16th order LPC analysis filter */ +void silk_LPC_analysis_filter_FLP( + silk_float r_LPC[], /* O LPC residual signal */ + const silk_float PredCoef[], /* I LPC coefficients */ + const silk_float s[], /* I Input signal */ + const opus_int length, /* I Length of input signal */ + const opus_int Order /* I LPC order */ +); + +/* LTP tap quantizer */ +void silk_quant_LTP_gains_FLP( + silk_float B[ MAX_NB_SUBFR * LTP_ORDER ], /* O Quantized LTP gains */ + opus_int8 cbk_index[ MAX_NB_SUBFR ], /* O Codebook index */ + opus_int8 *periodicity_index, /* O Periodicity index */ + opus_int32 *sum_log_gain_Q7, /* I/O Cumulative max prediction gain */ + silk_float *pred_gain_dB, /* O LTP prediction gain */ + const silk_float XX[ MAX_NB_SUBFR * LTP_ORDER * LTP_ORDER ], /* I Correlation matrix */ + const silk_float xX[ MAX_NB_SUBFR * LTP_ORDER ], /* I Correlation vector */ + const opus_int subfr_len, /* I Number of samples per subframe */ + const opus_int nb_subfr, /* I Number of subframes */ + int arch /* I Run-time architecture */ +); + +/* Residual energy: nrg = wxx - 2 * wXx * c + c' * wXX * c */ +silk_float silk_residual_energy_covar_FLP( /* O Weighted residual energy */ + const silk_float *c, /* I Filter coefficients */ + silk_float *wXX, /* I/O Weighted correlation matrix, reg. out */ + const silk_float *wXx, /* I Weighted correlation vector */ + const silk_float wxx, /* I Weighted correlation value */ + const opus_int D /* I Dimension */ +); + +/* Processing of gains */ +void silk_process_gains_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + opus_int condCoding /* I The type of conditional coding to use */ +); + +/******************/ +/* Linear Algebra */ +/******************/ +/* Calculates correlation matrix X'*X */ +void silk_corrMatrix_FLP( + const silk_float *x, /* I x vector [ L+order-1 ] used to create X */ + const opus_int L, /* I Length of vectors */ + const opus_int Order, /* I Max lag for correlation */ + silk_float *XX, /* O X'*X correlation matrix [order x order] */ + int arch +); + +/* Calculates correlation vector X'*t */ +void silk_corrVector_FLP( + const silk_float *x, /* I x vector [L+order-1] used to create X */ + const silk_float *t, /* I Target vector [L] */ + const opus_int L, /* I Length of vecors */ + const opus_int Order, /* I Max lag for correlation */ + silk_float *Xt, /* O X'*t correlation vector [order] */ + int arch +); + +/* Apply sine window to signal vector. */ +/* Window types: */ +/* 1 -> sine window from 0 to pi/2 */ +/* 2 -> sine window from pi/2 to pi */ +void silk_apply_sine_window_FLP( + silk_float px_win[], /* O Pointer to windowed signal */ + const silk_float px[], /* I Pointer to input signal */ + const opus_int win_type, /* I Selects a window type */ + const opus_int length /* I Window length, multiple of 4 */ +); + +/* Wrapper functions. Call flp / fix code */ + +/* Convert AR filter coefficients to NLSF parameters */ +void silk_A2NLSF_FLP( + opus_int16 *NLSF_Q15, /* O NLSF vector [ LPC_order ] */ + const silk_float *pAR, /* I LPC coefficients [ LPC_order ] */ + const opus_int LPC_order /* I LPC order */ +); + +/* Convert NLSF parameters to AR prediction filter coefficients */ +void silk_NLSF2A_FLP( + silk_float *pAR, /* O LPC coefficients [ LPC_order ] */ + const opus_int16 *NLSF_Q15, /* I NLSF vector [ LPC_order ] */ + const opus_int LPC_order, /* I LPC order */ + int arch /* I Run-time architecture */ +); + +/* Limit, stabilize, and quantize NLSFs */ +void silk_process_NLSFs_FLP( + silk_encoder_state *psEncC, /* I/O Encoder state */ + silk_float PredCoef[ 2 ][ MAX_LPC_ORDER ], /* O Prediction coefficients */ + opus_int16 NLSF_Q15[ MAX_LPC_ORDER ], /* I/O Normalized LSFs (quant out) (0 - (2^15-1)) */ + const opus_int16 prev_NLSF_Q15[ MAX_LPC_ORDER ] /* I Previous Normalized LSFs (0 - (2^15-1)) */ +); + +/* Floating-point Silk NSQ wrapper */ +void silk_NSQ_wrapper_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + SideInfoIndices *psIndices, /* I/O Quantization indices */ + silk_nsq_state *psNSQ, /* I/O Noise Shaping Quantzation state */ + opus_int8 pulses[], /* O Quantized pulse signal */ + const silk_float x[] /* I Prefiltered input signal */ +); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/port/third_party/opus/silk/float/noise_shape_analysis_FLP.c b/port/third_party/opus/silk/float/noise_shape_analysis_FLP.c new file mode 100644 index 00000000..0b5ea952 --- /dev/null +++ b/port/third_party/opus/silk/float/noise_shape_analysis_FLP.c @@ -0,0 +1,350 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" +#include "tuning_parameters.h" + +/* Compute gain to make warped filter coefficients have a zero mean log frequency response on a */ +/* non-warped frequency scale. (So that it can be implemented with a minimum-phase monic filter.) */ +/* Note: A monic filter is one with the first coefficient equal to 1.0. In Silk we omit the first */ +/* coefficient in an array of coefficients, for monic filters. */ +static OPUS_INLINE silk_float warped_gain( + const silk_float *coefs, + silk_float lambda, + opus_int order +) { + opus_int i; + silk_float gain; + + lambda = -lambda; + gain = coefs[ order - 1 ]; + for( i = order - 2; i >= 0; i-- ) { + gain = lambda * gain + coefs[ i ]; + } + return (silk_float)( 1.0f / ( 1.0f - lambda * gain ) ); +} + +/* Convert warped filter coefficients to monic pseudo-warped coefficients and limit maximum */ +/* amplitude of monic warped coefficients by using bandwidth expansion on the true coefficients */ +static OPUS_INLINE void warped_true2monic_coefs( + silk_float *coefs, + silk_float lambda, + silk_float limit, + opus_int order +) { + opus_int i, iter, ind = 0; + silk_float tmp, maxabs, chirp, gain; + + /* Convert to monic coefficients */ + for( i = order - 1; i > 0; i-- ) { + coefs[ i - 1 ] -= lambda * coefs[ i ]; + } + gain = ( 1.0f - lambda * lambda ) / ( 1.0f + lambda * coefs[ 0 ] ); + for( i = 0; i < order; i++ ) { + coefs[ i ] *= gain; + } + + /* Limit */ + for( iter = 0; iter < 10; iter++ ) { + /* Find maximum absolute value */ + maxabs = -1.0f; + for( i = 0; i < order; i++ ) { + tmp = silk_abs_float( coefs[ i ] ); + if( tmp > maxabs ) { + maxabs = tmp; + ind = i; + } + } + if( maxabs <= limit ) { + /* Coefficients are within range - done */ + return; + } + + /* Convert back to true warped coefficients */ + for( i = 1; i < order; i++ ) { + coefs[ i - 1 ] += lambda * coefs[ i ]; + } + gain = 1.0f / gain; + for( i = 0; i < order; i++ ) { + coefs[ i ] *= gain; + } + + /* Apply bandwidth expansion */ + chirp = 0.99f - ( 0.8f + 0.1f * iter ) * ( maxabs - limit ) / ( maxabs * ( ind + 1 ) ); + silk_bwexpander_FLP( coefs, order, chirp ); + + /* Convert to monic warped coefficients */ + for( i = order - 1; i > 0; i-- ) { + coefs[ i - 1 ] -= lambda * coefs[ i ]; + } + gain = ( 1.0f - lambda * lambda ) / ( 1.0f + lambda * coefs[ 0 ] ); + for( i = 0; i < order; i++ ) { + coefs[ i ] *= gain; + } + } + silk_assert( 0 ); +} + +static OPUS_INLINE void limit_coefs( + silk_float *coefs, + silk_float limit, + opus_int order +) { + opus_int i, iter, ind = 0; + silk_float tmp, maxabs, chirp; + + for( iter = 0; iter < 10; iter++ ) { + /* Find maximum absolute value */ + maxabs = -1.0f; + for( i = 0; i < order; i++ ) { + tmp = silk_abs_float( coefs[ i ] ); + if( tmp > maxabs ) { + maxabs = tmp; + ind = i; + } + } + if( maxabs <= limit ) { + /* Coefficients are within range - done */ + return; + } + + /* Apply bandwidth expansion */ + chirp = 0.99f - ( 0.8f + 0.1f * iter ) * ( maxabs - limit ) / ( maxabs * ( ind + 1 ) ); + silk_bwexpander_FLP( coefs, order, chirp ); + } + silk_assert( 0 ); +} + +/* Compute noise shaping coefficients and initial gain values */ +void silk_noise_shape_analysis_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + const silk_float *pitch_res, /* I LPC residual from pitch analysis */ + const silk_float *x /* I Input signal [frame_length + la_shape] */ +) +{ + silk_shape_state_FLP *psShapeSt = &psEnc->sShape; + opus_int k, nSamples, nSegs; + silk_float SNR_adj_dB, HarmShapeGain, Tilt; + silk_float nrg, log_energy, log_energy_prev, energy_variation; + silk_float BWExp, gain_mult, gain_add, strength, b, warping; + silk_float x_windowed[ SHAPE_LPC_WIN_MAX ]; + silk_float auto_corr[ MAX_SHAPE_LPC_ORDER + 1 ]; + silk_float rc[ MAX_SHAPE_LPC_ORDER + 1 ]; + const silk_float *x_ptr, *pitch_res_ptr; + + /* Point to start of first LPC analysis block */ + x_ptr = x - psEnc->sCmn.la_shape; + + /****************/ + /* GAIN CONTROL */ + /****************/ + SNR_adj_dB = psEnc->sCmn.SNR_dB_Q7 * ( 1 / 128.0f ); + + /* Input quality is the average of the quality in the lowest two VAD bands */ + psEncCtrl->input_quality = 0.5f * ( psEnc->sCmn.input_quality_bands_Q15[ 0 ] + psEnc->sCmn.input_quality_bands_Q15[ 1 ] ) * ( 1.0f / 32768.0f ); + + /* Coding quality level, between 0.0 and 1.0 */ + psEncCtrl->coding_quality = silk_sigmoid( 0.25f * ( SNR_adj_dB - 20.0f ) ); + + if( psEnc->sCmn.useCBR == 0 ) { + /* Reduce coding SNR during low speech activity */ + b = 1.0f - psEnc->sCmn.speech_activity_Q8 * ( 1.0f / 256.0f ); + SNR_adj_dB -= BG_SNR_DECR_dB * psEncCtrl->coding_quality * ( 0.5f + 0.5f * psEncCtrl->input_quality ) * b * b; + } + + if( psEnc->sCmn.indices.signalType == TYPE_VOICED ) { + /* Reduce gains for periodic signals */ + SNR_adj_dB += HARM_SNR_INCR_dB * psEnc->LTPCorr; + } else { + /* For unvoiced signals and low-quality input, adjust the quality slower than SNR_dB setting */ + SNR_adj_dB += ( -0.4f * psEnc->sCmn.SNR_dB_Q7 * ( 1 / 128.0f ) + 6.0f ) * ( 1.0f - psEncCtrl->input_quality ); + } + + /*************************/ + /* SPARSENESS PROCESSING */ + /*************************/ + /* Set quantizer offset */ + if( psEnc->sCmn.indices.signalType == TYPE_VOICED ) { + /* Initially set to 0; may be overruled in process_gains(..) */ + psEnc->sCmn.indices.quantOffsetType = 0; + } else { + /* Sparseness measure, based on relative fluctuations of energy per 2 milliseconds */ + nSamples = 2 * psEnc->sCmn.fs_kHz; + energy_variation = 0.0f; + log_energy_prev = 0.0f; + pitch_res_ptr = pitch_res; + nSegs = silk_SMULBB( SUB_FRAME_LENGTH_MS, psEnc->sCmn.nb_subfr ) / 2; + for( k = 0; k < nSegs; k++ ) { + nrg = ( silk_float )nSamples + ( silk_float )silk_energy_FLP( pitch_res_ptr, nSamples ); + log_energy = silk_log2( nrg ); + if( k > 0 ) { + energy_variation += silk_abs_float( log_energy - log_energy_prev ); + } + log_energy_prev = log_energy; + pitch_res_ptr += nSamples; + } + + /* Set quantization offset depending on sparseness measure */ + if( energy_variation > ENERGY_VARIATION_THRESHOLD_QNT_OFFSET * (nSegs-1) ) { + psEnc->sCmn.indices.quantOffsetType = 0; + } else { + psEnc->sCmn.indices.quantOffsetType = 1; + } + } + + /*******************************/ + /* Control bandwidth expansion */ + /*******************************/ + /* More BWE for signals with high prediction gain */ + strength = FIND_PITCH_WHITE_NOISE_FRACTION * psEncCtrl->predGain; /* between 0.0 and 1.0 */ + BWExp = BANDWIDTH_EXPANSION / ( 1.0f + strength * strength ); + + /* Slightly more warping in analysis will move quantization noise up in frequency, where it's better masked */ + warping = (silk_float)psEnc->sCmn.warping_Q16 / 65536.0f + 0.01f * psEncCtrl->coding_quality; + + /********************************************/ + /* Compute noise shaping AR coefs and gains */ + /********************************************/ + for( k = 0; k < psEnc->sCmn.nb_subfr; k++ ) { + /* Apply window: sine slope followed by flat part followed by cosine slope */ + opus_int shift, slope_part, flat_part; + flat_part = psEnc->sCmn.fs_kHz * 3; + slope_part = ( psEnc->sCmn.shapeWinLength - flat_part ) / 2; + + silk_apply_sine_window_FLP( x_windowed, x_ptr, 1, slope_part ); + shift = slope_part; + silk_memcpy( x_windowed + shift, x_ptr + shift, flat_part * sizeof(silk_float) ); + shift += flat_part; + silk_apply_sine_window_FLP( x_windowed + shift, x_ptr + shift, 2, slope_part ); + + /* Update pointer: next LPC analysis block */ + x_ptr += psEnc->sCmn.subfr_length; + + if( psEnc->sCmn.warping_Q16 > 0 ) { + /* Calculate warped auto correlation */ + silk_warped_autocorrelation_FLP( auto_corr, x_windowed, warping, + psEnc->sCmn.shapeWinLength, psEnc->sCmn.shapingLPCOrder ); + } else { + /* Calculate regular auto correlation */ + silk_autocorrelation_FLP( auto_corr, x_windowed, psEnc->sCmn.shapeWinLength, psEnc->sCmn.shapingLPCOrder + 1, psEnc->sCmn.arch ); + } + + /* Add white noise, as a fraction of energy */ + auto_corr[ 0 ] += auto_corr[ 0 ] * SHAPE_WHITE_NOISE_FRACTION + 1.0f; + + /* Convert correlations to prediction coefficients, and compute residual energy */ + nrg = silk_schur_FLP( rc, auto_corr, psEnc->sCmn.shapingLPCOrder ); + silk_k2a_FLP( &psEncCtrl->AR[ k * MAX_SHAPE_LPC_ORDER ], rc, psEnc->sCmn.shapingLPCOrder ); + psEncCtrl->Gains[ k ] = ( silk_float )sqrt( nrg ); + + if( psEnc->sCmn.warping_Q16 > 0 ) { + /* Adjust gain for warping */ + psEncCtrl->Gains[ k ] *= warped_gain( &psEncCtrl->AR[ k * MAX_SHAPE_LPC_ORDER ], warping, psEnc->sCmn.shapingLPCOrder ); + } + + /* Bandwidth expansion for synthesis filter shaping */ + silk_bwexpander_FLP( &psEncCtrl->AR[ k * MAX_SHAPE_LPC_ORDER ], psEnc->sCmn.shapingLPCOrder, BWExp ); + + if( psEnc->sCmn.warping_Q16 > 0 ) { + /* Convert to monic warped prediction coefficients and limit absolute values */ + warped_true2monic_coefs( &psEncCtrl->AR[ k * MAX_SHAPE_LPC_ORDER ], warping, 3.999f, psEnc->sCmn.shapingLPCOrder ); + } else { + /* Limit absolute values */ + limit_coefs( &psEncCtrl->AR[ k * MAX_SHAPE_LPC_ORDER ], 3.999f, psEnc->sCmn.shapingLPCOrder ); + } + } + + /*****************/ + /* Gain tweaking */ + /*****************/ + /* Increase gains during low speech activity */ + gain_mult = (silk_float)pow( 2.0f, -0.16f * SNR_adj_dB ); + gain_add = (silk_float)pow( 2.0f, 0.16f * MIN_QGAIN_DB ); + for( k = 0; k < psEnc->sCmn.nb_subfr; k++ ) { + psEncCtrl->Gains[ k ] *= gain_mult; + psEncCtrl->Gains[ k ] += gain_add; + } + + /************************************************/ + /* Control low-frequency shaping and noise tilt */ + /************************************************/ + /* Less low frequency shaping for noisy inputs */ + strength = LOW_FREQ_SHAPING * ( 1.0f + LOW_QUALITY_LOW_FREQ_SHAPING_DECR * ( psEnc->sCmn.input_quality_bands_Q15[ 0 ] * ( 1.0f / 32768.0f ) - 1.0f ) ); + strength *= psEnc->sCmn.speech_activity_Q8 * ( 1.0f / 256.0f ); + if( psEnc->sCmn.indices.signalType == TYPE_VOICED ) { + /* Reduce low frequencies quantization noise for periodic signals, depending on pitch lag */ + /*f = 400; freqz([1, -0.98 + 2e-4 * f], [1, -0.97 + 7e-4 * f], 2^12, Fs); axis([0, 1000, -10, 1])*/ + for( k = 0; k < psEnc->sCmn.nb_subfr; k++ ) { + b = 0.2f / psEnc->sCmn.fs_kHz + 3.0f / psEncCtrl->pitchL[ k ]; + psEncCtrl->LF_MA_shp[ k ] = -1.0f + b; + psEncCtrl->LF_AR_shp[ k ] = 1.0f - b - b * strength; + } + Tilt = - HP_NOISE_COEF - + (1 - HP_NOISE_COEF) * HARM_HP_NOISE_COEF * psEnc->sCmn.speech_activity_Q8 * ( 1.0f / 256.0f ); + } else { + b = 1.3f / psEnc->sCmn.fs_kHz; + psEncCtrl->LF_MA_shp[ 0 ] = -1.0f + b; + psEncCtrl->LF_AR_shp[ 0 ] = 1.0f - b - b * strength * 0.6f; + for( k = 1; k < psEnc->sCmn.nb_subfr; k++ ) { + psEncCtrl->LF_MA_shp[ k ] = psEncCtrl->LF_MA_shp[ 0 ]; + psEncCtrl->LF_AR_shp[ k ] = psEncCtrl->LF_AR_shp[ 0 ]; + } + Tilt = -HP_NOISE_COEF; + } + + /****************************/ + /* HARMONIC SHAPING CONTROL */ + /****************************/ + if( USE_HARM_SHAPING && psEnc->sCmn.indices.signalType == TYPE_VOICED ) { + /* Harmonic noise shaping */ + HarmShapeGain = HARMONIC_SHAPING; + + /* More harmonic noise shaping for high bitrates or noisy input */ + HarmShapeGain += HIGH_RATE_OR_LOW_QUALITY_HARMONIC_SHAPING * + ( 1.0f - ( 1.0f - psEncCtrl->coding_quality ) * psEncCtrl->input_quality ); + + /* Less harmonic noise shaping for less periodic signals */ + HarmShapeGain *= ( silk_float )sqrt( psEnc->LTPCorr ); + } else { + HarmShapeGain = 0.0f; + } + + /*************************/ + /* Smooth over subframes */ + /*************************/ + for( k = 0; k < psEnc->sCmn.nb_subfr; k++ ) { + psShapeSt->HarmShapeGain_smth += SUBFR_SMTH_COEF * ( HarmShapeGain - psShapeSt->HarmShapeGain_smth ); + psEncCtrl->HarmShapeGain[ k ] = psShapeSt->HarmShapeGain_smth; + psShapeSt->Tilt_smth += SUBFR_SMTH_COEF * ( Tilt - psShapeSt->Tilt_smth ); + psEncCtrl->Tilt[ k ] = psShapeSt->Tilt_smth; + } +} diff --git a/port/third_party/opus/silk/float/pitch_analysis_core_FLP.c b/port/third_party/opus/silk/float/pitch_analysis_core_FLP.c new file mode 100644 index 00000000..0530a883 --- /dev/null +++ b/port/third_party/opus/silk/float/pitch_analysis_core_FLP.c @@ -0,0 +1,630 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/***************************************************************************** +* Pitch analyser function +******************************************************************************/ +#include "SigProc_FLP.h" +#include "SigProc_FIX.h" +#include "pitch_est_defines.h" +#include "pitch.h" + +#define SCRATCH_SIZE 22 + +/************************************************************/ +/* Internally used functions */ +/************************************************************/ +static void silk_P_Ana_calc_corr_st3( + silk_float cross_corr_st3[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE3_MAX ][ PE_NB_STAGE3_LAGS ], /* O 3 DIM correlation array */ + const silk_float frame[], /* I vector to correlate */ + opus_int start_lag, /* I start lag */ + opus_int sf_length, /* I sub frame length */ + opus_int nb_subfr, /* I number of subframes */ + opus_int complexity, /* I Complexity setting */ + int arch /* I Run-time architecture */ +); + +static void silk_P_Ana_calc_energy_st3( + silk_float energies_st3[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE3_MAX ][ PE_NB_STAGE3_LAGS ], /* O 3 DIM correlation array */ + const silk_float frame[], /* I vector to correlate */ + opus_int start_lag, /* I start lag */ + opus_int sf_length, /* I sub frame length */ + opus_int nb_subfr, /* I number of subframes */ + opus_int complexity /* I Complexity setting */ +); + +/************************************************************/ +/* CORE PITCH ANALYSIS FUNCTION */ +/************************************************************/ +opus_int silk_pitch_analysis_core_FLP( /* O Voicing estimate: 0 voiced, 1 unvoiced */ + const silk_float *frame, /* I Signal of length PE_FRAME_LENGTH_MS*Fs_kHz */ + opus_int *pitch_out, /* O Pitch lag values [nb_subfr] */ + opus_int16 *lagIndex, /* O Lag Index */ + opus_int8 *contourIndex, /* O Pitch contour Index */ + silk_float *LTPCorr, /* I/O Normalized correlation; input: value from previous frame */ + opus_int prevLag, /* I Last lag of previous frame; set to zero is unvoiced */ + const silk_float search_thres1, /* I First stage threshold for lag candidates 0 - 1 */ + const silk_float search_thres2, /* I Final threshold for lag candidates 0 - 1 */ + const opus_int Fs_kHz, /* I sample frequency (kHz) */ + const opus_int complexity, /* I Complexity setting, 0-2, where 2 is highest */ + const opus_int nb_subfr, /* I Number of 5 ms subframes */ + int arch /* I Run-time architecture */ +) +{ + opus_int i, k, d, j; + silk_float frame_8kHz[ PE_MAX_FRAME_LENGTH_MS * 8 ]; + silk_float frame_4kHz[ PE_MAX_FRAME_LENGTH_MS * 4 ]; + opus_int16 frame_8_FIX[ PE_MAX_FRAME_LENGTH_MS * 8 ]; + opus_int16 frame_4_FIX[ PE_MAX_FRAME_LENGTH_MS * 4 ]; + opus_int32 filt_state[ 6 ]; + silk_float threshold, contour_bias; + silk_float C[ PE_MAX_NB_SUBFR][ (PE_MAX_LAG >> 1) + 5 ]; + opus_val32 xcorr[ PE_MAX_LAG_MS * 4 - PE_MIN_LAG_MS * 4 + 1 ]; + silk_float CC[ PE_NB_CBKS_STAGE2_EXT ]; + const silk_float *target_ptr, *basis_ptr; + double cross_corr, normalizer, energy, energy_tmp; + opus_int d_srch[ PE_D_SRCH_LENGTH ]; + opus_int16 d_comp[ (PE_MAX_LAG >> 1) + 5 ]; + opus_int length_d_srch, length_d_comp; + silk_float Cmax, CCmax, CCmax_b, CCmax_new_b, CCmax_new; + opus_int CBimax, CBimax_new, lag, start_lag, end_lag, lag_new; + opus_int cbk_size; + silk_float lag_log2, prevLag_log2, delta_lag_log2_sqr; + silk_float energies_st3[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE3_MAX ][ PE_NB_STAGE3_LAGS ]; + silk_float cross_corr_st3[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE3_MAX ][ PE_NB_STAGE3_LAGS ]; + opus_int lag_counter; + opus_int frame_length, frame_length_8kHz, frame_length_4kHz; + opus_int sf_length, sf_length_8kHz, sf_length_4kHz; + opus_int min_lag, min_lag_8kHz, min_lag_4kHz; + opus_int max_lag, max_lag_8kHz, max_lag_4kHz; + opus_int nb_cbk_search; + const opus_int8 *Lag_CB_ptr; + + /* Check for valid sampling frequency */ + celt_assert( Fs_kHz == 8 || Fs_kHz == 12 || Fs_kHz == 16 ); + + /* Check for valid complexity setting */ + celt_assert( complexity >= SILK_PE_MIN_COMPLEX ); + celt_assert( complexity <= SILK_PE_MAX_COMPLEX ); + + silk_assert( search_thres1 >= 0.0f && search_thres1 <= 1.0f ); + silk_assert( search_thres2 >= 0.0f && search_thres2 <= 1.0f ); + + /* Set up frame lengths max / min lag for the sampling frequency */ + frame_length = ( PE_LTP_MEM_LENGTH_MS + nb_subfr * PE_SUBFR_LENGTH_MS ) * Fs_kHz; + frame_length_4kHz = ( PE_LTP_MEM_LENGTH_MS + nb_subfr * PE_SUBFR_LENGTH_MS ) * 4; + frame_length_8kHz = ( PE_LTP_MEM_LENGTH_MS + nb_subfr * PE_SUBFR_LENGTH_MS ) * 8; + sf_length = PE_SUBFR_LENGTH_MS * Fs_kHz; + sf_length_4kHz = PE_SUBFR_LENGTH_MS * 4; + sf_length_8kHz = PE_SUBFR_LENGTH_MS * 8; + min_lag = PE_MIN_LAG_MS * Fs_kHz; + min_lag_4kHz = PE_MIN_LAG_MS * 4; + min_lag_8kHz = PE_MIN_LAG_MS * 8; + max_lag = PE_MAX_LAG_MS * Fs_kHz - 1; + max_lag_4kHz = PE_MAX_LAG_MS * 4; + max_lag_8kHz = PE_MAX_LAG_MS * 8 - 1; + + /* Resample from input sampled at Fs_kHz to 8 kHz */ + if( Fs_kHz == 16 ) { + /* Resample to 16 -> 8 khz */ + opus_int16 frame_16_FIX[ 16 * PE_MAX_FRAME_LENGTH_MS ]; + silk_float2short_array( frame_16_FIX, frame, frame_length ); + silk_memset( filt_state, 0, 2 * sizeof( opus_int32 ) ); + silk_resampler_down2( filt_state, frame_8_FIX, frame_16_FIX, frame_length ); + silk_short2float_array( frame_8kHz, frame_8_FIX, frame_length_8kHz ); + } else if( Fs_kHz == 12 ) { + /* Resample to 12 -> 8 khz */ + opus_int16 frame_12_FIX[ 12 * PE_MAX_FRAME_LENGTH_MS ]; + silk_float2short_array( frame_12_FIX, frame, frame_length ); + silk_memset( filt_state, 0, 6 * sizeof( opus_int32 ) ); + silk_resampler_down2_3( filt_state, frame_8_FIX, frame_12_FIX, frame_length ); + silk_short2float_array( frame_8kHz, frame_8_FIX, frame_length_8kHz ); + } else { + celt_assert( Fs_kHz == 8 ); + silk_float2short_array( frame_8_FIX, frame, frame_length_8kHz ); + } + + /* Decimate again to 4 kHz */ + silk_memset( filt_state, 0, 2 * sizeof( opus_int32 ) ); + silk_resampler_down2( filt_state, frame_4_FIX, frame_8_FIX, frame_length_8kHz ); + silk_short2float_array( frame_4kHz, frame_4_FIX, frame_length_4kHz ); + + /* Low-pass filter */ + for( i = frame_length_4kHz - 1; i > 0; i-- ) { + frame_4kHz[ i ] = silk_ADD_SAT16( frame_4kHz[ i ], frame_4kHz[ i - 1 ] ); + } + + /****************************************************************************** + * FIRST STAGE, operating in 4 khz + ******************************************************************************/ + silk_memset(C, 0, sizeof(silk_float) * nb_subfr * ((PE_MAX_LAG >> 1) + 5)); + target_ptr = &frame_4kHz[ silk_LSHIFT( sf_length_4kHz, 2 ) ]; + for( k = 0; k < nb_subfr >> 1; k++ ) { + /* Check that we are within range of the array */ + celt_assert( target_ptr >= frame_4kHz ); + celt_assert( target_ptr + sf_length_8kHz <= frame_4kHz + frame_length_4kHz ); + + basis_ptr = target_ptr - min_lag_4kHz; + + /* Check that we are within range of the array */ + celt_assert( basis_ptr >= frame_4kHz ); + celt_assert( basis_ptr + sf_length_8kHz <= frame_4kHz + frame_length_4kHz ); + + celt_pitch_xcorr( target_ptr, target_ptr-max_lag_4kHz, xcorr, sf_length_8kHz, max_lag_4kHz - min_lag_4kHz + 1, arch ); + + /* Calculate first vector products before loop */ + cross_corr = xcorr[ max_lag_4kHz - min_lag_4kHz ]; + normalizer = silk_energy_FLP( target_ptr, sf_length_8kHz ) + + silk_energy_FLP( basis_ptr, sf_length_8kHz ) + + sf_length_8kHz * 4000.0f; + + C[ 0 ][ min_lag_4kHz ] += (silk_float)( 2 * cross_corr / normalizer ); + + /* From now on normalizer is computed recursively */ + for( d = min_lag_4kHz + 1; d <= max_lag_4kHz; d++ ) { + basis_ptr--; + + /* Check that we are within range of the array */ + silk_assert( basis_ptr >= frame_4kHz ); + silk_assert( basis_ptr + sf_length_8kHz <= frame_4kHz + frame_length_4kHz ); + + cross_corr = xcorr[ max_lag_4kHz - d ]; + + /* Add contribution of new sample and remove contribution from oldest sample */ + normalizer += + basis_ptr[ 0 ] * (double)basis_ptr[ 0 ] - + basis_ptr[ sf_length_8kHz ] * (double)basis_ptr[ sf_length_8kHz ]; + C[ 0 ][ d ] += (silk_float)( 2 * cross_corr / normalizer ); + } + /* Update target pointer */ + target_ptr += sf_length_8kHz; + } + + /* Apply short-lag bias */ + for( i = max_lag_4kHz; i >= min_lag_4kHz; i-- ) { + C[ 0 ][ i ] -= C[ 0 ][ i ] * i / 4096.0f; + } + + /* Sort */ + length_d_srch = 4 + 2 * complexity; + celt_assert( 3 * length_d_srch <= PE_D_SRCH_LENGTH ); + silk_insertion_sort_decreasing_FLP( &C[ 0 ][ min_lag_4kHz ], d_srch, max_lag_4kHz - min_lag_4kHz + 1, length_d_srch ); + + /* Escape if correlation is very low already here */ + Cmax = C[ 0 ][ min_lag_4kHz ]; + if( Cmax < 0.2f ) { + silk_memset( pitch_out, 0, nb_subfr * sizeof( opus_int ) ); + *LTPCorr = 0.0f; + *lagIndex = 0; + *contourIndex = 0; + return 1; + } + + threshold = search_thres1 * Cmax; + for( i = 0; i < length_d_srch; i++ ) { + /* Convert to 8 kHz indices for the sorted correlation that exceeds the threshold */ + if( C[ 0 ][ min_lag_4kHz + i ] > threshold ) { + d_srch[ i ] = silk_LSHIFT( d_srch[ i ] + min_lag_4kHz, 1 ); + } else { + length_d_srch = i; + break; + } + } + celt_assert( length_d_srch > 0 ); + + for( i = min_lag_8kHz - 5; i < max_lag_8kHz + 5; i++ ) { + d_comp[ i ] = 0; + } + for( i = 0; i < length_d_srch; i++ ) { + d_comp[ d_srch[ i ] ] = 1; + } + + /* Convolution */ + for( i = max_lag_8kHz + 3; i >= min_lag_8kHz; i-- ) { + d_comp[ i ] += d_comp[ i - 1 ] + d_comp[ i - 2 ]; + } + + length_d_srch = 0; + for( i = min_lag_8kHz; i < max_lag_8kHz + 1; i++ ) { + if( d_comp[ i + 1 ] > 0 ) { + d_srch[ length_d_srch ] = i; + length_d_srch++; + } + } + + /* Convolution */ + for( i = max_lag_8kHz + 3; i >= min_lag_8kHz; i-- ) { + d_comp[ i ] += d_comp[ i - 1 ] + d_comp[ i - 2 ] + d_comp[ i - 3 ]; + } + + length_d_comp = 0; + for( i = min_lag_8kHz; i < max_lag_8kHz + 4; i++ ) { + if( d_comp[ i ] > 0 ) { + d_comp[ length_d_comp ] = (opus_int16)( i - 2 ); + length_d_comp++; + } + } + + /********************************************************************************** + ** SECOND STAGE, operating at 8 kHz, on lag sections with high correlation + *************************************************************************************/ + /********************************************************************************* + * Find energy of each subframe projected onto its history, for a range of delays + *********************************************************************************/ + silk_memset( C, 0, PE_MAX_NB_SUBFR*((PE_MAX_LAG >> 1) + 5) * sizeof(silk_float)); + + if( Fs_kHz == 8 ) { + target_ptr = &frame[ PE_LTP_MEM_LENGTH_MS * 8 ]; + } else { + target_ptr = &frame_8kHz[ PE_LTP_MEM_LENGTH_MS * 8 ]; + } + for( k = 0; k < nb_subfr; k++ ) { + energy_tmp = silk_energy_FLP( target_ptr, sf_length_8kHz ) + 1.0; + for( j = 0; j < length_d_comp; j++ ) { + d = d_comp[ j ]; + basis_ptr = target_ptr - d; + cross_corr = silk_inner_product_FLP( basis_ptr, target_ptr, sf_length_8kHz, arch ); + if( cross_corr > 0.0f ) { + energy = silk_energy_FLP( basis_ptr, sf_length_8kHz ); + C[ k ][ d ] = (silk_float)( 2 * cross_corr / ( energy + energy_tmp ) ); + } else { + C[ k ][ d ] = 0.0f; + } + } + target_ptr += sf_length_8kHz; + } + + /* search over lag range and lags codebook */ + /* scale factor for lag codebook, as a function of center lag */ + + CCmax = 0.0f; /* This value doesn't matter */ + CCmax_b = -1000.0f; + + CBimax = 0; /* To avoid returning undefined lag values */ + lag = -1; /* To check if lag with strong enough correlation has been found */ + + if( prevLag > 0 ) { + if( Fs_kHz == 12 ) { + prevLag = silk_LSHIFT( prevLag, 1 ) / 3; + } else if( Fs_kHz == 16 ) { + prevLag = silk_RSHIFT( prevLag, 1 ); + } + prevLag_log2 = silk_log2( (silk_float)prevLag ); + } else { + prevLag_log2 = 0; + } + + /* Set up stage 2 codebook based on number of subframes */ + if( nb_subfr == PE_MAX_NB_SUBFR ) { + cbk_size = PE_NB_CBKS_STAGE2_EXT; + Lag_CB_ptr = &silk_CB_lags_stage2[ 0 ][ 0 ]; + if( Fs_kHz == 8 && complexity > SILK_PE_MIN_COMPLEX ) { + /* If input is 8 khz use a larger codebook here because it is last stage */ + nb_cbk_search = PE_NB_CBKS_STAGE2_EXT; + } else { + nb_cbk_search = PE_NB_CBKS_STAGE2; + } + } else { + cbk_size = PE_NB_CBKS_STAGE2_10MS; + Lag_CB_ptr = &silk_CB_lags_stage2_10_ms[ 0 ][ 0 ]; + nb_cbk_search = PE_NB_CBKS_STAGE2_10MS; + } + + for( k = 0; k < length_d_srch; k++ ) { + d = d_srch[ k ]; + for( j = 0; j < nb_cbk_search; j++ ) { + CC[j] = 0.0f; + for( i = 0; i < nb_subfr; i++ ) { + /* Try all codebooks */ + CC[ j ] += C[ i ][ d + matrix_ptr( Lag_CB_ptr, i, j, cbk_size )]; + } + } + /* Find best codebook */ + CCmax_new = -1000.0f; + CBimax_new = 0; + for( i = 0; i < nb_cbk_search; i++ ) { + if( CC[ i ] > CCmax_new ) { + CCmax_new = CC[ i ]; + CBimax_new = i; + } + } + + /* Bias towards shorter lags */ + lag_log2 = silk_log2( (silk_float)d ); + CCmax_new_b = CCmax_new - PE_SHORTLAG_BIAS * nb_subfr * lag_log2; + + /* Bias towards previous lag */ + if( prevLag > 0 ) { + delta_lag_log2_sqr = lag_log2 - prevLag_log2; + delta_lag_log2_sqr *= delta_lag_log2_sqr; + CCmax_new_b -= PE_PREVLAG_BIAS * nb_subfr * (*LTPCorr) * delta_lag_log2_sqr / ( delta_lag_log2_sqr + 0.5f ); + } + + if( CCmax_new_b > CCmax_b && /* Find maximum biased correlation */ + CCmax_new > nb_subfr * search_thres2 /* Correlation needs to be high enough to be voiced */ + ) { + CCmax_b = CCmax_new_b; + CCmax = CCmax_new; + lag = d; + CBimax = CBimax_new; + } + } + + if( lag == -1 ) { + /* No suitable candidate found */ + silk_memset( pitch_out, 0, PE_MAX_NB_SUBFR * sizeof(opus_int) ); + *LTPCorr = 0.0f; + *lagIndex = 0; + *contourIndex = 0; + return 1; + } + + /* Output normalized correlation */ + *LTPCorr = (silk_float)( CCmax / nb_subfr ); + silk_assert( *LTPCorr >= 0.0f ); + + if( Fs_kHz > 8 ) { + /* Search in original signal */ + + /* Compensate for decimation */ + silk_assert( lag == silk_SAT16( lag ) ); + if( Fs_kHz == 12 ) { + lag = silk_RSHIFT_ROUND( silk_SMULBB( lag, 3 ), 1 ); + } else { /* Fs_kHz == 16 */ + lag = silk_LSHIFT( lag, 1 ); + } + + lag = silk_LIMIT_int( lag, min_lag, max_lag ); + start_lag = silk_max_int( lag - 2, min_lag ); + end_lag = silk_min_int( lag + 2, max_lag ); + lag_new = lag; /* to avoid undefined lag */ + CBimax = 0; /* to avoid undefined lag */ + + CCmax = -1000.0f; + + /* Calculate the correlations and energies needed in stage 3 */ + silk_P_Ana_calc_corr_st3( cross_corr_st3, frame, start_lag, sf_length, nb_subfr, complexity, arch ); + silk_P_Ana_calc_energy_st3( energies_st3, frame, start_lag, sf_length, nb_subfr, complexity ); + + lag_counter = 0; + silk_assert( lag == silk_SAT16( lag ) ); + contour_bias = PE_FLATCONTOUR_BIAS / lag; + + /* Set up cbk parameters according to complexity setting and frame length */ + if( nb_subfr == PE_MAX_NB_SUBFR ) { + nb_cbk_search = (opus_int)silk_nb_cbk_searchs_stage3[ complexity ]; + cbk_size = PE_NB_CBKS_STAGE3_MAX; + Lag_CB_ptr = &silk_CB_lags_stage3[ 0 ][ 0 ]; + } else { + nb_cbk_search = PE_NB_CBKS_STAGE3_10MS; + cbk_size = PE_NB_CBKS_STAGE3_10MS; + Lag_CB_ptr = &silk_CB_lags_stage3_10_ms[ 0 ][ 0 ]; + } + + target_ptr = &frame[ PE_LTP_MEM_LENGTH_MS * Fs_kHz ]; + energy_tmp = silk_energy_FLP( target_ptr, nb_subfr * sf_length ) + 1.0; + for( d = start_lag; d <= end_lag; d++ ) { + for( j = 0; j < nb_cbk_search; j++ ) { + cross_corr = 0.0; + energy = energy_tmp; + for( k = 0; k < nb_subfr; k++ ) { + cross_corr += cross_corr_st3[ k ][ j ][ lag_counter ]; + energy += energies_st3[ k ][ j ][ lag_counter ]; + } + if( cross_corr > 0.0 ) { + CCmax_new = (silk_float)( 2 * cross_corr / energy ); + /* Reduce depending on flatness of contour */ + CCmax_new *= 1.0f - contour_bias * j; + } else { + CCmax_new = 0.0f; + } + + if( CCmax_new > CCmax && ( d + (opus_int)silk_CB_lags_stage3[ 0 ][ j ] ) <= max_lag ) { + CCmax = CCmax_new; + lag_new = d; + CBimax = j; + } + } + lag_counter++; + } + + for( k = 0; k < nb_subfr; k++ ) { + pitch_out[ k ] = lag_new + matrix_ptr( Lag_CB_ptr, k, CBimax, cbk_size ); + pitch_out[ k ] = silk_LIMIT( pitch_out[ k ], min_lag, PE_MAX_LAG_MS * Fs_kHz ); + } + *lagIndex = (opus_int16)( lag_new - min_lag ); + *contourIndex = (opus_int8)CBimax; + } else { /* Fs_kHz == 8 */ + /* Save Lags */ + for( k = 0; k < nb_subfr; k++ ) { + pitch_out[ k ] = lag + matrix_ptr( Lag_CB_ptr, k, CBimax, cbk_size ); + pitch_out[ k ] = silk_LIMIT( pitch_out[ k ], min_lag_8kHz, PE_MAX_LAG_MS * 8 ); + } + *lagIndex = (opus_int16)( lag - min_lag_8kHz ); + *contourIndex = (opus_int8)CBimax; + } + celt_assert( *lagIndex >= 0 ); + /* return as voiced */ + return 0; +} + +/*********************************************************************** + * Calculates the correlations used in stage 3 search. In order to cover + * the whole lag codebook for all the searched offset lags (lag +- 2), + * the following correlations are needed in each sub frame: + * + * sf1: lag range [-8,...,7] total 16 correlations + * sf2: lag range [-4,...,4] total 9 correlations + * sf3: lag range [-3,....4] total 8 correltions + * sf4: lag range [-6,....8] total 15 correlations + * + * In total 48 correlations. The direct implementation computed in worst + * case 4*12*5 = 240 correlations, but more likely around 120. + ***********************************************************************/ +static void silk_P_Ana_calc_corr_st3( + silk_float cross_corr_st3[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE3_MAX ][ PE_NB_STAGE3_LAGS ], /* O 3 DIM correlation array */ + const silk_float frame[], /* I vector to correlate */ + opus_int start_lag, /* I start lag */ + opus_int sf_length, /* I sub frame length */ + opus_int nb_subfr, /* I number of subframes */ + opus_int complexity, /* I Complexity setting */ + int arch /* I Run-time architecture */ +) +{ + const silk_float *target_ptr; + opus_int i, j, k, lag_counter, lag_low, lag_high; + opus_int nb_cbk_search, delta, idx, cbk_size; + silk_float scratch_mem[ SCRATCH_SIZE ]; + opus_val32 xcorr[ SCRATCH_SIZE ]; + const opus_int8 *Lag_range_ptr, *Lag_CB_ptr; + + celt_assert( complexity >= SILK_PE_MIN_COMPLEX ); + celt_assert( complexity <= SILK_PE_MAX_COMPLEX ); + + if( nb_subfr == PE_MAX_NB_SUBFR ) { + Lag_range_ptr = &silk_Lag_range_stage3[ complexity ][ 0 ][ 0 ]; + Lag_CB_ptr = &silk_CB_lags_stage3[ 0 ][ 0 ]; + nb_cbk_search = silk_nb_cbk_searchs_stage3[ complexity ]; + cbk_size = PE_NB_CBKS_STAGE3_MAX; + } else { + celt_assert( nb_subfr == PE_MAX_NB_SUBFR >> 1); + Lag_range_ptr = &silk_Lag_range_stage3_10_ms[ 0 ][ 0 ]; + Lag_CB_ptr = &silk_CB_lags_stage3_10_ms[ 0 ][ 0 ]; + nb_cbk_search = PE_NB_CBKS_STAGE3_10MS; + cbk_size = PE_NB_CBKS_STAGE3_10MS; + } + + target_ptr = &frame[ silk_LSHIFT( sf_length, 2 ) ]; /* Pointer to middle of frame */ + for( k = 0; k < nb_subfr; k++ ) { + lag_counter = 0; + + /* Calculate the correlations for each subframe */ + lag_low = matrix_ptr( Lag_range_ptr, k, 0, 2 ); + lag_high = matrix_ptr( Lag_range_ptr, k, 1, 2 ); + silk_assert(lag_high-lag_low+1 <= SCRATCH_SIZE); + celt_pitch_xcorr( target_ptr, target_ptr - start_lag - lag_high, xcorr, sf_length, lag_high - lag_low + 1, arch ); + for( j = lag_low; j <= lag_high; j++ ) { + silk_assert( lag_counter < SCRATCH_SIZE ); + scratch_mem[ lag_counter ] = xcorr[ lag_high - j ]; + lag_counter++; + } + + delta = matrix_ptr( Lag_range_ptr, k, 0, 2 ); + for( i = 0; i < nb_cbk_search; i++ ) { + /* Fill out the 3 dim array that stores the correlations for */ + /* each code_book vector for each start lag */ + idx = matrix_ptr( Lag_CB_ptr, k, i, cbk_size ) - delta; + for( j = 0; j < PE_NB_STAGE3_LAGS; j++ ) { + silk_assert( idx + j < SCRATCH_SIZE ); + silk_assert( idx + j < lag_counter ); + cross_corr_st3[ k ][ i ][ j ] = scratch_mem[ idx + j ]; + } + } + target_ptr += sf_length; + } +} + +/********************************************************************/ +/* Calculate the energies for first two subframes. The energies are */ +/* calculated recursively. */ +/********************************************************************/ +static void silk_P_Ana_calc_energy_st3( + silk_float energies_st3[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE3_MAX ][ PE_NB_STAGE3_LAGS ], /* O 3 DIM correlation array */ + const silk_float frame[], /* I vector to correlate */ + opus_int start_lag, /* I start lag */ + opus_int sf_length, /* I sub frame length */ + opus_int nb_subfr, /* I number of subframes */ + opus_int complexity /* I Complexity setting */ +) +{ + const silk_float *target_ptr, *basis_ptr; + double energy; + opus_int k, i, j, lag_counter; + opus_int nb_cbk_search, delta, idx, cbk_size, lag_diff; + silk_float scratch_mem[ SCRATCH_SIZE ]; + const opus_int8 *Lag_range_ptr, *Lag_CB_ptr; + + celt_assert( complexity >= SILK_PE_MIN_COMPLEX ); + celt_assert( complexity <= SILK_PE_MAX_COMPLEX ); + + if( nb_subfr == PE_MAX_NB_SUBFR ) { + Lag_range_ptr = &silk_Lag_range_stage3[ complexity ][ 0 ][ 0 ]; + Lag_CB_ptr = &silk_CB_lags_stage3[ 0 ][ 0 ]; + nb_cbk_search = silk_nb_cbk_searchs_stage3[ complexity ]; + cbk_size = PE_NB_CBKS_STAGE3_MAX; + } else { + celt_assert( nb_subfr == PE_MAX_NB_SUBFR >> 1); + Lag_range_ptr = &silk_Lag_range_stage3_10_ms[ 0 ][ 0 ]; + Lag_CB_ptr = &silk_CB_lags_stage3_10_ms[ 0 ][ 0 ]; + nb_cbk_search = PE_NB_CBKS_STAGE3_10MS; + cbk_size = PE_NB_CBKS_STAGE3_10MS; + } + + target_ptr = &frame[ silk_LSHIFT( sf_length, 2 ) ]; + for( k = 0; k < nb_subfr; k++ ) { + lag_counter = 0; + + /* Calculate the energy for first lag */ + basis_ptr = target_ptr - ( start_lag + matrix_ptr( Lag_range_ptr, k, 0, 2 ) ); + energy = silk_energy_FLP( basis_ptr, sf_length ) + 1e-3; + silk_assert( energy >= 0.0 ); + scratch_mem[lag_counter] = (silk_float)energy; + lag_counter++; + + lag_diff = ( matrix_ptr( Lag_range_ptr, k, 1, 2 ) - matrix_ptr( Lag_range_ptr, k, 0, 2 ) + 1 ); + for( i = 1; i < lag_diff; i++ ) { + /* remove part outside new window */ + energy -= basis_ptr[sf_length - i] * (double)basis_ptr[sf_length - i]; + silk_assert( energy >= 0.0 ); + + /* add part that comes into window */ + energy += basis_ptr[ -i ] * (double)basis_ptr[ -i ]; + silk_assert( energy >= 0.0 ); + silk_assert( lag_counter < SCRATCH_SIZE ); + scratch_mem[lag_counter] = (silk_float)energy; + lag_counter++; + } + + delta = matrix_ptr( Lag_range_ptr, k, 0, 2 ); + for( i = 0; i < nb_cbk_search; i++ ) { + /* Fill out the 3 dim array that stores the correlations for */ + /* each code_book vector for each start lag */ + idx = matrix_ptr( Lag_CB_ptr, k, i, cbk_size ) - delta; + for( j = 0; j < PE_NB_STAGE3_LAGS; j++ ) { + silk_assert( idx + j < SCRATCH_SIZE ); + silk_assert( idx + j < lag_counter ); + energies_st3[ k ][ i ][ j ] = scratch_mem[ idx + j ]; + silk_assert( energies_st3[ k ][ i ][ j ] >= 0.0f ); + } + } + target_ptr += sf_length; + } +} diff --git a/port/third_party/opus/silk/float/process_gains_FLP.c b/port/third_party/opus/silk/float/process_gains_FLP.c new file mode 100644 index 00000000..c0da0dae --- /dev/null +++ b/port/third_party/opus/silk/float/process_gains_FLP.c @@ -0,0 +1,103 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" +#include "tuning_parameters.h" + +/* Processing of gains */ +void silk_process_gains_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + opus_int condCoding /* I The type of conditional coding to use */ +) +{ + silk_shape_state_FLP *psShapeSt = &psEnc->sShape; + opus_int k; + opus_int32 pGains_Q16[ MAX_NB_SUBFR ]; + silk_float s, InvMaxSqrVal, gain, quant_offset; + + /* Gain reduction when LTP coding gain is high */ + if( psEnc->sCmn.indices.signalType == TYPE_VOICED ) { + s = 1.0f - 0.5f * silk_sigmoid( 0.25f * ( psEncCtrl->LTPredCodGain - 12.0f ) ); + for( k = 0; k < psEnc->sCmn.nb_subfr; k++ ) { + psEncCtrl->Gains[ k ] *= s; + } + } + + /* Limit the quantized signal */ + InvMaxSqrVal = ( silk_float )( pow( 2.0f, 0.33f * ( 21.0f - psEnc->sCmn.SNR_dB_Q7 * ( 1 / 128.0f ) ) ) / psEnc->sCmn.subfr_length ); + + for( k = 0; k < psEnc->sCmn.nb_subfr; k++ ) { + /* Soft limit on ratio residual energy and squared gains */ + gain = psEncCtrl->Gains[ k ]; + gain = ( silk_float )sqrt( gain * gain + psEncCtrl->ResNrg[ k ] * InvMaxSqrVal ); + psEncCtrl->Gains[ k ] = silk_min_float( gain, 32767.0f ); + } + + /* Prepare gains for noise shaping quantization */ + for( k = 0; k < psEnc->sCmn.nb_subfr; k++ ) { + pGains_Q16[ k ] = (opus_int32)( psEncCtrl->Gains[ k ] * 65536.0f ); + } + + /* Save unquantized gains and gain Index */ + silk_memcpy( psEncCtrl->GainsUnq_Q16, pGains_Q16, psEnc->sCmn.nb_subfr * sizeof( opus_int32 ) ); + psEncCtrl->lastGainIndexPrev = psShapeSt->LastGainIndex; + + /* Quantize gains */ + silk_gains_quant( psEnc->sCmn.indices.GainsIndices, pGains_Q16, + &psShapeSt->LastGainIndex, condCoding == CODE_CONDITIONALLY, psEnc->sCmn.nb_subfr ); + + /* Overwrite unquantized gains with quantized gains and convert back to Q0 from Q16 */ + for( k = 0; k < psEnc->sCmn.nb_subfr; k++ ) { + psEncCtrl->Gains[ k ] = pGains_Q16[ k ] / 65536.0f; + } + + /* Set quantizer offset for voiced signals. Larger offset when LTP coding gain is low or tilt is high (ie low-pass) */ + if( psEnc->sCmn.indices.signalType == TYPE_VOICED ) { + if( psEncCtrl->LTPredCodGain + psEnc->sCmn.input_tilt_Q15 * ( 1.0f / 32768.0f ) > 1.0f ) { + psEnc->sCmn.indices.quantOffsetType = 0; + } else { + psEnc->sCmn.indices.quantOffsetType = 1; + } + } + + /* Quantizer boundary adjustment */ + quant_offset = silk_Quantization_Offsets_Q10[ psEnc->sCmn.indices.signalType >> 1 ][ psEnc->sCmn.indices.quantOffsetType ] / 1024.0f; + psEncCtrl->Lambda = LAMBDA_OFFSET + + LAMBDA_DELAYED_DECISIONS * psEnc->sCmn.nStatesDelayedDecision + + LAMBDA_SPEECH_ACT * psEnc->sCmn.speech_activity_Q8 * ( 1.0f / 256.0f ) + + LAMBDA_INPUT_QUALITY * psEncCtrl->input_quality + + LAMBDA_CODING_QUALITY * psEncCtrl->coding_quality + + LAMBDA_QUANT_OFFSET * quant_offset; + + silk_assert( psEncCtrl->Lambda > 0.0f ); + silk_assert( psEncCtrl->Lambda < 2.0f ); +} diff --git a/port/third_party/opus/silk/float/regularize_correlations_FLP.c b/port/third_party/opus/silk/float/regularize_correlations_FLP.c new file mode 100644 index 00000000..df461260 --- /dev/null +++ b/port/third_party/opus/silk/float/regularize_correlations_FLP.c @@ -0,0 +1,48 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" + +/* Add noise to matrix diagonal */ +void silk_regularize_correlations_FLP( + silk_float *XX, /* I/O Correlation matrices */ + silk_float *xx, /* I/O Correlation values */ + const silk_float noise, /* I Noise energy to add */ + const opus_int D /* I Dimension of XX */ +) +{ + opus_int i; + + for( i = 0; i < D; i++ ) { + matrix_ptr( &XX[ 0 ], i, i, D ) += noise; + } + xx[ 0 ] += noise; +} diff --git a/port/third_party/opus/silk/float/residual_energy_FLP.c b/port/third_party/opus/silk/float/residual_energy_FLP.c new file mode 100644 index 00000000..1bd07b33 --- /dev/null +++ b/port/third_party/opus/silk/float/residual_energy_FLP.c @@ -0,0 +1,117 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" + +#define MAX_ITERATIONS_RESIDUAL_NRG 10 +#define REGULARIZATION_FACTOR 1e-8f + +/* Residual energy: nrg = wxx - 2 * wXx * c + c' * wXX * c */ +silk_float silk_residual_energy_covar_FLP( /* O Weighted residual energy */ + const silk_float *c, /* I Filter coefficients */ + silk_float *wXX, /* I/O Weighted correlation matrix, reg. out */ + const silk_float *wXx, /* I Weighted correlation vector */ + const silk_float wxx, /* I Weighted correlation value */ + const opus_int D /* I Dimension */ +) +{ + opus_int i, j, k; + silk_float tmp, nrg = 0.0f, regularization; + + /* Safety checks */ + celt_assert( D >= 0 ); + + regularization = REGULARIZATION_FACTOR * ( wXX[ 0 ] + wXX[ D * D - 1 ] ); + for( k = 0; k < MAX_ITERATIONS_RESIDUAL_NRG; k++ ) { + nrg = wxx; + + tmp = 0.0f; + for( i = 0; i < D; i++ ) { + tmp += wXx[ i ] * c[ i ]; + } + nrg -= 2.0f * tmp; + + /* compute c' * wXX * c, assuming wXX is symmetric */ + for( i = 0; i < D; i++ ) { + tmp = 0.0f; + for( j = i + 1; j < D; j++ ) { + tmp += matrix_c_ptr( wXX, i, j, D ) * c[ j ]; + } + nrg += c[ i ] * ( 2.0f * tmp + matrix_c_ptr( wXX, i, i, D ) * c[ i ] ); + } + if( nrg > 0 ) { + break; + } else { + /* Add white noise */ + for( i = 0; i < D; i++ ) { + matrix_c_ptr( wXX, i, i, D ) += regularization; + } + /* Increase noise for next run */ + regularization *= 2.0f; + } + } + if( k == MAX_ITERATIONS_RESIDUAL_NRG ) { + silk_assert( nrg == 0 ); + nrg = 1.0f; + } + + return nrg; +} + +/* Calculates residual energies of input subframes where all subframes have LPC_order */ +/* of preceding samples */ +void silk_residual_energy_FLP( + silk_float nrgs[ MAX_NB_SUBFR ], /* O Residual energy per subframe */ + const silk_float x[], /* I Input signal */ + silk_float a[ 2 ][ MAX_LPC_ORDER ], /* I AR coefs for each frame half */ + const silk_float gains[], /* I Quantization gains */ + const opus_int subfr_length, /* I Subframe length */ + const opus_int nb_subfr, /* I number of subframes */ + const opus_int LPC_order /* I LPC order */ +) +{ + opus_int shift; + silk_float *LPC_res_ptr, LPC_res[ ( MAX_FRAME_LENGTH + MAX_NB_SUBFR * MAX_LPC_ORDER ) / 2 ]; + + LPC_res_ptr = LPC_res + LPC_order; + shift = LPC_order + subfr_length; + + /* Filter input to create the LPC residual for each frame half, and measure subframe energies */ + silk_LPC_analysis_filter_FLP( LPC_res, a[ 0 ], x + 0 * shift, 2 * shift, LPC_order ); + nrgs[ 0 ] = ( silk_float )( gains[ 0 ] * gains[ 0 ] * silk_energy_FLP( LPC_res_ptr + 0 * shift, subfr_length ) ); + nrgs[ 1 ] = ( silk_float )( gains[ 1 ] * gains[ 1 ] * silk_energy_FLP( LPC_res_ptr + 1 * shift, subfr_length ) ); + + if( nb_subfr == MAX_NB_SUBFR ) { + silk_LPC_analysis_filter_FLP( LPC_res, a[ 1 ], x + 2 * shift, 2 * shift, LPC_order ); + nrgs[ 2 ] = ( silk_float )( gains[ 2 ] * gains[ 2 ] * silk_energy_FLP( LPC_res_ptr + 0 * shift, subfr_length ) ); + nrgs[ 3 ] = ( silk_float )( gains[ 3 ] * gains[ 3 ] * silk_energy_FLP( LPC_res_ptr + 1 * shift, subfr_length ) ); + } +} diff --git a/port/third_party/opus/silk/float/scale_copy_vector_FLP.c b/port/third_party/opus/silk/float/scale_copy_vector_FLP.c new file mode 100644 index 00000000..20db32b3 --- /dev/null +++ b/port/third_party/opus/silk/float/scale_copy_vector_FLP.c @@ -0,0 +1,57 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FLP.h" + +/* copy and multiply a vector by a constant */ +void silk_scale_copy_vector_FLP( + silk_float *data_out, + const silk_float *data_in, + silk_float gain, + opus_int dataSize +) +{ + opus_int i, dataSize4; + + /* 4x unrolled loop */ + dataSize4 = dataSize & 0xFFFC; + for( i = 0; i < dataSize4; i += 4 ) { + data_out[ i + 0 ] = gain * data_in[ i + 0 ]; + data_out[ i + 1 ] = gain * data_in[ i + 1 ]; + data_out[ i + 2 ] = gain * data_in[ i + 2 ]; + data_out[ i + 3 ] = gain * data_in[ i + 3 ]; + } + + /* any remaining elements */ + for( ; i < dataSize; i++ ) { + data_out[ i ] = gain * data_in[ i ]; + } +} diff --git a/port/third_party/opus/silk/float/scale_vector_FLP.c b/port/third_party/opus/silk/float/scale_vector_FLP.c new file mode 100644 index 00000000..108fdcbe --- /dev/null +++ b/port/third_party/opus/silk/float/scale_vector_FLP.c @@ -0,0 +1,56 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FLP.h" + +/* multiply a vector by a constant */ +void silk_scale_vector_FLP( + silk_float *data1, + silk_float gain, + opus_int dataSize +) +{ + opus_int i, dataSize4; + + /* 4x unrolled loop */ + dataSize4 = dataSize & 0xFFFC; + for( i = 0; i < dataSize4; i += 4 ) { + data1[ i + 0 ] *= gain; + data1[ i + 1 ] *= gain; + data1[ i + 2 ] *= gain; + data1[ i + 3 ] *= gain; + } + + /* any remaining elements */ + for( ; i < dataSize; i++ ) { + data1[ i ] *= gain; + } +} diff --git a/port/third_party/opus/silk/float/schur_FLP.c b/port/third_party/opus/silk/float/schur_FLP.c new file mode 100644 index 00000000..8526c748 --- /dev/null +++ b/port/third_party/opus/silk/float/schur_FLP.c @@ -0,0 +1,70 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FLP.h" + +silk_float silk_schur_FLP( /* O returns residual energy */ + silk_float refl_coef[], /* O reflection coefficients (length order) */ + const silk_float auto_corr[], /* I autocorrelation sequence (length order+1) */ + opus_int order /* I order */ +) +{ + opus_int k, n; + double C[ SILK_MAX_ORDER_LPC + 1 ][ 2 ]; + double Ctmp1, Ctmp2, rc_tmp; + + celt_assert( order >= 0 && order <= SILK_MAX_ORDER_LPC ); + + /* Copy correlations */ + k = 0; + do { + C[ k ][ 0 ] = C[ k ][ 1 ] = auto_corr[ k ]; + } while( ++k <= order ); + + for( k = 0; k < order; k++ ) { + /* Get reflection coefficient */ + rc_tmp = -C[ k + 1 ][ 0 ] / silk_max_float( C[ 0 ][ 1 ], 1e-9f ); + + /* Save the output */ + refl_coef[ k ] = (silk_float)rc_tmp; + + /* Update correlations */ + for( n = 0; n < order - k; n++ ) { + Ctmp1 = C[ n + k + 1 ][ 0 ]; + Ctmp2 = C[ n ][ 1 ]; + C[ n + k + 1 ][ 0 ] = Ctmp1 + Ctmp2 * rc_tmp; + C[ n ][ 1 ] = Ctmp2 + Ctmp1 * rc_tmp; + } + } + + /* Return residual energy */ + return (silk_float)C[ 0 ][ 1 ]; +} diff --git a/port/third_party/opus/silk/float/sort_FLP.c b/port/third_party/opus/silk/float/sort_FLP.c new file mode 100644 index 00000000..0e18f319 --- /dev/null +++ b/port/third_party/opus/silk/float/sort_FLP.c @@ -0,0 +1,83 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/* Insertion sort (fast for already almost sorted arrays): */ +/* Best case: O(n) for an already sorted array */ +/* Worst case: O(n^2) for an inversely sorted array */ + +#include "typedef.h" +#include "SigProc_FLP.h" + +void silk_insertion_sort_decreasing_FLP( + silk_float *a, /* I/O Unsorted / Sorted vector */ + opus_int *idx, /* O Index vector for the sorted elements */ + const opus_int L, /* I Vector length */ + const opus_int K /* I Number of correctly sorted positions */ +) +{ + silk_float value; + opus_int i, j; + + /* Safety checks */ + celt_assert( K > 0 ); + celt_assert( L > 0 ); + celt_assert( L >= K ); + + /* Write start indices in index vector */ + for( i = 0; i < K; i++ ) { + idx[ i ] = i; + } + + /* Sort vector elements by value, decreasing order */ + for( i = 1; i < K; i++ ) { + value = a[ i ]; + for( j = i - 1; ( j >= 0 ) && ( value > a[ j ] ); j-- ) { + a[ j + 1 ] = a[ j ]; /* Shift value */ + idx[ j + 1 ] = idx[ j ]; /* Shift index */ + } + a[ j + 1 ] = value; /* Write value */ + idx[ j + 1 ] = i; /* Write index */ + } + + /* If less than L values are asked check the remaining values, */ + /* but only spend CPU to ensure that the K first values are correct */ + for( i = K; i < L; i++ ) { + value = a[ i ]; + if( value > a[ K - 1 ] ) { + for( j = K - 2; ( j >= 0 ) && ( value > a[ j ] ); j-- ) { + a[ j + 1 ] = a[ j ]; /* Shift value */ + idx[ j + 1 ] = idx[ j ]; /* Shift index */ + } + a[ j + 1 ] = value; /* Write value */ + idx[ j + 1 ] = i; /* Write index */ + } + } +} diff --git a/port/third_party/opus/silk/float/structs_FLP.h b/port/third_party/opus/silk/float/structs_FLP.h new file mode 100644 index 00000000..3150b386 --- /dev/null +++ b/port/third_party/opus/silk/float/structs_FLP.h @@ -0,0 +1,112 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_STRUCTS_FLP_H +#define SILK_STRUCTS_FLP_H + +#include "typedef.h" +#include "main.h" +#include "structs.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/********************************/ +/* Noise shaping analysis state */ +/********************************/ +typedef struct { + opus_int8 LastGainIndex; + silk_float HarmShapeGain_smth; + silk_float Tilt_smth; +} silk_shape_state_FLP; + +/********************************/ +/* Encoder state FLP */ +/********************************/ +typedef struct { + silk_encoder_state sCmn; /* Common struct, shared with fixed-point code */ + silk_shape_state_FLP sShape; /* Noise shaping state */ + + /* Buffer for find pitch and noise shape analysis */ + silk_float x_buf[ 2 * MAX_FRAME_LENGTH + LA_SHAPE_MAX ];/* Buffer for find pitch and noise shape analysis */ + silk_float LTPCorr; /* Normalized correlation from pitch lag estimator */ +} silk_encoder_state_FLP; + +/************************/ +/* Encoder control FLP */ +/************************/ +typedef struct { + /* Prediction and coding parameters */ + silk_float Gains[ MAX_NB_SUBFR ]; + silk_float PredCoef[ 2 ][ MAX_LPC_ORDER ]; /* holds interpolated and final coefficients */ + silk_float LTPCoef[LTP_ORDER * MAX_NB_SUBFR]; + silk_float LTP_scale; + opus_int pitchL[ MAX_NB_SUBFR ]; + + /* Noise shaping parameters */ + silk_float AR[ MAX_NB_SUBFR * MAX_SHAPE_LPC_ORDER ]; + silk_float LF_MA_shp[ MAX_NB_SUBFR ]; + silk_float LF_AR_shp[ MAX_NB_SUBFR ]; + silk_float Tilt[ MAX_NB_SUBFR ]; + silk_float HarmShapeGain[ MAX_NB_SUBFR ]; + silk_float Lambda; + silk_float input_quality; + silk_float coding_quality; + + /* Measures */ + silk_float predGain; + silk_float LTPredCodGain; + silk_float ResNrg[ MAX_NB_SUBFR ]; /* Residual energy per subframe */ + + /* Parameters for CBR mode */ + opus_int32 GainsUnq_Q16[ MAX_NB_SUBFR ]; + opus_int8 lastGainIndexPrev; +} silk_encoder_control_FLP; + +/************************/ +/* Encoder Super Struct */ +/************************/ +typedef struct { + silk_encoder_state_FLP state_Fxx[ ENCODER_NUM_CHANNELS ]; + stereo_enc_state sStereo; + opus_int32 nBitsUsedLBRR; + opus_int32 nBitsExceeded; + opus_int nChannelsAPI; + opus_int nChannelsInternal; + opus_int nPrevChannelsInternal; + opus_int timeSinceSwitchAllowed_ms; + opus_int allowBandwidthSwitch; + opus_int prev_decode_only_middle; +} silk_encoder; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/port/third_party/opus/silk/float/warped_autocorrelation_FLP.c b/port/third_party/opus/silk/float/warped_autocorrelation_FLP.c new file mode 100644 index 00000000..116dab92 --- /dev/null +++ b/port/third_party/opus/silk/float/warped_autocorrelation_FLP.c @@ -0,0 +1,75 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" + +/* Autocorrelations for a warped frequency axis */ +void silk_warped_autocorrelation_FLP( + silk_float *corr, /* O Result [order + 1] */ + const silk_float *input, /* I Input data to correlate */ + const silk_float warping, /* I Warping coefficient */ + const opus_int length, /* I Length of input */ + const opus_int order /* I Correlation order (even) */ +) +{ + opus_int n, i; + double tmp1, tmp2; + double state[ MAX_SHAPE_LPC_ORDER + 1 ] = { 0 }; + double C[ MAX_SHAPE_LPC_ORDER + 1 ] = { 0 }; + + /* Order must be even */ + celt_assert( ( order & 1 ) == 0 ); + + /* Loop over samples */ + for( n = 0; n < length; n++ ) { + tmp1 = input[ n ]; + /* Loop over allpass sections */ + for( i = 0; i < order; i += 2 ) { + /* Output of allpass section */ + /* We voluntarily use two multiples instead of factoring the expression to + reduce the length of the dependency chain (tmp1->tmp2->tmp1... ). */ + tmp2 = state[ i ] + warping * state[ i + 1 ] - warping * tmp1; + state[ i ] = tmp1; + C[ i ] += state[ 0 ] * tmp1; + /* Output of allpass section */ + tmp1 = state[ i + 1 ] + warping * state[ i + 2 ] - warping * tmp2; + state[ i + 1 ] = tmp2; + C[ i + 1 ] += state[ 0 ] * tmp2; + } + state[ order ] = tmp1; + C[ order ] += state[ 0 ] * tmp1; + } + + /* Copy correlations in silk_float output format */ + for( i = 0; i < order + 1; i++ ) { + corr[ i ] = ( silk_float )C[ i ]; + } +} diff --git a/port/third_party/opus/silk/float/wrappers_FLP.c b/port/third_party/opus/silk/float/wrappers_FLP.c new file mode 100644 index 00000000..c0c183e3 --- /dev/null +++ b/port/third_party/opus/silk/float/wrappers_FLP.c @@ -0,0 +1,209 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main_FLP.h" + +/* Wrappers. Calls flp / fix code */ + +/* Convert AR filter coefficients to NLSF parameters */ +void silk_A2NLSF_FLP( + opus_int16 *NLSF_Q15, /* O NLSF vector [ LPC_order ] */ + const silk_float *pAR, /* I LPC coefficients [ LPC_order ] */ + const opus_int LPC_order /* I LPC order */ +) +{ + opus_int i; + opus_int32 a_fix_Q16[ MAX_LPC_ORDER ]; + + for( i = 0; i < LPC_order; i++ ) { + a_fix_Q16[ i ] = silk_float2int( pAR[ i ] * 65536.0f ); + } + + silk_A2NLSF( NLSF_Q15, a_fix_Q16, LPC_order ); +} + +/* Convert LSF parameters to AR prediction filter coefficients */ +void silk_NLSF2A_FLP( + silk_float *pAR, /* O LPC coefficients [ LPC_order ] */ + const opus_int16 *NLSF_Q15, /* I NLSF vector [ LPC_order ] */ + const opus_int LPC_order, /* I LPC order */ + int arch /* I Run-time architecture */ +) +{ + opus_int i; + opus_int16 a_fix_Q12[ MAX_LPC_ORDER ]; + + silk_NLSF2A( a_fix_Q12, NLSF_Q15, LPC_order, arch ); + + for( i = 0; i < LPC_order; i++ ) { + pAR[ i ] = ( silk_float )a_fix_Q12[ i ] * ( 1.0f / 4096.0f ); + } +} + +/******************************************/ +/* Floating-point NLSF processing wrapper */ +/******************************************/ +void silk_process_NLSFs_FLP( + silk_encoder_state *psEncC, /* I/O Encoder state */ + silk_float PredCoef[ 2 ][ MAX_LPC_ORDER ], /* O Prediction coefficients */ + opus_int16 NLSF_Q15[ MAX_LPC_ORDER ], /* I/O Normalized LSFs (quant out) (0 - (2^15-1)) */ + const opus_int16 prev_NLSF_Q15[ MAX_LPC_ORDER ] /* I Previous Normalized LSFs (0 - (2^15-1)) */ +) +{ + opus_int i, j; + opus_int16 PredCoef_Q12[ 2 ][ MAX_LPC_ORDER ]; + + silk_process_NLSFs( psEncC, PredCoef_Q12, NLSF_Q15, prev_NLSF_Q15); + + for( j = 0; j < 2; j++ ) { + for( i = 0; i < psEncC->predictLPCOrder; i++ ) { + PredCoef[ j ][ i ] = ( silk_float )PredCoef_Q12[ j ][ i ] * ( 1.0f / 4096.0f ); + } + } +} + +/****************************************/ +/* Floating-point Silk NSQ wrapper */ +/****************************************/ +void silk_NSQ_wrapper_FLP( + silk_encoder_state_FLP *psEnc, /* I/O Encoder state FLP */ + silk_encoder_control_FLP *psEncCtrl, /* I/O Encoder control FLP */ + SideInfoIndices *psIndices, /* I/O Quantization indices */ + silk_nsq_state *psNSQ, /* I/O Noise Shaping Quantzation state */ + opus_int8 pulses[], /* O Quantized pulse signal */ + const silk_float x[] /* I Prefiltered input signal */ +) +{ + opus_int i, j; + opus_int16 x16[ MAX_FRAME_LENGTH ]; + opus_int32 Gains_Q16[ MAX_NB_SUBFR ]; + silk_DWORD_ALIGN opus_int16 PredCoef_Q12[ 2 ][ MAX_LPC_ORDER ]; + opus_int16 LTPCoef_Q14[ LTP_ORDER * MAX_NB_SUBFR ]; + opus_int LTP_scale_Q14; + + /* Noise shaping parameters */ + opus_int16 AR_Q13[ MAX_NB_SUBFR * MAX_SHAPE_LPC_ORDER ]; + opus_int32 LF_shp_Q14[ MAX_NB_SUBFR ]; /* Packs two int16 coefficients per int32 value */ + opus_int Lambda_Q10; + opus_int Tilt_Q14[ MAX_NB_SUBFR ]; + opus_int HarmShapeGain_Q14[ MAX_NB_SUBFR ]; + + /* Convert control struct to fix control struct */ + /* Noise shape parameters */ + for( i = 0; i < psEnc->sCmn.nb_subfr; i++ ) { + for( j = 0; j < psEnc->sCmn.shapingLPCOrder; j++ ) { + AR_Q13[ i * MAX_SHAPE_LPC_ORDER + j ] = silk_float2int( psEncCtrl->AR[ i * MAX_SHAPE_LPC_ORDER + j ] * 8192.0f ); + } + } + + for( i = 0; i < psEnc->sCmn.nb_subfr; i++ ) { + LF_shp_Q14[ i ] = silk_LSHIFT32( silk_float2int( psEncCtrl->LF_AR_shp[ i ] * 16384.0f ), 16 ) | + (opus_uint16)silk_float2int( psEncCtrl->LF_MA_shp[ i ] * 16384.0f ); + Tilt_Q14[ i ] = (opus_int)silk_float2int( psEncCtrl->Tilt[ i ] * 16384.0f ); + HarmShapeGain_Q14[ i ] = (opus_int)silk_float2int( psEncCtrl->HarmShapeGain[ i ] * 16384.0f ); + } + Lambda_Q10 = ( opus_int )silk_float2int( psEncCtrl->Lambda * 1024.0f ); + + /* prediction and coding parameters */ + for( i = 0; i < psEnc->sCmn.nb_subfr * LTP_ORDER; i++ ) { + LTPCoef_Q14[ i ] = (opus_int16)silk_float2int( psEncCtrl->LTPCoef[ i ] * 16384.0f ); + } + + for( j = 0; j < 2; j++ ) { + for( i = 0; i < psEnc->sCmn.predictLPCOrder; i++ ) { + PredCoef_Q12[ j ][ i ] = (opus_int16)silk_float2int( psEncCtrl->PredCoef[ j ][ i ] * 4096.0f ); + } + } + + for( i = 0; i < psEnc->sCmn.nb_subfr; i++ ) { + Gains_Q16[ i ] = silk_float2int( psEncCtrl->Gains[ i ] * 65536.0f ); + silk_assert( Gains_Q16[ i ] > 0 ); + } + + if( psIndices->signalType == TYPE_VOICED ) { + LTP_scale_Q14 = silk_LTPScales_table_Q14[ psIndices->LTP_scaleIndex ]; + } else { + LTP_scale_Q14 = 0; + } + + /* Convert input to fix */ + for( i = 0; i < psEnc->sCmn.frame_length; i++ ) { + x16[ i ] = silk_float2int( x[ i ] ); + } + + /* Call NSQ */ + if( psEnc->sCmn.nStatesDelayedDecision > 1 || psEnc->sCmn.warping_Q16 > 0 ) { + silk_NSQ_del_dec( &psEnc->sCmn, psNSQ, psIndices, x16, pulses, PredCoef_Q12[ 0 ], LTPCoef_Q14, + AR_Q13, HarmShapeGain_Q14, Tilt_Q14, LF_shp_Q14, Gains_Q16, psEncCtrl->pitchL, Lambda_Q10, LTP_scale_Q14, psEnc->sCmn.arch ); + } else { + silk_NSQ( &psEnc->sCmn, psNSQ, psIndices, x16, pulses, PredCoef_Q12[ 0 ], LTPCoef_Q14, + AR_Q13, HarmShapeGain_Q14, Tilt_Q14, LF_shp_Q14, Gains_Q16, psEncCtrl->pitchL, Lambda_Q10, LTP_scale_Q14, psEnc->sCmn.arch ); + } +} + +/***********************************************/ +/* Floating-point Silk LTP quantiation wrapper */ +/***********************************************/ +void silk_quant_LTP_gains_FLP( + silk_float B[ MAX_NB_SUBFR * LTP_ORDER ], /* O Quantized LTP gains */ + opus_int8 cbk_index[ MAX_NB_SUBFR ], /* O Codebook index */ + opus_int8 *periodicity_index, /* O Periodicity index */ + opus_int32 *sum_log_gain_Q7, /* I/O Cumulative max prediction gain */ + silk_float *pred_gain_dB, /* O LTP prediction gain */ + const silk_float XX[ MAX_NB_SUBFR * LTP_ORDER * LTP_ORDER ], /* I Correlation matrix */ + const silk_float xX[ MAX_NB_SUBFR * LTP_ORDER ], /* I Correlation vector */ + const opus_int subfr_len, /* I Number of samples per subframe */ + const opus_int nb_subfr, /* I Number of subframes */ + int arch /* I Run-time architecture */ +) +{ + opus_int i, pred_gain_dB_Q7; + opus_int16 B_Q14[ MAX_NB_SUBFR * LTP_ORDER ]; + opus_int32 XX_Q17[ MAX_NB_SUBFR * LTP_ORDER * LTP_ORDER ]; + opus_int32 xX_Q17[ MAX_NB_SUBFR * LTP_ORDER ]; + + i = 0; + do { + XX_Q17[ i ] = (opus_int32)silk_float2int( XX[ i ] * 131072.0f ); + } while ( ++i < nb_subfr * LTP_ORDER * LTP_ORDER ); + i = 0; + do { + xX_Q17[ i ] = (opus_int32)silk_float2int( xX[ i ] * 131072.0f ); + } while ( ++i < nb_subfr * LTP_ORDER ); + + silk_quant_LTP_gains( B_Q14, cbk_index, periodicity_index, sum_log_gain_Q7, &pred_gain_dB_Q7, XX_Q17, xX_Q17, subfr_len, nb_subfr, arch ); + + for( i = 0; i < nb_subfr * LTP_ORDER; i++ ) { + B[ i ] = (silk_float)B_Q14[ i ] * ( 1.0f / 16384.0f ); + } + + *pred_gain_dB = (silk_float)pred_gain_dB_Q7 * ( 1.0f / 128.0f ); +} diff --git a/port/third_party/opus/silk/gain_quant.c b/port/third_party/opus/silk/gain_quant.c new file mode 100644 index 00000000..ee65245a --- /dev/null +++ b/port/third_party/opus/silk/gain_quant.c @@ -0,0 +1,142 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +#define OFFSET ( ( MIN_QGAIN_DB * 128 ) / 6 + 16 * 128 ) +#define SCALE_Q16 ( ( 65536 * ( N_LEVELS_QGAIN - 1 ) ) / ( ( ( MAX_QGAIN_DB - MIN_QGAIN_DB ) * 128 ) / 6 ) ) +#define INV_SCALE_Q16 ( ( 65536 * ( ( ( MAX_QGAIN_DB - MIN_QGAIN_DB ) * 128 ) / 6 ) ) / ( N_LEVELS_QGAIN - 1 ) ) + +/* Gain scalar quantization with hysteresis, uniform on log scale */ +void silk_gains_quant( + opus_int8 ind[ MAX_NB_SUBFR ], /* O gain indices */ + opus_int32 gain_Q16[ MAX_NB_SUBFR ], /* I/O gains (quantized out) */ + opus_int8 *prev_ind, /* I/O last index in previous frame */ + const opus_int conditional, /* I first gain is delta coded if 1 */ + const opus_int nb_subfr /* I number of subframes */ +) +{ + opus_int k, double_step_size_threshold; + + for( k = 0; k < nb_subfr; k++ ) { + /* Convert to log scale, scale, floor() */ + ind[ k ] = silk_SMULWB( SCALE_Q16, silk_lin2log( gain_Q16[ k ] ) - OFFSET ); + + /* Round towards previous quantized gain (hysteresis) */ + if( ind[ k ] < *prev_ind ) { + ind[ k ]++; + } + ind[ k ] = silk_LIMIT_int( ind[ k ], 0, N_LEVELS_QGAIN - 1 ); + + /* Compute delta indices and limit */ + if( k == 0 && conditional == 0 ) { + /* Full index */ + ind[ k ] = silk_LIMIT_int( ind[ k ], *prev_ind + MIN_DELTA_GAIN_QUANT, N_LEVELS_QGAIN - 1 ); + *prev_ind = ind[ k ]; + } else { + /* Delta index */ + ind[ k ] = ind[ k ] - *prev_ind; + + /* Double the quantization step size for large gain increases, so that the max gain level can be reached */ + double_step_size_threshold = 2 * MAX_DELTA_GAIN_QUANT - N_LEVELS_QGAIN + *prev_ind; + if( ind[ k ] > double_step_size_threshold ) { + ind[ k ] = double_step_size_threshold + silk_RSHIFT( ind[ k ] - double_step_size_threshold + 1, 1 ); + } + + ind[ k ] = silk_LIMIT_int( ind[ k ], MIN_DELTA_GAIN_QUANT, MAX_DELTA_GAIN_QUANT ); + + /* Accumulate deltas */ + if( ind[ k ] > double_step_size_threshold ) { + *prev_ind += silk_LSHIFT( ind[ k ], 1 ) - double_step_size_threshold; + *prev_ind = silk_min_int( *prev_ind, N_LEVELS_QGAIN - 1 ); + } else { + *prev_ind += ind[ k ]; + } + + /* Shift to make non-negative */ + ind[ k ] -= MIN_DELTA_GAIN_QUANT; + } + + /* Scale and convert to linear scale */ + gain_Q16[ k ] = silk_log2lin( silk_min_32( silk_SMULWB( INV_SCALE_Q16, *prev_ind ) + OFFSET, 3967 ) ); /* 3967 = 31 in Q7 */ + } +} + +/* Gains scalar dequantization, uniform on log scale */ +void silk_gains_dequant( + opus_int32 gain_Q16[ MAX_NB_SUBFR ], /* O quantized gains */ + const opus_int8 ind[ MAX_NB_SUBFR ], /* I gain indices */ + opus_int8 *prev_ind, /* I/O last index in previous frame */ + const opus_int conditional, /* I first gain is delta coded if 1 */ + const opus_int nb_subfr /* I number of subframes */ +) +{ + opus_int k, ind_tmp, double_step_size_threshold; + + for( k = 0; k < nb_subfr; k++ ) { + if( k == 0 && conditional == 0 ) { + /* Gain index is not allowed to go down more than 16 steps (~21.8 dB) */ + *prev_ind = silk_max_int( ind[ k ], *prev_ind - 16 ); + } else { + /* Delta index */ + ind_tmp = ind[ k ] + MIN_DELTA_GAIN_QUANT; + + /* Accumulate deltas */ + double_step_size_threshold = 2 * MAX_DELTA_GAIN_QUANT - N_LEVELS_QGAIN + *prev_ind; + if( ind_tmp > double_step_size_threshold ) { + *prev_ind += silk_LSHIFT( ind_tmp, 1 ) - double_step_size_threshold; + } else { + *prev_ind += ind_tmp; + } + } + *prev_ind = silk_LIMIT_int( *prev_ind, 0, N_LEVELS_QGAIN - 1 ); + + /* Scale and convert to linear scale */ + gain_Q16[ k ] = silk_log2lin( silk_min_32( silk_SMULWB( INV_SCALE_Q16, *prev_ind ) + OFFSET, 3967 ) ); /* 3967 = 31 in Q7 */ + } +} + +/* Compute unique identifier of gain indices vector */ +opus_int32 silk_gains_ID( /* O returns unique identifier of gains */ + const opus_int8 ind[ MAX_NB_SUBFR ], /* I gain indices */ + const opus_int nb_subfr /* I number of subframes */ +) +{ + opus_int k; + opus_int32 gainsID; + + gainsID = 0; + for( k = 0; k < nb_subfr; k++ ) { + gainsID = silk_ADD_LSHIFT32( ind[ k ], gainsID, 8 ); + } + + return gainsID; +} diff --git a/port/third_party/opus/silk/init_decoder.c b/port/third_party/opus/silk/init_decoder.c new file mode 100644 index 00000000..01bc4b7a --- /dev/null +++ b/port/third_party/opus/silk/init_decoder.c @@ -0,0 +1,84 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +#ifdef ENABLE_OSCE +#include "osce.h" +#endif + +#include "structs.h" + +/************************/ +/* Reset Decoder State */ +/************************/ +opus_int silk_reset_decoder( + silk_decoder_state *psDec /* I/O Decoder state pointer */ +) +{ + /* Clear the entire encoder state, except anything copied */ + silk_memset( &psDec->SILK_DECODER_STATE_RESET_START, 0, sizeof( silk_decoder_state ) - ((char*) &psDec->SILK_DECODER_STATE_RESET_START - (char*)psDec) ); + + /* Used to deactivate LSF interpolation */ + psDec->first_frame_after_reset = 1; + psDec->prev_gain_Q16 = 65536; + psDec->arch = opus_select_arch(); + + /* Reset CNG state */ + silk_CNG_Reset( psDec ); + + /* Reset PLC state */ + silk_PLC_Reset( psDec ); + +#ifdef ENABLE_OSCE + /* Reset OSCE state and method */ + osce_reset(&psDec->osce, OSCE_DEFAULT_METHOD); +#endif + + return 0; +} + + +/************************/ +/* Init Decoder State */ +/************************/ +opus_int silk_init_decoder( + silk_decoder_state *psDec /* I/O Decoder state pointer */ +) +{ + /* Clear the entire encoder state, except anything copied */ + silk_memset( psDec, 0, sizeof( silk_decoder_state ) ); + + silk_reset_decoder( psDec ); + + return(0); +} + diff --git a/port/third_party/opus/silk/init_encoder.c b/port/third_party/opus/silk/init_encoder.c new file mode 100644 index 00000000..10d41287 --- /dev/null +++ b/port/third_party/opus/silk/init_encoder.c @@ -0,0 +1,68 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#ifdef FIXED_POINT +#include "main_FIX.h" +#else +#include "main_FLP.h" +#endif +#include "tuning_parameters.h" +#include "cpu_support.h" + +#ifdef ENABLE_DRED +#include "dred_encoder.h" +#endif + +/*********************************/ +/* Initialize Silk Encoder state */ +/*********************************/ +opus_int silk_init_encoder( + silk_encoder_state_Fxx *psEnc, /* I/O Pointer to Silk FIX encoder state */ + int arch /* I Run-time architecture */ +) +{ + opus_int ret = 0; + + /* Clear the entire encoder state */ + silk_memset( psEnc, 0, sizeof( silk_encoder_state_Fxx ) ); + + psEnc->sCmn.arch = arch; + + psEnc->sCmn.variable_HP_smth1_Q15 = silk_LSHIFT( silk_lin2log( SILK_FIX_CONST( VARIABLE_HP_MIN_CUTOFF_HZ, 16 ) ) - ( 16 << 7 ), 8 ); + psEnc->sCmn.variable_HP_smth2_Q15 = psEnc->sCmn.variable_HP_smth1_Q15; + + /* Used to deactivate LSF interpolation, pitch prediction */ + psEnc->sCmn.first_frame_after_reset = 1; + + /* Initialize Silk VAD */ + ret += silk_VAD_Init( &psEnc->sCmn.sVAD ); + + return ret; +} diff --git a/port/third_party/opus/silk/inner_prod_aligned.c b/port/third_party/opus/silk/inner_prod_aligned.c new file mode 100644 index 00000000..257ae9e0 --- /dev/null +++ b/port/third_party/opus/silk/inner_prod_aligned.c @@ -0,0 +1,47 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" + +opus_int32 silk_inner_prod_aligned_scale( + const opus_int16 *const inVec1, /* I input vector 1 */ + const opus_int16 *const inVec2, /* I input vector 2 */ + const opus_int scale, /* I number of bits to shift */ + const opus_int len /* I vector lengths */ +) +{ + opus_int i; + opus_int32 sum = 0; + for( i = 0; i < len; i++ ) { + sum = silk_ADD_RSHIFT32( sum, silk_SMULBB( inVec1[ i ], inVec2[ i ] ), scale ); + } + return sum; +} diff --git a/port/third_party/opus/silk/interpolate.c b/port/third_party/opus/silk/interpolate.c new file mode 100644 index 00000000..833c28ef --- /dev/null +++ b/port/third_party/opus/silk/interpolate.c @@ -0,0 +1,51 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Interpolate two vectors */ +void silk_interpolate( + opus_int16 xi[ MAX_LPC_ORDER ], /* O interpolated vector */ + const opus_int16 x0[ MAX_LPC_ORDER ], /* I first vector */ + const opus_int16 x1[ MAX_LPC_ORDER ], /* I second vector */ + const opus_int ifact_Q2, /* I interp. factor, weight on 2nd vector */ + const opus_int d /* I number of parameters */ +) +{ + opus_int i; + + celt_assert( ifact_Q2 >= 0 ); + celt_assert( ifact_Q2 <= 4 ); + + for( i = 0; i < d; i++ ) { + xi[ i ] = (opus_int16)silk_ADD_RSHIFT( x0[ i ], silk_SMULBB( x1[ i ] - x0[ i ], ifact_Q2 ), 2 ); + } +} diff --git a/port/third_party/opus/silk/lin2log.c b/port/third_party/opus/silk/lin2log.c new file mode 100644 index 00000000..0d5155aa --- /dev/null +++ b/port/third_party/opus/silk/lin2log.c @@ -0,0 +1,46 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +/* Approximation of 128 * log2() (very close inverse of silk_log2lin()) */ +/* Convert input to a log scale */ +opus_int32 silk_lin2log( + const opus_int32 inLin /* I input in linear scale */ +) +{ + opus_int32 lz, frac_Q7; + + silk_CLZ_FRAC( inLin, &lz, &frac_Q7 ); + + /* Piece-wise parabolic approximation */ + return silk_ADD_LSHIFT32( silk_SMLAWB( frac_Q7, silk_MUL( frac_Q7, 128 - frac_Q7 ), 179 ), 31 - lz, 7 ); +} + diff --git a/port/third_party/opus/silk/log2lin.c b/port/third_party/opus/silk/log2lin.c new file mode 100644 index 00000000..b7c48e47 --- /dev/null +++ b/port/third_party/opus/silk/log2lin.c @@ -0,0 +1,58 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" + +/* Approximation of 2^() (very close inverse of silk_lin2log()) */ +/* Convert input to a linear scale */ +opus_int32 silk_log2lin( + const opus_int32 inLog_Q7 /* I input on log scale */ +) +{ + opus_int32 out, frac_Q7; + + if( inLog_Q7 < 0 ) { + return 0; + } else if ( inLog_Q7 >= 3967 ) { + return silk_int32_MAX; + } + + out = silk_LSHIFT( 1, silk_RSHIFT( inLog_Q7, 7 ) ); + frac_Q7 = inLog_Q7 & 0x7F; + if( inLog_Q7 < 2048 ) { + /* Piece-wise parabolic approximation */ + out = silk_ADD_RSHIFT32( out, silk_MUL( out, silk_SMLAWB( frac_Q7, silk_SMULBB( frac_Q7, 128 - frac_Q7 ), -174 ) ), 7 ); + } else { + /* Piece-wise parabolic approximation */ + out = silk_MLA( out, silk_RSHIFT( out, 7 ), silk_SMLAWB( frac_Q7, silk_SMULBB( frac_Q7, 128 - frac_Q7 ), -174 ) ); + } + return out; +} diff --git a/port/third_party/opus/silk/macros.h b/port/third_party/opus/silk/macros.h new file mode 100644 index 00000000..3c67b6e5 --- /dev/null +++ b/port/third_party/opus/silk/macros.h @@ -0,0 +1,151 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_MACROS_H +#define SILK_MACROS_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "opus_types.h" +#include "opus_defines.h" +#include "arch.h" + +/* This is an OPUS_INLINE header file for general platform. */ + +/* (a32 * (opus_int32)((opus_int16)(b32))) >> 16 output have to be 32bit int */ +#if OPUS_FAST_INT64 +#define silk_SMULWB(a32, b32) ((opus_int32)(((a32) * (opus_int64)((opus_int16)(b32))) >> 16)) +#else +#define silk_SMULWB(a32, b32) ((((a32) >> 16) * (opus_int32)((opus_int16)(b32))) + ((((a32) & 0x0000FFFF) * (opus_int32)((opus_int16)(b32))) >> 16)) +#endif + +/* a32 + (b32 * (opus_int32)((opus_int16)(c32))) >> 16 output have to be 32bit int */ +#if OPUS_FAST_INT64 +#define silk_SMLAWB(a32, b32, c32) ((opus_int32)((a32) + (((b32) * (opus_int64)((opus_int16)(c32))) >> 16))) +#else +#define silk_SMLAWB(a32, b32, c32) ((a32) + ((((b32) >> 16) * (opus_int32)((opus_int16)(c32))) + ((((b32) & 0x0000FFFF) * (opus_int32)((opus_int16)(c32))) >> 16))) +#endif + +/* (a32 * (b32 >> 16)) >> 16 */ +#if OPUS_FAST_INT64 +#define silk_SMULWT(a32, b32) ((opus_int32)(((a32) * (opus_int64)((b32) >> 16)) >> 16)) +#else +#define silk_SMULWT(a32, b32) (((a32) >> 16) * ((b32) >> 16) + ((((a32) & 0x0000FFFF) * ((b32) >> 16)) >> 16)) +#endif + +/* a32 + (b32 * (c32 >> 16)) >> 16 */ +#if OPUS_FAST_INT64 +#define silk_SMLAWT(a32, b32, c32) ((opus_int32)((a32) + (((b32) * ((opus_int64)(c32) >> 16)) >> 16))) +#else +#define silk_SMLAWT(a32, b32, c32) ((a32) + (((b32) >> 16) * ((c32) >> 16)) + ((((b32) & 0x0000FFFF) * ((c32) >> 16)) >> 16)) +#endif + +/* (opus_int32)((opus_int16)(a3))) * (opus_int32)((opus_int16)(b32)) output have to be 32bit int */ +#define silk_SMULBB(a32, b32) ((opus_int32)((opus_int16)(a32)) * (opus_int32)((opus_int16)(b32))) + +/* a32 + (opus_int32)((opus_int16)(b32)) * (opus_int32)((opus_int16)(c32)) output have to be 32bit int */ +#define silk_SMLABB(a32, b32, c32) ((a32) + ((opus_int32)((opus_int16)(b32))) * (opus_int32)((opus_int16)(c32))) + +/* (opus_int32)((opus_int16)(a32)) * (b32 >> 16) */ +#define silk_SMULBT(a32, b32) ((opus_int32)((opus_int16)(a32)) * ((b32) >> 16)) + +/* a32 + (opus_int32)((opus_int16)(b32)) * (c32 >> 16) */ +#define silk_SMLABT(a32, b32, c32) ((a32) + ((opus_int32)((opus_int16)(b32))) * ((c32) >> 16)) + +/* a64 + (b32 * c32) */ +#define silk_SMLAL(a64, b32, c32) (silk_ADD64((a64), ((opus_int64)(b32) * (opus_int64)(c32)))) + +/* (a32 * b32) >> 16 */ +#if OPUS_FAST_INT64 +#define silk_SMULWW(a32, b32) ((opus_int32)(((opus_int64)(a32) * (b32)) >> 16)) +#else +#define silk_SMULWW(a32, b32) silk_MLA(silk_SMULWB((a32), (b32)), (a32), silk_RSHIFT_ROUND((b32), 16)) +#endif + +/* a32 + ((b32 * c32) >> 16) */ +#if OPUS_FAST_INT64 +#define silk_SMLAWW(a32, b32, c32) ((opus_int32)((a32) + (((opus_int64)(b32) * (c32)) >> 16))) +#else +#define silk_SMLAWW(a32, b32, c32) silk_MLA(silk_SMLAWB((a32), (b32), (c32)), (b32), silk_RSHIFT_ROUND((c32), 16)) +#endif + +/* add/subtract with output saturated */ +#define silk_ADD_SAT32(a, b) ((((opus_uint32)(a) + (opus_uint32)(b)) & 0x80000000) == 0 ? \ + ((((a) & (b)) & 0x80000000) != 0 ? silk_int32_MIN : (a)+(b)) : \ + ((((a) | (b)) & 0x80000000) == 0 ? silk_int32_MAX : (a)+(b)) ) + +#define silk_SUB_SAT32(a, b) ((((opus_uint32)(a)-(opus_uint32)(b)) & 0x80000000) == 0 ? \ + (( (a) & ((b)^0x80000000) & 0x80000000) ? silk_int32_MIN : (a)-(b)) : \ + ((((a)^0x80000000) & (b) & 0x80000000) ? silk_int32_MAX : (a)-(b)) ) + +#if defined(MIPSr1_ASM) +#include "mips/macros_mipsr1.h" +#endif + +#include "ecintrin.h" +#ifndef OVERRIDE_silk_CLZ16 +static OPUS_INLINE opus_int32 silk_CLZ16(opus_int16 in16) +{ + return 32 - EC_ILOG(in16<<16|0x8000); +} +#endif + +#ifndef OVERRIDE_silk_CLZ32 +static OPUS_INLINE opus_int32 silk_CLZ32(opus_int32 in32) +{ + return in32 ? 32 - EC_ILOG(in32) : 32; +} +#endif + +/* Row based */ +#define matrix_ptr(Matrix_base_adr, row, column, N) \ + (*((Matrix_base_adr) + ((row)*(N)+(column)))) +#define matrix_adr(Matrix_base_adr, row, column, N) \ + ((Matrix_base_adr) + ((row)*(N)+(column))) + +/* Column based */ +#ifndef matrix_c_ptr +# define matrix_c_ptr(Matrix_base_adr, row, column, M) \ + (*((Matrix_base_adr) + ((row)+(M)*(column)))) +#endif + +#ifdef OPUS_ARM_INLINE_ASM +#include "arm/macros_armv4.h" +#endif + +#ifdef OPUS_ARM_INLINE_EDSP +#include "arm/macros_armv5e.h" +#endif + +#ifdef OPUS_ARM_PRESUME_AARCH64_NEON_INTR +#include "arm/macros_arm64.h" +#endif + +#endif /* SILK_MACROS_H */ + diff --git a/port/third_party/opus/silk/main.h b/port/third_party/opus/silk/main.h new file mode 100644 index 00000000..cd576d8c --- /dev/null +++ b/port/third_party/opus/silk/main.h @@ -0,0 +1,486 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_MAIN_H +#define SILK_MAIN_H + +#include "SigProc_FIX.h" +#include "define.h" +#include "structs.h" +#include "tables.h" +#include "PLC.h" +#include "control.h" +#include "debug.h" +#include "entenc.h" +#include "entdec.h" + +#if defined(OPUS_X86_MAY_HAVE_SSE4_1) +#include "x86/main_sse.h" +#endif + +#if (defined(OPUS_ARM_ASM) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)) +#include "arm/NSQ_del_dec_arm.h" +#endif + +/* Convert Left/Right stereo signal to adaptive Mid/Side representation */ +void silk_stereo_LR_to_MS( + stereo_enc_state *state, /* I/O State */ + opus_int16 x1[], /* I/O Left input signal, becomes mid signal */ + opus_int16 x2[], /* I/O Right input signal, becomes side signal */ + opus_int8 ix[ 2 ][ 3 ], /* O Quantization indices */ + opus_int8 *mid_only_flag, /* O Flag: only mid signal coded */ + opus_int32 mid_side_rates_bps[], /* O Bitrates for mid and side signals */ + opus_int32 total_rate_bps, /* I Total bitrate */ + opus_int prev_speech_act_Q8, /* I Speech activity level in previous frame */ + opus_int toMono, /* I Last frame before a stereo->mono transition */ + opus_int fs_kHz, /* I Sample rate (kHz) */ + opus_int frame_length /* I Number of samples */ +); + +/* Convert adaptive Mid/Side representation to Left/Right stereo signal */ +void silk_stereo_MS_to_LR( + stereo_dec_state *state, /* I/O State */ + opus_int16 x1[], /* I/O Left input signal, becomes mid signal */ + opus_int16 x2[], /* I/O Right input signal, becomes side signal */ + const opus_int32 pred_Q13[], /* I Predictors */ + opus_int fs_kHz, /* I Samples rate (kHz) */ + opus_int frame_length /* I Number of samples */ +); + +/* Find least-squares prediction gain for one signal based on another and quantize it */ +opus_int32 silk_stereo_find_predictor( /* O Returns predictor in Q13 */ + opus_int32 *ratio_Q14, /* O Ratio of residual and mid energies */ + const opus_int16 x[], /* I Basis signal */ + const opus_int16 y[], /* I Target signal */ + opus_int32 mid_res_amp_Q0[], /* I/O Smoothed mid, residual norms */ + opus_int length, /* I Number of samples */ + opus_int smooth_coef_Q16 /* I Smoothing coefficient */ +); + +/* Quantize mid/side predictors */ +void silk_stereo_quant_pred( + opus_int32 pred_Q13[], /* I/O Predictors (out: quantized) */ + opus_int8 ix[ 2 ][ 3 ] /* O Quantization indices */ +); + +/* Entropy code the mid/side quantization indices */ +void silk_stereo_encode_pred( + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + opus_int8 ix[ 2 ][ 3 ] /* I Quantization indices */ +); + +/* Entropy code the mid-only flag */ +void silk_stereo_encode_mid_only( + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + opus_int8 mid_only_flag +); + +/* Decode mid/side predictors */ +void silk_stereo_decode_pred( + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int32 pred_Q13[] /* O Predictors */ +); + +/* Decode mid-only flag */ +void silk_stereo_decode_mid_only( + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int *decode_only_mid /* O Flag that only mid channel has been coded */ +); + +/* Encodes signs of excitation */ +void silk_encode_signs( + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + const opus_int8 pulses[], /* I pulse signal */ + opus_int length, /* I length of input */ + const opus_int signalType, /* I Signal type */ + const opus_int quantOffsetType, /* I Quantization offset type */ + const opus_int sum_pulses[ MAX_NB_SHELL_BLOCKS ] /* I Sum of absolute pulses per block */ +); + +/* Decodes signs of excitation */ +void silk_decode_signs( + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int16 pulses[], /* I/O pulse signal */ + opus_int length, /* I length of input */ + const opus_int signalType, /* I Signal type */ + const opus_int quantOffsetType, /* I Quantization offset type */ + const opus_int sum_pulses[ MAX_NB_SHELL_BLOCKS ] /* I Sum of absolute pulses per block */ +); + +/* Check encoder control struct */ +opus_int check_control_input( + silk_EncControlStruct *encControl /* I Control structure */ +); + +/* Control internal sampling rate */ +opus_int silk_control_audio_bandwidth( + silk_encoder_state *psEncC, /* I/O Pointer to Silk encoder state */ + silk_EncControlStruct *encControl /* I Control structure */ +); + +/* Control SNR of redidual quantizer */ +opus_int silk_control_SNR( + silk_encoder_state *psEncC, /* I/O Pointer to Silk encoder state */ + opus_int32 TargetRate_bps /* I Target max bitrate (bps) */ +); + +/***************/ +/* Shell coder */ +/***************/ + +/* Encode quantization indices of excitation */ +void silk_encode_pulses( + ec_enc *psRangeEnc, /* I/O compressor data structure */ + const opus_int signalType, /* I Signal type */ + const opus_int quantOffsetType, /* I quantOffsetType */ + opus_int8 pulses[], /* I quantization indices */ + const opus_int frame_length /* I Frame length */ +); + +/* Shell encoder, operates on one shell code frame of 16 pulses */ +void silk_shell_encoder( + ec_enc *psRangeEnc, /* I/O compressor data structure */ + const opus_int *pulses0 /* I data: nonnegative pulse amplitudes */ +); + +/* Shell decoder, operates on one shell code frame of 16 pulses */ +void silk_shell_decoder( + opus_int16 *pulses0, /* O data: nonnegative pulse amplitudes */ + ec_dec *psRangeDec, /* I/O Compressor data structure */ + const opus_int pulses4 /* I number of pulses per pulse-subframe */ +); + +/* Gain scalar quantization with hysteresis, uniform on log scale */ +void silk_gains_quant( + opus_int8 ind[ MAX_NB_SUBFR ], /* O gain indices */ + opus_int32 gain_Q16[ MAX_NB_SUBFR ], /* I/O gains (quantized out) */ + opus_int8 *prev_ind, /* I/O last index in previous frame */ + const opus_int conditional, /* I first gain is delta coded if 1 */ + const opus_int nb_subfr /* I number of subframes */ +); + +/* Gains scalar dequantization, uniform on log scale */ +void silk_gains_dequant( + opus_int32 gain_Q16[ MAX_NB_SUBFR ], /* O quantized gains */ + const opus_int8 ind[ MAX_NB_SUBFR ], /* I gain indices */ + opus_int8 *prev_ind, /* I/O last index in previous frame */ + const opus_int conditional, /* I first gain is delta coded if 1 */ + const opus_int nb_subfr /* I number of subframes */ +); + +/* Compute unique identifier of gain indices vector */ +opus_int32 silk_gains_ID( /* O returns unique identifier of gains */ + const opus_int8 ind[ MAX_NB_SUBFR ], /* I gain indices */ + const opus_int nb_subfr /* I number of subframes */ +); + +/* Interpolate two vectors */ +void silk_interpolate( + opus_int16 xi[ MAX_LPC_ORDER ], /* O interpolated vector */ + const opus_int16 x0[ MAX_LPC_ORDER ], /* I first vector */ + const opus_int16 x1[ MAX_LPC_ORDER ], /* I second vector */ + const opus_int ifact_Q2, /* I interp. factor, weight on 2nd vector */ + const opus_int d /* I number of parameters */ +); + +/* LTP tap quantizer */ +void silk_quant_LTP_gains( + opus_int16 B_Q14[ MAX_NB_SUBFR * LTP_ORDER ], /* O Quantized LTP gains */ + opus_int8 cbk_index[ MAX_NB_SUBFR ], /* O Codebook Index */ + opus_int8 *periodicity_index, /* O Periodicity Index */ + opus_int32 *sum_gain_dB_Q7, /* I/O Cumulative max prediction gain */ + opus_int *pred_gain_dB_Q7, /* O LTP prediction gain */ + const opus_int32 XX_Q17[ MAX_NB_SUBFR*LTP_ORDER*LTP_ORDER ], /* I Correlation matrix in Q18 */ + const opus_int32 xX_Q17[ MAX_NB_SUBFR*LTP_ORDER ], /* I Correlation vector in Q18 */ + const opus_int subfr_len, /* I Number of samples per subframe */ + const opus_int nb_subfr, /* I Number of subframes */ + int arch /* I Run-time architecture */ +); + +/* Entropy constrained matrix-weighted VQ, for a single input data vector */ +void silk_VQ_WMat_EC_c( + opus_int8 *ind, /* O index of best codebook vector */ + opus_int32 *res_nrg_Q15, /* O best residual energy */ + opus_int32 *rate_dist_Q8, /* O best total bitrate */ + opus_int *gain_Q7, /* O sum of absolute LTP coefficients */ + const opus_int32 *XX_Q17, /* I correlation matrix */ + const opus_int32 *xX_Q17, /* I correlation vector */ + const opus_int8 *cb_Q7, /* I codebook */ + const opus_uint8 *cb_gain_Q7, /* I codebook effective gain */ + const opus_uint8 *cl_Q5, /* I code length for each codebook vector */ + const opus_int subfr_len, /* I number of samples per subframe */ + const opus_int32 max_gain_Q7, /* I maximum sum of absolute LTP coefficients */ + const opus_int L /* I number of vectors in codebook */ +); + +#if !defined(OVERRIDE_silk_VQ_WMat_EC) +#define silk_VQ_WMat_EC(ind, res_nrg_Q15, rate_dist_Q8, gain_Q7, XX_Q17, xX_Q17, cb_Q7, cb_gain_Q7, cl_Q5, subfr_len, max_gain_Q7, L, arch) \ + ((void)(arch),silk_VQ_WMat_EC_c(ind, res_nrg_Q15, rate_dist_Q8, gain_Q7, XX_Q17, xX_Q17, cb_Q7, cb_gain_Q7, cl_Q5, subfr_len, max_gain_Q7, L)) +#endif + +/************************************/ +/* Noise shaping quantization (NSQ) */ +/************************************/ + +void silk_NSQ_c( + const silk_encoder_state *psEncC, /* I Encoder State */ + silk_nsq_state *NSQ, /* I/O NSQ state */ + SideInfoIndices *psIndices, /* I/O Quantization Indices */ + const opus_int16 x16[], /* I Input */ + opus_int8 pulses[], /* O Quantized pulse signal */ + const opus_int16 *PredCoef_Q12, /* I Short term prediction coefs */ + const opus_int16 LTPCoef_Q14[ LTP_ORDER * MAX_NB_SUBFR ], /* I Long term prediction coefs */ + const opus_int16 AR_Q13[ MAX_NB_SUBFR * MAX_SHAPE_LPC_ORDER ], /* I Noise shaping coefs */ + const opus_int HarmShapeGain_Q14[ MAX_NB_SUBFR ], /* I Long term shaping coefs */ + const opus_int Tilt_Q14[ MAX_NB_SUBFR ], /* I Spectral tilt */ + const opus_int32 LF_shp_Q14[ MAX_NB_SUBFR ], /* I Low frequency shaping coefs */ + const opus_int32 Gains_Q16[ MAX_NB_SUBFR ], /* I Quantization step sizes */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lags */ + const opus_int Lambda_Q10, /* I Rate/distortion tradeoff */ + const opus_int LTP_scale_Q14 /* I LTP state scaling */ +); + +#if !defined(OVERRIDE_silk_NSQ) +#define silk_NSQ(psEncC, NSQ, psIndices, x16, pulses, PredCoef_Q12, LTPCoef_Q14, AR_Q13, \ + HarmShapeGain_Q14, Tilt_Q14, LF_shp_Q14, Gains_Q16, pitchL, Lambda_Q10, LTP_scale_Q14, arch) \ + ((void)(arch),silk_NSQ_c(psEncC, NSQ, psIndices, x16, pulses, PredCoef_Q12, LTPCoef_Q14, AR_Q13, \ + HarmShapeGain_Q14, Tilt_Q14, LF_shp_Q14, Gains_Q16, pitchL, Lambda_Q10, LTP_scale_Q14)) +#endif + +/* Noise shaping using delayed decision */ +void silk_NSQ_del_dec_c( + const silk_encoder_state *psEncC, /* I Encoder State */ + silk_nsq_state *NSQ, /* I/O NSQ state */ + SideInfoIndices *psIndices, /* I/O Quantization Indices */ + const opus_int16 x16[], /* I Input */ + opus_int8 pulses[], /* O Quantized pulse signal */ + const opus_int16 *PredCoef_Q12, /* I Short term prediction coefs */ + const opus_int16 LTPCoef_Q14[ LTP_ORDER * MAX_NB_SUBFR ], /* I Long term prediction coefs */ + const opus_int16 AR_Q13[ MAX_NB_SUBFR * MAX_SHAPE_LPC_ORDER ], /* I Noise shaping coefs */ + const opus_int HarmShapeGain_Q14[ MAX_NB_SUBFR ], /* I Long term shaping coefs */ + const opus_int Tilt_Q14[ MAX_NB_SUBFR ], /* I Spectral tilt */ + const opus_int32 LF_shp_Q14[ MAX_NB_SUBFR ], /* I Low frequency shaping coefs */ + const opus_int32 Gains_Q16[ MAX_NB_SUBFR ], /* I Quantization step sizes */ + const opus_int pitchL[ MAX_NB_SUBFR ], /* I Pitch lags */ + const opus_int Lambda_Q10, /* I Rate/distortion tradeoff */ + const opus_int LTP_scale_Q14 /* I LTP state scaling */ +); + +#if !defined(OVERRIDE_silk_NSQ_del_dec) +#define silk_NSQ_del_dec(psEncC, NSQ, psIndices, x16, pulses, PredCoef_Q12, LTPCoef_Q14, AR_Q13, \ + HarmShapeGain_Q14, Tilt_Q14, LF_shp_Q14, Gains_Q16, pitchL, Lambda_Q10, LTP_scale_Q14, arch) \ + ((void)(arch),silk_NSQ_del_dec_c(psEncC, NSQ, psIndices, x16, pulses, PredCoef_Q12, LTPCoef_Q14, AR_Q13, \ + HarmShapeGain_Q14, Tilt_Q14, LF_shp_Q14, Gains_Q16, pitchL, Lambda_Q10, LTP_scale_Q14)) +#endif + +/************/ +/* Silk VAD */ +/************/ +/* Initialize the Silk VAD */ +opus_int silk_VAD_Init( /* O Return value, 0 if success */ + silk_VAD_state *psSilk_VAD /* I/O Pointer to Silk VAD state */ +); + +/* Get speech activity level in Q8 */ +opus_int silk_VAD_GetSA_Q8_c( /* O Return value, 0 if success */ + silk_encoder_state *psEncC, /* I/O Encoder state */ + const opus_int16 pIn[] /* I PCM input */ +); + +#if !defined(OVERRIDE_silk_VAD_GetSA_Q8) +#define silk_VAD_GetSA_Q8(psEnC, pIn, arch) ((void)(arch),silk_VAD_GetSA_Q8_c(psEnC, pIn)) +#endif + +/* Low-pass filter with variable cutoff frequency based on */ +/* piece-wise linear interpolation between elliptic filters */ +/* Start by setting transition_frame_no = 1; */ +void silk_LP_variable_cutoff( + silk_LP_state *psLP, /* I/O LP filter state */ + opus_int16 *frame, /* I/O Low-pass filtered output signal */ + const opus_int frame_length /* I Frame length */ +); + +/******************/ +/* NLSF Quantizer */ +/******************/ +/* Limit, stabilize, convert and quantize NLSFs */ +void silk_process_NLSFs( + silk_encoder_state *psEncC, /* I/O Encoder state */ + opus_int16 PredCoef_Q12[ 2 ][ MAX_LPC_ORDER ], /* O Prediction coefficients */ + opus_int16 pNLSF_Q15[ MAX_LPC_ORDER ], /* I/O Normalized LSFs (quant out) (0 - (2^15-1)) */ + const opus_int16 prev_NLSFq_Q15[ MAX_LPC_ORDER ] /* I Previous Normalized LSFs (0 - (2^15-1)) */ +); + +opus_int32 silk_NLSF_encode( /* O Returns RD value in Q25 */ + opus_int8 *NLSFIndices, /* I Codebook path vector [ LPC_ORDER + 1 ] */ + opus_int16 *pNLSF_Q15, /* I/O Quantized NLSF vector [ LPC_ORDER ] */ + const silk_NLSF_CB_struct *psNLSF_CB, /* I Codebook object */ + const opus_int16 *pW_QW, /* I NLSF weight vector [ LPC_ORDER ] */ + const opus_int NLSF_mu_Q20, /* I Rate weight for the RD optimization */ + const opus_int nSurvivors, /* I Max survivors after first stage */ + const opus_int signalType /* I Signal type: 0/1/2 */ +); + +/* Compute quantization errors for an LPC_order element input vector for a VQ codebook */ +void silk_NLSF_VQ( + opus_int32 err_Q26[], /* O Quantization errors [K] */ + const opus_int16 in_Q15[], /* I Input vectors to be quantized [LPC_order] */ + const opus_uint8 pCB_Q8[], /* I Codebook vectors [K*LPC_order] */ + const opus_int16 pWght_Q9[], /* I Codebook weights [K*LPC_order] */ + const opus_int K, /* I Number of codebook vectors */ + const opus_int LPC_order /* I Number of LPCs */ +); + +/* Delayed-decision quantizer for NLSF residuals */ +opus_int32 silk_NLSF_del_dec_quant( /* O Returns RD value in Q25 */ + opus_int8 indices[], /* O Quantization indices [ order ] */ + const opus_int16 x_Q10[], /* I Input [ order ] */ + const opus_int16 w_Q5[], /* I Weights [ order ] */ + const opus_uint8 pred_coef_Q8[], /* I Backward predictor coefs [ order ] */ + const opus_int16 ec_ix[], /* I Indices to entropy coding tables [ order ] */ + const opus_uint8 ec_rates_Q5[], /* I Rates [] */ + const opus_int quant_step_size_Q16, /* I Quantization step size */ + const opus_int16 inv_quant_step_size_Q6, /* I Inverse quantization step size */ + const opus_int32 mu_Q20, /* I R/D tradeoff */ + const opus_int16 order /* I Number of input values */ +); + +/* Unpack predictor values and indices for entropy coding tables */ +void silk_NLSF_unpack( + opus_int16 ec_ix[], /* O Indices to entropy tables [ LPC_ORDER ] */ + opus_uint8 pred_Q8[], /* O LSF predictor [ LPC_ORDER ] */ + const silk_NLSF_CB_struct *psNLSF_CB, /* I Codebook object */ + const opus_int CB1_index /* I Index of vector in first LSF codebook */ +); + +/***********************/ +/* NLSF vector decoder */ +/***********************/ +void silk_NLSF_decode( + opus_int16 *pNLSF_Q15, /* O Quantized NLSF vector [ LPC_ORDER ] */ + opus_int8 *NLSFIndices, /* I Codebook path vector [ LPC_ORDER + 1 ] */ + const silk_NLSF_CB_struct *psNLSF_CB /* I Codebook object */ +); + +/****************************************************/ +/* Decoder Functions */ +/****************************************************/ +opus_int silk_reset_decoder( + silk_decoder_state *psDec /* I/O Decoder state pointer */ +); + +opus_int silk_init_decoder( + silk_decoder_state *psDec /* I/O Decoder state pointer */ +); + +/* Set decoder sampling rate */ +opus_int silk_decoder_set_fs( + silk_decoder_state *psDec, /* I/O Decoder state pointer */ + opus_int fs_kHz, /* I Sampling frequency (kHz) */ + opus_int32 fs_API_Hz /* I API Sampling frequency (Hz) */ +); + +/****************/ +/* Decode frame */ +/****************/ +opus_int silk_decode_frame( + silk_decoder_state *psDec, /* I/O Pointer to Silk decoder state */ + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int16 pOut[], /* O Pointer to output speech frame */ + opus_int32 *pN, /* O Pointer to size of output frame */ + opus_int lostFlag, /* I 0: no loss, 1 loss, 2 decode fec */ + opus_int condCoding, /* I The type of conditional coding to use */ +#ifdef ENABLE_DEEP_PLC + LPCNetPLCState *lpcnet, +#endif +#ifdef ENABLE_OSCE + OSCEModel *osce_model, +#endif + int arch /* I Run-time architecture */ +); + +/* Decode indices from bitstream */ +void silk_decode_indices( + silk_decoder_state *psDec, /* I/O State */ + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int FrameIndex, /* I Frame number */ + opus_int decode_LBRR, /* I Flag indicating LBRR data is being decoded */ + opus_int condCoding /* I The type of conditional coding to use */ +); + +/* Decode parameters from payload */ +void silk_decode_parameters( + silk_decoder_state *psDec, /* I/O State */ + silk_decoder_control *psDecCtrl, /* I/O Decoder control */ + opus_int condCoding /* I The type of conditional coding to use */ +); + +/* Core decoder. Performs inverse NSQ operation LTP + LPC */ +void silk_decode_core( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl, /* I Decoder control */ + opus_int16 xq[], /* O Decoded speech */ + const opus_int16 pulses[ MAX_FRAME_LENGTH ], /* I Pulse signal */ + int arch /* I Run-time architecture */ +); + +/* Decode quantization indices of excitation (Shell coding) */ +void silk_decode_pulses( + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int16 pulses[], /* O Excitation signal */ + const opus_int signalType, /* I Sigtype */ + const opus_int quantOffsetType, /* I quantOffsetType */ + const opus_int frame_length /* I Frame length */ +); + +/******************/ +/* CNG */ +/******************/ + +/* Reset CNG */ +void silk_CNG_Reset( + silk_decoder_state *psDec /* I/O Decoder state */ +); + +/* Updates CNG estimate, and applies the CNG when packet was lost */ +void silk_CNG( + silk_decoder_state *psDec, /* I/O Decoder state */ + silk_decoder_control *psDecCtrl, /* I/O Decoder control */ + opus_int16 frame[], /* I/O Signal */ + opus_int length /* I Length of residual */ +); + +/* Encoding of various parameters */ +void silk_encode_indices( + silk_encoder_state *psEncC, /* I/O Encoder state */ + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + opus_int FrameIndex, /* I Frame number */ + opus_int encode_LBRR, /* I Flag indicating LBRR data is being encoded */ + opus_int condCoding /* I The type of conditional coding to use */ +); + +#endif diff --git a/port/third_party/opus/silk/pitch_est_defines.h b/port/third_party/opus/silk/pitch_est_defines.h new file mode 100644 index 00000000..e1e4b5d7 --- /dev/null +++ b/port/third_party/opus/silk/pitch_est_defines.h @@ -0,0 +1,88 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_PE_DEFINES_H +#define SILK_PE_DEFINES_H + +#include "SigProc_FIX.h" + +/********************************************************/ +/* Definitions for pitch estimator */ +/********************************************************/ + +#define PE_MAX_FS_KHZ 16 /* Maximum sampling frequency used */ + +#define PE_MAX_NB_SUBFR 4 +#define PE_SUBFR_LENGTH_MS 5 /* 5 ms */ + +#define PE_LTP_MEM_LENGTH_MS ( 4 * PE_SUBFR_LENGTH_MS ) + +#define PE_MAX_FRAME_LENGTH_MS ( PE_LTP_MEM_LENGTH_MS + PE_MAX_NB_SUBFR * PE_SUBFR_LENGTH_MS ) +#define PE_MAX_FRAME_LENGTH ( PE_MAX_FRAME_LENGTH_MS * PE_MAX_FS_KHZ ) +#define PE_MAX_FRAME_LENGTH_ST_1 ( PE_MAX_FRAME_LENGTH >> 2 ) +#define PE_MAX_FRAME_LENGTH_ST_2 ( PE_MAX_FRAME_LENGTH >> 1 ) + +#define PE_MAX_LAG_MS 18 /* 18 ms -> 56 Hz */ +#define PE_MIN_LAG_MS 2 /* 2 ms -> 500 Hz */ +#define PE_MAX_LAG ( PE_MAX_LAG_MS * PE_MAX_FS_KHZ ) +#define PE_MIN_LAG ( PE_MIN_LAG_MS * PE_MAX_FS_KHZ ) + +#define PE_D_SRCH_LENGTH 24 + +#define PE_NB_STAGE3_LAGS 5 + +#define PE_NB_CBKS_STAGE2 3 +#define PE_NB_CBKS_STAGE2_EXT 11 + +#define PE_NB_CBKS_STAGE3_MAX 34 +#define PE_NB_CBKS_STAGE3_MID 24 +#define PE_NB_CBKS_STAGE3_MIN 16 + +#define PE_NB_CBKS_STAGE3_10MS 12 +#define PE_NB_CBKS_STAGE2_10MS 3 + +#define PE_SHORTLAG_BIAS 0.2f /* for logarithmic weighting */ +#define PE_PREVLAG_BIAS 0.2f /* for logarithmic weighting */ +#define PE_FLATCONTOUR_BIAS 0.05f + +#define SILK_PE_MIN_COMPLEX 0 +#define SILK_PE_MID_COMPLEX 1 +#define SILK_PE_MAX_COMPLEX 2 + +/* Tables for 20 ms frames */ +extern const opus_int8 silk_CB_lags_stage2[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE2_EXT ]; +extern const opus_int8 silk_CB_lags_stage3[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE3_MAX ]; +extern const opus_int8 silk_Lag_range_stage3[ SILK_PE_MAX_COMPLEX + 1 ] [ PE_MAX_NB_SUBFR ][ 2 ]; +extern const opus_int8 silk_nb_cbk_searchs_stage3[ SILK_PE_MAX_COMPLEX + 1 ]; + +/* Tables for 10 ms frames */ +extern const opus_int8 silk_CB_lags_stage2_10_ms[ PE_MAX_NB_SUBFR >> 1][ 3 ]; +extern const opus_int8 silk_CB_lags_stage3_10_ms[ PE_MAX_NB_SUBFR >> 1 ][ 12 ]; +extern const opus_int8 silk_Lag_range_stage3_10_ms[ PE_MAX_NB_SUBFR >> 1 ][ 2 ]; + +#endif + diff --git a/port/third_party/opus/silk/pitch_est_tables.c b/port/third_party/opus/silk/pitch_est_tables.c new file mode 100644 index 00000000..81a8baca --- /dev/null +++ b/port/third_party/opus/silk/pitch_est_tables.c @@ -0,0 +1,99 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "typedef.h" +#include "pitch_est_defines.h" + +const opus_int8 silk_CB_lags_stage2_10_ms[ PE_MAX_NB_SUBFR >> 1][ PE_NB_CBKS_STAGE2_10MS ] = +{ + {0, 1, 0}, + {0, 0, 1} +}; + +const opus_int8 silk_CB_lags_stage3_10_ms[ PE_MAX_NB_SUBFR >> 1 ][ PE_NB_CBKS_STAGE3_10MS ] = +{ + { 0, 0, 1,-1, 1,-1, 2,-2, 2,-2, 3,-3}, + { 0, 1, 0, 1,-1, 2,-1, 2,-2, 3,-2, 3} +}; + +const opus_int8 silk_Lag_range_stage3_10_ms[ PE_MAX_NB_SUBFR >> 1 ][ 2 ] = +{ + {-3, 7}, + {-2, 7} +}; + +const opus_int8 silk_CB_lags_stage2[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE2_EXT ] = +{ + {0, 2,-1,-1,-1, 0, 0, 1, 1, 0, 1}, + {0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0}, + {0,-1, 2, 1, 0, 1, 1, 0, 0,-1,-1} +}; + +const opus_int8 silk_CB_lags_stage3[ PE_MAX_NB_SUBFR ][ PE_NB_CBKS_STAGE3_MAX ] = +{ + {0, 0, 1,-1, 0, 1,-1, 0,-1, 1,-2, 2,-2,-2, 2,-3, 2, 3,-3,-4, 3,-4, 4, 4,-5, 5,-6,-5, 6,-7, 6, 5, 8,-9}, + {0, 0, 1, 0, 0, 0, 0, 0, 0, 0,-1, 1, 0, 0, 1,-1, 0, 1,-1,-1, 1,-1, 2, 1,-1, 2,-2,-2, 2,-2, 2, 2, 3,-3}, + {0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1,-1, 1, 0, 0, 2, 1,-1, 2,-1,-1, 2,-1, 2, 2,-1, 3,-2,-2,-2, 3}, + {0, 1, 0, 0, 1, 0, 1,-1, 2,-1, 2,-1, 2, 3,-2, 3,-2,-2, 4, 4,-3, 5,-3,-4, 6,-4, 6, 5,-5, 8,-6,-5,-7, 9} +}; + +const opus_int8 silk_Lag_range_stage3[ SILK_PE_MAX_COMPLEX + 1 ] [ PE_MAX_NB_SUBFR ][ 2 ] = +{ + /* Lags to search for low number of stage3 cbks */ + { + {-5,8}, + {-1,6}, + {-1,6}, + {-4,10} + }, + /* Lags to search for middle number of stage3 cbks */ + { + {-6,10}, + {-2,6}, + {-1,6}, + {-5,10} + }, + /* Lags to search for max number of stage3 cbks */ + { + {-9,12}, + {-3,7}, + {-2,7}, + {-7,13} + } +}; + +const opus_int8 silk_nb_cbk_searchs_stage3[ SILK_PE_MAX_COMPLEX + 1 ] = +{ + PE_NB_CBKS_STAGE3_MIN, + PE_NB_CBKS_STAGE3_MID, + PE_NB_CBKS_STAGE3_MAX +}; diff --git a/port/third_party/opus/silk/process_NLSFs.c b/port/third_party/opus/silk/process_NLSFs.c new file mode 100644 index 00000000..d1308095 --- /dev/null +++ b/port/third_party/opus/silk/process_NLSFs.c @@ -0,0 +1,107 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Limit, stabilize, convert and quantize NLSFs */ +void silk_process_NLSFs( + silk_encoder_state *psEncC, /* I/O Encoder state */ + opus_int16 PredCoef_Q12[ 2 ][ MAX_LPC_ORDER ], /* O Prediction coefficients */ + opus_int16 pNLSF_Q15[ MAX_LPC_ORDER ], /* I/O Normalized LSFs (quant out) (0 - (2^15-1)) */ + const opus_int16 prev_NLSFq_Q15[ MAX_LPC_ORDER ] /* I Previous Normalized LSFs (0 - (2^15-1)) */ +) +{ + opus_int i, doInterpolate; + opus_int NLSF_mu_Q20; + opus_int16 i_sqr_Q15; + opus_int16 pNLSF0_temp_Q15[ MAX_LPC_ORDER ]; + opus_int16 pNLSFW_QW[ MAX_LPC_ORDER ]; + opus_int16 pNLSFW0_temp_QW[ MAX_LPC_ORDER ]; + + silk_assert( psEncC->speech_activity_Q8 >= 0 ); + silk_assert( psEncC->speech_activity_Q8 <= SILK_FIX_CONST( 1.0, 8 ) ); + celt_assert( psEncC->useInterpolatedNLSFs == 1 || psEncC->indices.NLSFInterpCoef_Q2 == ( 1 << 2 ) ); + + /***********************/ + /* Calculate mu values */ + /***********************/ + /* NLSF_mu = 0.003 - 0.0015 * psEnc->speech_activity; */ + NLSF_mu_Q20 = silk_SMLAWB( SILK_FIX_CONST( 0.003, 20 ), SILK_FIX_CONST( -0.001, 28 ), psEncC->speech_activity_Q8 ); + if( psEncC->nb_subfr == 2 ) { + /* Multiply by 1.5 for 10 ms packets */ + NLSF_mu_Q20 = silk_ADD_RSHIFT( NLSF_mu_Q20, NLSF_mu_Q20, 1 ); + } + + celt_assert( NLSF_mu_Q20 > 0 ); + silk_assert( NLSF_mu_Q20 <= SILK_FIX_CONST( 0.005, 20 ) ); + + /* Calculate NLSF weights */ + silk_NLSF_VQ_weights_laroia( pNLSFW_QW, pNLSF_Q15, psEncC->predictLPCOrder ); + + /* Update NLSF weights for interpolated NLSFs */ + doInterpolate = ( psEncC->useInterpolatedNLSFs == 1 ) && ( psEncC->indices.NLSFInterpCoef_Q2 < 4 ); + if( doInterpolate ) { + /* Calculate the interpolated NLSF vector for the first half */ + silk_interpolate( pNLSF0_temp_Q15, prev_NLSFq_Q15, pNLSF_Q15, + psEncC->indices.NLSFInterpCoef_Q2, psEncC->predictLPCOrder ); + + /* Calculate first half NLSF weights for the interpolated NLSFs */ + silk_NLSF_VQ_weights_laroia( pNLSFW0_temp_QW, pNLSF0_temp_Q15, psEncC->predictLPCOrder ); + + /* Update NLSF weights with contribution from first half */ + i_sqr_Q15 = silk_LSHIFT( silk_SMULBB( psEncC->indices.NLSFInterpCoef_Q2, psEncC->indices.NLSFInterpCoef_Q2 ), 11 ); + for( i = 0; i < psEncC->predictLPCOrder; i++ ) { + pNLSFW_QW[ i ] = silk_ADD16( silk_RSHIFT( pNLSFW_QW[ i ], 1 ), silk_RSHIFT( + silk_SMULBB( pNLSFW0_temp_QW[ i ], i_sqr_Q15 ), 16) ); + silk_assert( pNLSFW_QW[ i ] >= 1 ); + } + } + + silk_NLSF_encode( psEncC->indices.NLSFIndices, pNLSF_Q15, psEncC->psNLSF_CB, pNLSFW_QW, + NLSF_mu_Q20, psEncC->NLSF_MSVQ_Survivors, psEncC->indices.signalType ); + + /* Convert quantized NLSFs back to LPC coefficients */ + silk_NLSF2A( PredCoef_Q12[ 1 ], pNLSF_Q15, psEncC->predictLPCOrder, psEncC->arch ); + + if( doInterpolate ) { + /* Calculate the interpolated, quantized LSF vector for the first half */ + silk_interpolate( pNLSF0_temp_Q15, prev_NLSFq_Q15, pNLSF_Q15, + psEncC->indices.NLSFInterpCoef_Q2, psEncC->predictLPCOrder ); + + /* Convert back to LPC coefficients */ + silk_NLSF2A( PredCoef_Q12[ 0 ], pNLSF0_temp_Q15, psEncC->predictLPCOrder, psEncC->arch ); + + } else { + /* Copy LPC coefficients for first half from second half */ + celt_assert( psEncC->predictLPCOrder <= MAX_LPC_ORDER ); + silk_memcpy( PredCoef_Q12[ 0 ], PredCoef_Q12[ 1 ], psEncC->predictLPCOrder * sizeof( opus_int16 ) ); + } +} diff --git a/port/third_party/opus/silk/quant_LTP_gains.c b/port/third_party/opus/silk/quant_LTP_gains.c new file mode 100644 index 00000000..d6b8eff8 --- /dev/null +++ b/port/third_party/opus/silk/quant_LTP_gains.c @@ -0,0 +1,132 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "tuning_parameters.h" + +void silk_quant_LTP_gains( + opus_int16 B_Q14[ MAX_NB_SUBFR * LTP_ORDER ], /* O Quantized LTP gains */ + opus_int8 cbk_index[ MAX_NB_SUBFR ], /* O Codebook Index */ + opus_int8 *periodicity_index, /* O Periodicity Index */ + opus_int32 *sum_log_gain_Q7, /* I/O Cumulative max prediction gain */ + opus_int *pred_gain_dB_Q7, /* O LTP prediction gain */ + const opus_int32 XX_Q17[ MAX_NB_SUBFR*LTP_ORDER*LTP_ORDER ], /* I Correlation matrix in Q18 */ + const opus_int32 xX_Q17[ MAX_NB_SUBFR*LTP_ORDER ], /* I Correlation vector in Q18 */ + const opus_int subfr_len, /* I Number of samples per subframe */ + const opus_int nb_subfr, /* I Number of subframes */ + int arch /* I Run-time architecture */ +) +{ + opus_int j, k, cbk_size; + opus_int8 temp_idx[ MAX_NB_SUBFR ]; + const opus_uint8 *cl_ptr_Q5; + const opus_int8 *cbk_ptr_Q7; + const opus_uint8 *cbk_gain_ptr_Q7; + const opus_int32 *XX_Q17_ptr, *xX_Q17_ptr; + opus_int32 res_nrg_Q15_subfr, res_nrg_Q15, rate_dist_Q7_subfr, rate_dist_Q7, min_rate_dist_Q7; + opus_int32 sum_log_gain_tmp_Q7, best_sum_log_gain_Q7, max_gain_Q7; + opus_int gain_Q7; + + /***************************************************/ + /* iterate over different codebooks with different */ + /* rates/distortions, and choose best */ + /***************************************************/ + min_rate_dist_Q7 = silk_int32_MAX; + best_sum_log_gain_Q7 = 0; + for( k = 0; k < 3; k++ ) { + /* Safety margin for pitch gain control, to take into account factors + such as state rescaling/rewhitening. */ + opus_int32 gain_safety = SILK_FIX_CONST( 0.4, 7 ); + + cl_ptr_Q5 = silk_LTP_gain_BITS_Q5_ptrs[ k ]; + cbk_ptr_Q7 = silk_LTP_vq_ptrs_Q7[ k ]; + cbk_gain_ptr_Q7 = silk_LTP_vq_gain_ptrs_Q7[ k ]; + cbk_size = silk_LTP_vq_sizes[ k ]; + + /* Set up pointers to first subframe */ + XX_Q17_ptr = XX_Q17; + xX_Q17_ptr = xX_Q17; + + res_nrg_Q15 = 0; + rate_dist_Q7 = 0; + sum_log_gain_tmp_Q7 = *sum_log_gain_Q7; + for( j = 0; j < nb_subfr; j++ ) { + max_gain_Q7 = silk_log2lin( ( SILK_FIX_CONST( MAX_SUM_LOG_GAIN_DB / 6.0, 7 ) - sum_log_gain_tmp_Q7 ) + + SILK_FIX_CONST( 7, 7 ) ) - gain_safety; + silk_VQ_WMat_EC( + &temp_idx[ j ], /* O index of best codebook vector */ + &res_nrg_Q15_subfr, /* O residual energy */ + &rate_dist_Q7_subfr, /* O best weighted quantization error + mu * rate */ + &gain_Q7, /* O sum of absolute LTP coefficients */ + XX_Q17_ptr, /* I correlation matrix */ + xX_Q17_ptr, /* I correlation vector */ + cbk_ptr_Q7, /* I codebook */ + cbk_gain_ptr_Q7, /* I codebook effective gains */ + cl_ptr_Q5, /* I code length for each codebook vector */ + subfr_len, /* I number of samples per subframe */ + max_gain_Q7, /* I maximum sum of absolute LTP coefficients */ + cbk_size, /* I number of vectors in codebook */ + arch /* I Run-time architecture */ + ); + + res_nrg_Q15 = silk_ADD_POS_SAT32( res_nrg_Q15, res_nrg_Q15_subfr ); + rate_dist_Q7 = silk_ADD_POS_SAT32( rate_dist_Q7, rate_dist_Q7_subfr ); + sum_log_gain_tmp_Q7 = silk_max(0, sum_log_gain_tmp_Q7 + + silk_lin2log( gain_safety + gain_Q7 ) - SILK_FIX_CONST( 7, 7 )); + + XX_Q17_ptr += LTP_ORDER * LTP_ORDER; + xX_Q17_ptr += LTP_ORDER; + } + + if( rate_dist_Q7 <= min_rate_dist_Q7 ) { + min_rate_dist_Q7 = rate_dist_Q7; + *periodicity_index = (opus_int8)k; + silk_memcpy( cbk_index, temp_idx, nb_subfr * sizeof( opus_int8 ) ); + best_sum_log_gain_Q7 = sum_log_gain_tmp_Q7; + } + } + + cbk_ptr_Q7 = silk_LTP_vq_ptrs_Q7[ *periodicity_index ]; + for( j = 0; j < nb_subfr; j++ ) { + for( k = 0; k < LTP_ORDER; k++ ) { + B_Q14[ j * LTP_ORDER + k ] = silk_LSHIFT( cbk_ptr_Q7[ cbk_index[ j ] * LTP_ORDER + k ], 7 ); + } + } + + if( nb_subfr == 2 ) { + res_nrg_Q15 = silk_RSHIFT32( res_nrg_Q15, 1 ); + } else { + res_nrg_Q15 = silk_RSHIFT32( res_nrg_Q15, 2 ); + } + + *sum_log_gain_Q7 = best_sum_log_gain_Q7; + *pred_gain_dB_Q7 = (opus_int)silk_SMULBB( -3, silk_lin2log( res_nrg_Q15 ) - ( 15 << 7 ) ); +} diff --git a/port/third_party/opus/silk/resampler.c b/port/third_party/opus/silk/resampler.c new file mode 100644 index 00000000..1f11e508 --- /dev/null +++ b/port/third_party/opus/silk/resampler.c @@ -0,0 +1,215 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/* + * Matrix of resampling methods used: + * Fs_out (kHz) + * 8 12 16 24 48 + * + * 8 C UF U UF UF + * 12 AF C UF U UF + * Fs_in (kHz) 16 D AF C UF UF + * 24 AF D AF C U + * 48 AF AF AF D C + * + * C -> Copy (no resampling) + * D -> Allpass-based 2x downsampling + * U -> Allpass-based 2x upsampling + * UF -> Allpass-based 2x upsampling followed by FIR interpolation + * AF -> AR2 filter followed by FIR interpolation + */ + +#include "resampler_private.h" + +/* Tables with delay compensation values to equalize total delay for different modes */ +static const opus_int8 delay_matrix_enc[ 5 ][ 3 ] = { +/* in \ out 8 12 16 */ +/* 8 */ { 6, 0, 3 }, +/* 12 */ { 0, 7, 3 }, +/* 16 */ { 0, 1, 10 }, +/* 24 */ { 0, 2, 6 }, +/* 48 */ { 18, 10, 12 } +}; + +static const opus_int8 delay_matrix_dec[ 3 ][ 5 ] = { +/* in \ out 8 12 16 24 48 */ +/* 8 */ { 4, 0, 2, 0, 0 }, +/* 12 */ { 0, 9, 4, 7, 4 }, +/* 16 */ { 0, 3, 12, 7, 7 } +}; + +/* Simple way to make [8000, 12000, 16000, 24000, 48000] to [0, 1, 2, 3, 4] */ +#define rateID(R) ( ( ( ((R)>>12) - ((R)>16000) ) >> ((R)>24000) ) - 1 ) + +#define USE_silk_resampler_copy (0) +#define USE_silk_resampler_private_up2_HQ_wrapper (1) +#define USE_silk_resampler_private_IIR_FIR (2) +#define USE_silk_resampler_private_down_FIR (3) + +/* Initialize/reset the resampler state for a given pair of input/output sampling rates */ +opus_int silk_resampler_init( + silk_resampler_state_struct *S, /* I/O Resampler state */ + opus_int32 Fs_Hz_in, /* I Input sampling rate (Hz) */ + opus_int32 Fs_Hz_out, /* I Output sampling rate (Hz) */ + opus_int forEnc /* I If 1: encoder; if 0: decoder */ +) +{ + opus_int up2x; + + /* Clear state */ + silk_memset( S, 0, sizeof( silk_resampler_state_struct ) ); + + /* Input checking */ + if( forEnc ) { + if( ( Fs_Hz_in != 8000 && Fs_Hz_in != 12000 && Fs_Hz_in != 16000 && Fs_Hz_in != 24000 && Fs_Hz_in != 48000 ) || + ( Fs_Hz_out != 8000 && Fs_Hz_out != 12000 && Fs_Hz_out != 16000 ) ) { + celt_assert( 0 ); + return -1; + } + S->inputDelay = delay_matrix_enc[ rateID( Fs_Hz_in ) ][ rateID( Fs_Hz_out ) ]; + } else { + if( ( Fs_Hz_in != 8000 && Fs_Hz_in != 12000 && Fs_Hz_in != 16000 ) || + ( Fs_Hz_out != 8000 && Fs_Hz_out != 12000 && Fs_Hz_out != 16000 && Fs_Hz_out != 24000 && Fs_Hz_out != 48000 ) ) { + celt_assert( 0 ); + return -1; + } + S->inputDelay = delay_matrix_dec[ rateID( Fs_Hz_in ) ][ rateID( Fs_Hz_out ) ]; + } + + S->Fs_in_kHz = silk_DIV32_16( Fs_Hz_in, 1000 ); + S->Fs_out_kHz = silk_DIV32_16( Fs_Hz_out, 1000 ); + + /* Number of samples processed per batch */ + S->batchSize = S->Fs_in_kHz * RESAMPLER_MAX_BATCH_SIZE_MS; + + /* Find resampler with the right sampling ratio */ + up2x = 0; + if( Fs_Hz_out > Fs_Hz_in ) { + /* Upsample */ + if( Fs_Hz_out == silk_MUL( Fs_Hz_in, 2 ) ) { /* Fs_out : Fs_in = 2 : 1 */ + /* Special case: directly use 2x upsampler */ + S->resampler_function = USE_silk_resampler_private_up2_HQ_wrapper; + } else { + /* Default resampler */ + S->resampler_function = USE_silk_resampler_private_IIR_FIR; + up2x = 1; + } + } else if ( Fs_Hz_out < Fs_Hz_in ) { + /* Downsample */ + S->resampler_function = USE_silk_resampler_private_down_FIR; + if( silk_MUL( Fs_Hz_out, 4 ) == silk_MUL( Fs_Hz_in, 3 ) ) { /* Fs_out : Fs_in = 3 : 4 */ + S->FIR_Fracs = 3; + S->FIR_Order = RESAMPLER_DOWN_ORDER_FIR0; + S->Coefs = silk_Resampler_3_4_COEFS; + } else if( silk_MUL( Fs_Hz_out, 3 ) == silk_MUL( Fs_Hz_in, 2 ) ) { /* Fs_out : Fs_in = 2 : 3 */ + S->FIR_Fracs = 2; + S->FIR_Order = RESAMPLER_DOWN_ORDER_FIR0; + S->Coefs = silk_Resampler_2_3_COEFS; + } else if( silk_MUL( Fs_Hz_out, 2 ) == Fs_Hz_in ) { /* Fs_out : Fs_in = 1 : 2 */ + S->FIR_Fracs = 1; + S->FIR_Order = RESAMPLER_DOWN_ORDER_FIR1; + S->Coefs = silk_Resampler_1_2_COEFS; + } else if( silk_MUL( Fs_Hz_out, 3 ) == Fs_Hz_in ) { /* Fs_out : Fs_in = 1 : 3 */ + S->FIR_Fracs = 1; + S->FIR_Order = RESAMPLER_DOWN_ORDER_FIR2; + S->Coefs = silk_Resampler_1_3_COEFS; + } else if( silk_MUL( Fs_Hz_out, 4 ) == Fs_Hz_in ) { /* Fs_out : Fs_in = 1 : 4 */ + S->FIR_Fracs = 1; + S->FIR_Order = RESAMPLER_DOWN_ORDER_FIR2; + S->Coefs = silk_Resampler_1_4_COEFS; + } else if( silk_MUL( Fs_Hz_out, 6 ) == Fs_Hz_in ) { /* Fs_out : Fs_in = 1 : 6 */ + S->FIR_Fracs = 1; + S->FIR_Order = RESAMPLER_DOWN_ORDER_FIR2; + S->Coefs = silk_Resampler_1_6_COEFS; + } else { + /* None available */ + celt_assert( 0 ); + return -1; + } + } else { + /* Input and output sampling rates are equal: copy */ + S->resampler_function = USE_silk_resampler_copy; + } + + /* Ratio of input/output samples */ + S->invRatio_Q16 = silk_LSHIFT32( silk_DIV32( silk_LSHIFT32( Fs_Hz_in, 14 + up2x ), Fs_Hz_out ), 2 ); + /* Make sure the ratio is rounded up */ + while( silk_SMULWW( S->invRatio_Q16, Fs_Hz_out ) < silk_LSHIFT32( Fs_Hz_in, up2x ) ) { + S->invRatio_Q16++; + } + + return 0; +} + +/* Resampler: convert from one sampling rate to another */ +/* Input and output sampling rate are at most 48000 Hz */ +opus_int silk_resampler( + silk_resampler_state_struct *S, /* I/O Resampler state */ + opus_int16 out[], /* O Output signal */ + const opus_int16 in[], /* I Input signal */ + opus_int32 inLen /* I Number of input samples */ +) +{ + opus_int nSamples; + + /* Need at least 1 ms of input data */ + celt_assert( inLen >= S->Fs_in_kHz ); + /* Delay can't exceed the 1 ms of buffering */ + celt_assert( S->inputDelay <= S->Fs_in_kHz ); + + nSamples = S->Fs_in_kHz - S->inputDelay; + + /* Copy to delay buffer */ + silk_memcpy( &S->delayBuf[ S->inputDelay ], in, nSamples * sizeof( opus_int16 ) ); + + switch( S->resampler_function ) { + case USE_silk_resampler_private_up2_HQ_wrapper: + silk_resampler_private_up2_HQ_wrapper( S, out, S->delayBuf, S->Fs_in_kHz ); + silk_resampler_private_up2_HQ_wrapper( S, &out[ S->Fs_out_kHz ], &in[ nSamples ], inLen - S->Fs_in_kHz ); + break; + case USE_silk_resampler_private_IIR_FIR: + silk_resampler_private_IIR_FIR( S, out, S->delayBuf, S->Fs_in_kHz ); + silk_resampler_private_IIR_FIR( S, &out[ S->Fs_out_kHz ], &in[ nSamples ], inLen - S->Fs_in_kHz ); + break; + case USE_silk_resampler_private_down_FIR: + silk_resampler_private_down_FIR( S, out, S->delayBuf, S->Fs_in_kHz ); + silk_resampler_private_down_FIR( S, &out[ S->Fs_out_kHz ], &in[ nSamples ], inLen - S->Fs_in_kHz ); + break; + default: + silk_memcpy( out, S->delayBuf, S->Fs_in_kHz * sizeof( opus_int16 ) ); + silk_memcpy( &out[ S->Fs_out_kHz ], &in[ nSamples ], ( inLen - S->Fs_in_kHz ) * sizeof( opus_int16 ) ); + } + + /* Copy to delay buffer */ + silk_memcpy( S->delayBuf, &in[ inLen - S->inputDelay ], S->inputDelay * sizeof( opus_int16 ) ); + + return 0; +} diff --git a/port/third_party/opus/silk/resampler_down2.c b/port/third_party/opus/silk/resampler_down2.c new file mode 100644 index 00000000..971d7bfd --- /dev/null +++ b/port/third_party/opus/silk/resampler_down2.c @@ -0,0 +1,74 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "resampler_rom.h" + +/* Downsample by a factor 2 */ +void silk_resampler_down2( + opus_int32 *S, /* I/O State vector [ 2 ] */ + opus_int16 *out, /* O Output signal [ floor(len/2) ] */ + const opus_int16 *in, /* I Input signal [ len ] */ + opus_int32 inLen /* I Number of input samples */ +) +{ + opus_int32 k, len2 = silk_RSHIFT32( inLen, 1 ); + opus_int32 in32, out32, Y, X; + + celt_assert( silk_resampler_down2_0 > 0 ); + celt_assert( silk_resampler_down2_1 < 0 ); + + /* Internal variables and state are in Q10 format */ + for( k = 0; k < len2; k++ ) { + /* Convert to Q10 */ + in32 = silk_LSHIFT( (opus_int32)in[ 2 * k ], 10 ); + + /* All-pass section for even input sample */ + Y = silk_SUB32( in32, S[ 0 ] ); + X = silk_SMLAWB( Y, Y, silk_resampler_down2_1 ); + out32 = silk_ADD32( S[ 0 ], X ); + S[ 0 ] = silk_ADD32( in32, X ); + + /* Convert to Q10 */ + in32 = silk_LSHIFT( (opus_int32)in[ 2 * k + 1 ], 10 ); + + /* All-pass section for odd input sample, and add to output of previous section */ + Y = silk_SUB32( in32, S[ 1 ] ); + X = silk_SMULWB( Y, silk_resampler_down2_0 ); + out32 = silk_ADD32( out32, S[ 1 ] ); + out32 = silk_ADD32( out32, X ); + S[ 1 ] = silk_ADD32( in32, X ); + + /* Add, convert back to int16 and store to output */ + out[ k ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( out32, 11 ) ); + } +} + diff --git a/port/third_party/opus/silk/resampler_down2_3.c b/port/third_party/opus/silk/resampler_down2_3.c new file mode 100644 index 00000000..4342614d --- /dev/null +++ b/port/third_party/opus/silk/resampler_down2_3.c @@ -0,0 +1,103 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "resampler_private.h" +#include "stack_alloc.h" + +#define ORDER_FIR 4 + +/* Downsample by a factor 2/3, low quality */ +void silk_resampler_down2_3( + opus_int32 *S, /* I/O State vector [ 6 ] */ + opus_int16 *out, /* O Output signal [ floor(2*inLen/3) ] */ + const opus_int16 *in, /* I Input signal [ inLen ] */ + opus_int32 inLen /* I Number of input samples */ +) +{ + opus_int32 nSamplesIn, counter, res_Q6; + VARDECL( opus_int32, buf ); + opus_int32 *buf_ptr; + SAVE_STACK; + + ALLOC( buf, RESAMPLER_MAX_BATCH_SIZE_IN + ORDER_FIR, opus_int32 ); + + /* Copy buffered samples to start of buffer */ + silk_memcpy( buf, S, ORDER_FIR * sizeof( opus_int32 ) ); + + /* Iterate over blocks of frameSizeIn input samples */ + while( 1 ) { + nSamplesIn = silk_min( inLen, RESAMPLER_MAX_BATCH_SIZE_IN ); + + /* Second-order AR filter (output in Q8) */ + silk_resampler_private_AR2( &S[ ORDER_FIR ], &buf[ ORDER_FIR ], in, + silk_Resampler_2_3_COEFS_LQ, nSamplesIn ); + + /* Interpolate filtered signal */ + buf_ptr = buf; + counter = nSamplesIn; + while( counter > 2 ) { + /* Inner product */ + res_Q6 = silk_SMULWB( buf_ptr[ 0 ], silk_Resampler_2_3_COEFS_LQ[ 2 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 1 ], silk_Resampler_2_3_COEFS_LQ[ 3 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 2 ], silk_Resampler_2_3_COEFS_LQ[ 5 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 3 ], silk_Resampler_2_3_COEFS_LQ[ 4 ] ); + + /* Scale down, saturate and store in output array */ + *out++ = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( res_Q6, 6 ) ); + + res_Q6 = silk_SMULWB( buf_ptr[ 1 ], silk_Resampler_2_3_COEFS_LQ[ 4 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 2 ], silk_Resampler_2_3_COEFS_LQ[ 5 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 3 ], silk_Resampler_2_3_COEFS_LQ[ 3 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 4 ], silk_Resampler_2_3_COEFS_LQ[ 2 ] ); + + /* Scale down, saturate and store in output array */ + *out++ = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( res_Q6, 6 ) ); + + buf_ptr += 3; + counter -= 3; + } + + in += nSamplesIn; + inLen -= nSamplesIn; + + if( inLen > 0 ) { + /* More iterations to do; copy last part of filtered signal to beginning of buffer */ + silk_memcpy( buf, &buf[ nSamplesIn ], ORDER_FIR * sizeof( opus_int32 ) ); + } else { + break; + } + } + + /* Copy last part of filtered signal to the state for the next call */ + silk_memcpy( S, &buf[ nSamplesIn ], ORDER_FIR * sizeof( opus_int32 ) ); + RESTORE_STACK; +} diff --git a/port/third_party/opus/silk/resampler_private.h b/port/third_party/opus/silk/resampler_private.h new file mode 100644 index 00000000..422a7d9d --- /dev/null +++ b/port/third_party/opus/silk/resampler_private.h @@ -0,0 +1,88 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_RESAMPLER_PRIVATE_H +#define SILK_RESAMPLER_PRIVATE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "SigProc_FIX.h" +#include "resampler_structs.h" +#include "resampler_rom.h" + +/* Number of input samples to process in the inner loop */ +#define RESAMPLER_MAX_BATCH_SIZE_MS 10 +#define RESAMPLER_MAX_FS_KHZ 48 +#define RESAMPLER_MAX_BATCH_SIZE_IN ( RESAMPLER_MAX_BATCH_SIZE_MS * RESAMPLER_MAX_FS_KHZ ) + +/* Description: Hybrid IIR/FIR polyphase implementation of resampling */ +void silk_resampler_private_IIR_FIR( + void *SS, /* I/O Resampler state */ + opus_int16 out[], /* O Output signal */ + const opus_int16 in[], /* I Input signal */ + opus_int32 inLen /* I Number of input samples */ +); + +/* Description: Hybrid IIR/FIR polyphase implementation of resampling */ +void silk_resampler_private_down_FIR( + void *SS, /* I/O Resampler state */ + opus_int16 out[], /* O Output signal */ + const opus_int16 in[], /* I Input signal */ + opus_int32 inLen /* I Number of input samples */ +); + +/* Upsample by a factor 2, high quality */ +void silk_resampler_private_up2_HQ_wrapper( + void *SS, /* I/O Resampler state (unused) */ + opus_int16 *out, /* O Output signal [ 2 * len ] */ + const opus_int16 *in, /* I Input signal [ len ] */ + opus_int32 len /* I Number of input samples */ +); + +/* Upsample by a factor 2, high quality */ +void silk_resampler_private_up2_HQ( + opus_int32 *S, /* I/O Resampler state [ 6 ] */ + opus_int16 *out, /* O Output signal [ 2 * len ] */ + const opus_int16 *in, /* I Input signal [ len ] */ + opus_int32 len /* I Number of input samples */ +); + +/* Second order AR filter */ +void silk_resampler_private_AR2( + opus_int32 S[], /* I/O State vector [ 2 ] */ + opus_int32 out_Q8[], /* O Output signal */ + const opus_int16 in[], /* I Input signal */ + const opus_int16 A_Q14[], /* I AR coefficients, Q14 */ + opus_int32 len /* I Signal length */ +); + +#ifdef __cplusplus +} +#endif +#endif /* SILK_RESAMPLER_PRIVATE_H */ diff --git a/port/third_party/opus/silk/resampler_private_AR2.c b/port/third_party/opus/silk/resampler_private_AR2.c new file mode 100644 index 00000000..5fff2371 --- /dev/null +++ b/port/third_party/opus/silk/resampler_private_AR2.c @@ -0,0 +1,55 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "resampler_private.h" + +/* Second order AR filter with single delay elements */ +void silk_resampler_private_AR2( + opus_int32 S[], /* I/O State vector [ 2 ] */ + opus_int32 out_Q8[], /* O Output signal */ + const opus_int16 in[], /* I Input signal */ + const opus_int16 A_Q14[], /* I AR coefficients, Q14 */ + opus_int32 len /* I Signal length */ +) +{ + opus_int32 k; + opus_int32 out32; + + for( k = 0; k < len; k++ ) { + out32 = silk_ADD_LSHIFT32( S[ 0 ], (opus_int32)in[ k ], 8 ); + out_Q8[ k ] = out32; + out32 = silk_LSHIFT( out32, 2 ); + S[ 0 ] = silk_SMLAWB( S[ 1 ], out32, A_Q14[ 0 ] ); + S[ 1 ] = silk_SMULWB( out32, A_Q14[ 1 ] ); + } +} + diff --git a/port/third_party/opus/silk/resampler_private_IIR_FIR.c b/port/third_party/opus/silk/resampler_private_IIR_FIR.c new file mode 100644 index 00000000..6b2b3a2e --- /dev/null +++ b/port/third_party/opus/silk/resampler_private_IIR_FIR.c @@ -0,0 +1,107 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "resampler_private.h" +#include "stack_alloc.h" + +static OPUS_INLINE opus_int16 *silk_resampler_private_IIR_FIR_INTERPOL( + opus_int16 *out, + opus_int16 *buf, + opus_int32 max_index_Q16, + opus_int32 index_increment_Q16 +) +{ + opus_int32 index_Q16, res_Q15; + opus_int16 *buf_ptr; + opus_int32 table_index; + + /* Interpolate upsampled signal and store in output array */ + for( index_Q16 = 0; index_Q16 < max_index_Q16; index_Q16 += index_increment_Q16 ) { + table_index = silk_SMULWB( index_Q16 & 0xFFFF, 12 ); + buf_ptr = &buf[ index_Q16 >> 16 ]; + + res_Q15 = silk_SMULBB( buf_ptr[ 0 ], silk_resampler_frac_FIR_12[ table_index ][ 0 ] ); + res_Q15 = silk_SMLABB( res_Q15, buf_ptr[ 1 ], silk_resampler_frac_FIR_12[ table_index ][ 1 ] ); + res_Q15 = silk_SMLABB( res_Q15, buf_ptr[ 2 ], silk_resampler_frac_FIR_12[ table_index ][ 2 ] ); + res_Q15 = silk_SMLABB( res_Q15, buf_ptr[ 3 ], silk_resampler_frac_FIR_12[ table_index ][ 3 ] ); + res_Q15 = silk_SMLABB( res_Q15, buf_ptr[ 4 ], silk_resampler_frac_FIR_12[ 11 - table_index ][ 3 ] ); + res_Q15 = silk_SMLABB( res_Q15, buf_ptr[ 5 ], silk_resampler_frac_FIR_12[ 11 - table_index ][ 2 ] ); + res_Q15 = silk_SMLABB( res_Q15, buf_ptr[ 6 ], silk_resampler_frac_FIR_12[ 11 - table_index ][ 1 ] ); + res_Q15 = silk_SMLABB( res_Q15, buf_ptr[ 7 ], silk_resampler_frac_FIR_12[ 11 - table_index ][ 0 ] ); + *out++ = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( res_Q15, 15 ) ); + } + return out; +} +/* Upsample using a combination of allpass-based 2x upsampling and FIR interpolation */ +void silk_resampler_private_IIR_FIR( + void *SS, /* I/O Resampler state */ + opus_int16 out[], /* O Output signal */ + const opus_int16 in[], /* I Input signal */ + opus_int32 inLen /* I Number of input samples */ +) +{ + silk_resampler_state_struct *S = (silk_resampler_state_struct *)SS; + opus_int32 nSamplesIn; + opus_int32 max_index_Q16, index_increment_Q16; + VARDECL( opus_int16, buf ); + SAVE_STACK; + + ALLOC( buf, 2 * S->batchSize + RESAMPLER_ORDER_FIR_12, opus_int16 ); + + /* Copy buffered samples to start of buffer */ + silk_memcpy( buf, S->sFIR.i16, RESAMPLER_ORDER_FIR_12 * sizeof( opus_int16 ) ); + + /* Iterate over blocks of frameSizeIn input samples */ + index_increment_Q16 = S->invRatio_Q16; + while( 1 ) { + nSamplesIn = silk_min( inLen, S->batchSize ); + + /* Upsample 2x */ + silk_resampler_private_up2_HQ( S->sIIR, &buf[ RESAMPLER_ORDER_FIR_12 ], in, nSamplesIn ); + + max_index_Q16 = silk_LSHIFT32( nSamplesIn, 16 + 1 ); /* + 1 because 2x upsampling */ + out = silk_resampler_private_IIR_FIR_INTERPOL( out, buf, max_index_Q16, index_increment_Q16 ); + in += nSamplesIn; + inLen -= nSamplesIn; + + if( inLen > 0 ) { + /* More iterations to do; copy last part of filtered signal to beginning of buffer */ + silk_memcpy( buf, &buf[ nSamplesIn << 1 ], RESAMPLER_ORDER_FIR_12 * sizeof( opus_int16 ) ); + } else { + break; + } + } + + /* Copy last part of filtered signal to the state for the next call */ + silk_memcpy( S->sFIR.i16, &buf[ nSamplesIn << 1 ], RESAMPLER_ORDER_FIR_12 * sizeof( opus_int16 ) ); + RESTORE_STACK; +} diff --git a/port/third_party/opus/silk/resampler_private_down_FIR.c b/port/third_party/opus/silk/resampler_private_down_FIR.c new file mode 100644 index 00000000..3e8735a3 --- /dev/null +++ b/port/third_party/opus/silk/resampler_private_down_FIR.c @@ -0,0 +1,194 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "resampler_private.h" +#include "stack_alloc.h" + +static OPUS_INLINE opus_int16 *silk_resampler_private_down_FIR_INTERPOL( + opus_int16 *out, + opus_int32 *buf, + const opus_int16 *FIR_Coefs, + opus_int FIR_Order, + opus_int FIR_Fracs, + opus_int32 max_index_Q16, + opus_int32 index_increment_Q16 +) +{ + opus_int32 index_Q16, res_Q6; + opus_int32 *buf_ptr; + opus_int32 interpol_ind; + const opus_int16 *interpol_ptr; + + switch( FIR_Order ) { + case RESAMPLER_DOWN_ORDER_FIR0: + for( index_Q16 = 0; index_Q16 < max_index_Q16; index_Q16 += index_increment_Q16 ) { + /* Integer part gives pointer to buffered input */ + buf_ptr = buf + silk_RSHIFT( index_Q16, 16 ); + + /* Fractional part gives interpolation coefficients */ + interpol_ind = silk_SMULWB( index_Q16 & 0xFFFF, FIR_Fracs ); + + /* Inner product */ + interpol_ptr = &FIR_Coefs[ RESAMPLER_DOWN_ORDER_FIR0 / 2 * interpol_ind ]; + res_Q6 = silk_SMULWB( buf_ptr[ 0 ], interpol_ptr[ 0 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 1 ], interpol_ptr[ 1 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 2 ], interpol_ptr[ 2 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 3 ], interpol_ptr[ 3 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 4 ], interpol_ptr[ 4 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 5 ], interpol_ptr[ 5 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 6 ], interpol_ptr[ 6 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 7 ], interpol_ptr[ 7 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 8 ], interpol_ptr[ 8 ] ); + interpol_ptr = &FIR_Coefs[ RESAMPLER_DOWN_ORDER_FIR0 / 2 * ( FIR_Fracs - 1 - interpol_ind ) ]; + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 17 ], interpol_ptr[ 0 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 16 ], interpol_ptr[ 1 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 15 ], interpol_ptr[ 2 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 14 ], interpol_ptr[ 3 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 13 ], interpol_ptr[ 4 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 12 ], interpol_ptr[ 5 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 11 ], interpol_ptr[ 6 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 10 ], interpol_ptr[ 7 ] ); + res_Q6 = silk_SMLAWB( res_Q6, buf_ptr[ 9 ], interpol_ptr[ 8 ] ); + + /* Scale down, saturate and store in output array */ + *out++ = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( res_Q6, 6 ) ); + } + break; + case RESAMPLER_DOWN_ORDER_FIR1: + for( index_Q16 = 0; index_Q16 < max_index_Q16; index_Q16 += index_increment_Q16 ) { + /* Integer part gives pointer to buffered input */ + buf_ptr = buf + silk_RSHIFT( index_Q16, 16 ); + + /* Inner product */ + res_Q6 = silk_SMULWB( silk_ADD32( buf_ptr[ 0 ], buf_ptr[ 23 ] ), FIR_Coefs[ 0 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 1 ], buf_ptr[ 22 ] ), FIR_Coefs[ 1 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 2 ], buf_ptr[ 21 ] ), FIR_Coefs[ 2 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 3 ], buf_ptr[ 20 ] ), FIR_Coefs[ 3 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 4 ], buf_ptr[ 19 ] ), FIR_Coefs[ 4 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 5 ], buf_ptr[ 18 ] ), FIR_Coefs[ 5 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 6 ], buf_ptr[ 17 ] ), FIR_Coefs[ 6 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 7 ], buf_ptr[ 16 ] ), FIR_Coefs[ 7 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 8 ], buf_ptr[ 15 ] ), FIR_Coefs[ 8 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 9 ], buf_ptr[ 14 ] ), FIR_Coefs[ 9 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 10 ], buf_ptr[ 13 ] ), FIR_Coefs[ 10 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 11 ], buf_ptr[ 12 ] ), FIR_Coefs[ 11 ] ); + + /* Scale down, saturate and store in output array */ + *out++ = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( res_Q6, 6 ) ); + } + break; + case RESAMPLER_DOWN_ORDER_FIR2: + for( index_Q16 = 0; index_Q16 < max_index_Q16; index_Q16 += index_increment_Q16 ) { + /* Integer part gives pointer to buffered input */ + buf_ptr = buf + silk_RSHIFT( index_Q16, 16 ); + + /* Inner product */ + res_Q6 = silk_SMULWB( silk_ADD32( buf_ptr[ 0 ], buf_ptr[ 35 ] ), FIR_Coefs[ 0 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 1 ], buf_ptr[ 34 ] ), FIR_Coefs[ 1 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 2 ], buf_ptr[ 33 ] ), FIR_Coefs[ 2 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 3 ], buf_ptr[ 32 ] ), FIR_Coefs[ 3 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 4 ], buf_ptr[ 31 ] ), FIR_Coefs[ 4 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 5 ], buf_ptr[ 30 ] ), FIR_Coefs[ 5 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 6 ], buf_ptr[ 29 ] ), FIR_Coefs[ 6 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 7 ], buf_ptr[ 28 ] ), FIR_Coefs[ 7 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 8 ], buf_ptr[ 27 ] ), FIR_Coefs[ 8 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 9 ], buf_ptr[ 26 ] ), FIR_Coefs[ 9 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 10 ], buf_ptr[ 25 ] ), FIR_Coefs[ 10 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 11 ], buf_ptr[ 24 ] ), FIR_Coefs[ 11 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 12 ], buf_ptr[ 23 ] ), FIR_Coefs[ 12 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 13 ], buf_ptr[ 22 ] ), FIR_Coefs[ 13 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 14 ], buf_ptr[ 21 ] ), FIR_Coefs[ 14 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 15 ], buf_ptr[ 20 ] ), FIR_Coefs[ 15 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 16 ], buf_ptr[ 19 ] ), FIR_Coefs[ 16 ] ); + res_Q6 = silk_SMLAWB( res_Q6, silk_ADD32( buf_ptr[ 17 ], buf_ptr[ 18 ] ), FIR_Coefs[ 17 ] ); + + /* Scale down, saturate and store in output array */ + *out++ = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( res_Q6, 6 ) ); + } + break; + default: + celt_assert( 0 ); + } + return out; +} + +/* Resample with a 2nd order AR filter followed by FIR interpolation */ +void silk_resampler_private_down_FIR( + void *SS, /* I/O Resampler state */ + opus_int16 out[], /* O Output signal */ + const opus_int16 in[], /* I Input signal */ + opus_int32 inLen /* I Number of input samples */ +) +{ + silk_resampler_state_struct *S = (silk_resampler_state_struct *)SS; + opus_int32 nSamplesIn; + opus_int32 max_index_Q16, index_increment_Q16; + VARDECL( opus_int32, buf ); + const opus_int16 *FIR_Coefs; + SAVE_STACK; + + ALLOC( buf, S->batchSize + S->FIR_Order, opus_int32 ); + + /* Copy buffered samples to start of buffer */ + silk_memcpy( buf, S->sFIR.i32, S->FIR_Order * sizeof( opus_int32 ) ); + + FIR_Coefs = &S->Coefs[ 2 ]; + + /* Iterate over blocks of frameSizeIn input samples */ + index_increment_Q16 = S->invRatio_Q16; + while( 1 ) { + nSamplesIn = silk_min( inLen, S->batchSize ); + + /* Second-order AR filter (output in Q8) */ + silk_resampler_private_AR2( S->sIIR, &buf[ S->FIR_Order ], in, S->Coefs, nSamplesIn ); + + max_index_Q16 = silk_LSHIFT32( nSamplesIn, 16 ); + + /* Interpolate filtered signal */ + out = silk_resampler_private_down_FIR_INTERPOL( out, buf, FIR_Coefs, S->FIR_Order, + S->FIR_Fracs, max_index_Q16, index_increment_Q16 ); + + in += nSamplesIn; + inLen -= nSamplesIn; + + if( inLen > 1 ) { + /* More iterations to do; copy last part of filtered signal to beginning of buffer */ + silk_memcpy( buf, &buf[ nSamplesIn ], S->FIR_Order * sizeof( opus_int32 ) ); + } else { + break; + } + } + + /* Copy last part of filtered signal to the state for the next call */ + silk_memcpy( S->sFIR.i32, &buf[ nSamplesIn ], S->FIR_Order * sizeof( opus_int32 ) ); + RESTORE_STACK; +} diff --git a/port/third_party/opus/silk/resampler_private_up2_HQ.c b/port/third_party/opus/silk/resampler_private_up2_HQ.c new file mode 100644 index 00000000..c7ec8de3 --- /dev/null +++ b/port/third_party/opus/silk/resampler_private_up2_HQ.c @@ -0,0 +1,113 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" +#include "resampler_private.h" + +/* Upsample by a factor 2, high quality */ +/* Uses 2nd order allpass filters for the 2x upsampling, followed by a */ +/* notch filter just above Nyquist. */ +void silk_resampler_private_up2_HQ( + opus_int32 *S, /* I/O Resampler state [ 6 ] */ + opus_int16 *out, /* O Output signal [ 2 * len ] */ + const opus_int16 *in, /* I Input signal [ len ] */ + opus_int32 len /* I Number of input samples */ +) +{ + opus_int32 k; + opus_int32 in32, out32_1, out32_2, Y, X; + + silk_assert( silk_resampler_up2_hq_0[ 0 ] > 0 ); + silk_assert( silk_resampler_up2_hq_0[ 1 ] > 0 ); + silk_assert( silk_resampler_up2_hq_0[ 2 ] < 0 ); + silk_assert( silk_resampler_up2_hq_1[ 0 ] > 0 ); + silk_assert( silk_resampler_up2_hq_1[ 1 ] > 0 ); + silk_assert( silk_resampler_up2_hq_1[ 2 ] < 0 ); + + /* Internal variables and state are in Q10 format */ + for( k = 0; k < len; k++ ) { + /* Convert to Q10 */ + in32 = silk_LSHIFT( (opus_int32)in[ k ], 10 ); + + /* First all-pass section for even output sample */ + Y = silk_SUB32( in32, S[ 0 ] ); + X = silk_SMULWB( Y, silk_resampler_up2_hq_0[ 0 ] ); + out32_1 = silk_ADD32( S[ 0 ], X ); + S[ 0 ] = silk_ADD32( in32, X ); + + /* Second all-pass section for even output sample */ + Y = silk_SUB32( out32_1, S[ 1 ] ); + X = silk_SMULWB( Y, silk_resampler_up2_hq_0[ 1 ] ); + out32_2 = silk_ADD32( S[ 1 ], X ); + S[ 1 ] = silk_ADD32( out32_1, X ); + + /* Third all-pass section for even output sample */ + Y = silk_SUB32( out32_2, S[ 2 ] ); + X = silk_SMLAWB( Y, Y, silk_resampler_up2_hq_0[ 2 ] ); + out32_1 = silk_ADD32( S[ 2 ], X ); + S[ 2 ] = silk_ADD32( out32_2, X ); + + /* Apply gain in Q15, convert back to int16 and store to output */ + out[ 2 * k ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( out32_1, 10 ) ); + + /* First all-pass section for odd output sample */ + Y = silk_SUB32( in32, S[ 3 ] ); + X = silk_SMULWB( Y, silk_resampler_up2_hq_1[ 0 ] ); + out32_1 = silk_ADD32( S[ 3 ], X ); + S[ 3 ] = silk_ADD32( in32, X ); + + /* Second all-pass section for odd output sample */ + Y = silk_SUB32( out32_1, S[ 4 ] ); + X = silk_SMULWB( Y, silk_resampler_up2_hq_1[ 1 ] ); + out32_2 = silk_ADD32( S[ 4 ], X ); + S[ 4 ] = silk_ADD32( out32_1, X ); + + /* Third all-pass section for odd output sample */ + Y = silk_SUB32( out32_2, S[ 5 ] ); + X = silk_SMLAWB( Y, Y, silk_resampler_up2_hq_1[ 2 ] ); + out32_1 = silk_ADD32( S[ 5 ], X ); + S[ 5 ] = silk_ADD32( out32_2, X ); + + /* Apply gain in Q15, convert back to int16 and store to output */ + out[ 2 * k + 1 ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( out32_1, 10 ) ); + } +} + +void silk_resampler_private_up2_HQ_wrapper( + void *SS, /* I/O Resampler state (unused) */ + opus_int16 *out, /* O Output signal [ 2 * len ] */ + const opus_int16 *in, /* I Input signal [ len ] */ + opus_int32 len /* I Number of input samples */ +) +{ + silk_resampler_state_struct *S = (silk_resampler_state_struct *)SS; + silk_resampler_private_up2_HQ( S->sIIR, out, in, len ); +} diff --git a/port/third_party/opus/silk/resampler_rom.c b/port/third_party/opus/silk/resampler_rom.c new file mode 100644 index 00000000..5e6b0447 --- /dev/null +++ b/port/third_party/opus/silk/resampler_rom.c @@ -0,0 +1,96 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/* Filter coefficients for IIR/FIR polyphase resampling * + * Total size: 179 Words (358 Bytes) */ + +#include "resampler_private.h" + +/* Matlab code for the notch filter coefficients: */ +/* B = [1, 0.147, 1]; A = [1, 0.107, 0.89]; G = 0.93; freqz(G * B, A, 2^14, 16e3); axis([0, 8000, -10, 1]) */ +/* fprintf('\t%6d, %6d, %6d, %6d\n', round(B(2)*2^16), round(-A(2)*2^16), round((1-A(3))*2^16), round(G*2^15)) */ +/* const opus_int16 silk_resampler_up2_hq_notch[ 4 ] = { 9634, -7012, 7209, 30474 }; */ + +/* Tables with IIR and FIR coefficients for fractional downsamplers (123 Words) */ +silk_DWORD_ALIGN const opus_int16 silk_Resampler_3_4_COEFS[ 2 + 3 * RESAMPLER_DOWN_ORDER_FIR0 / 2 ] = { + -20694, -13867, + -49, 64, 17, -157, 353, -496, 163, 11047, 22205, + -39, 6, 91, -170, 186, 23, -896, 6336, 19928, + -19, -36, 102, -89, -24, 328, -951, 2568, 15909, +}; + +silk_DWORD_ALIGN const opus_int16 silk_Resampler_2_3_COEFS[ 2 + 2 * RESAMPLER_DOWN_ORDER_FIR0 / 2 ] = { + -14457, -14019, + 64, 128, -122, 36, 310, -768, 584, 9267, 17733, + 12, 128, 18, -142, 288, -117, -865, 4123, 14459, +}; + +silk_DWORD_ALIGN const opus_int16 silk_Resampler_1_2_COEFS[ 2 + RESAMPLER_DOWN_ORDER_FIR1 / 2 ] = { + 616, -14323, + -10, 39, 58, -46, -84, 120, 184, -315, -541, 1284, 5380, 9024, +}; + +silk_DWORD_ALIGN const opus_int16 silk_Resampler_1_3_COEFS[ 2 + RESAMPLER_DOWN_ORDER_FIR2 / 2 ] = { + 16102, -15162, + -13, 0, 20, 26, 5, -31, -43, -4, 65, 90, 7, -157, -248, -44, 593, 1583, 2612, 3271, +}; + +silk_DWORD_ALIGN const opus_int16 silk_Resampler_1_4_COEFS[ 2 + RESAMPLER_DOWN_ORDER_FIR2 / 2 ] = { + 22500, -15099, + 3, -14, -20, -15, 2, 25, 37, 25, -16, -71, -107, -79, 50, 292, 623, 982, 1288, 1464, +}; + +silk_DWORD_ALIGN const opus_int16 silk_Resampler_1_6_COEFS[ 2 + RESAMPLER_DOWN_ORDER_FIR2 / 2 ] = { + 27540, -15257, + 17, 12, 8, 1, -10, -22, -30, -32, -22, 3, 44, 100, 168, 243, 317, 381, 429, 455, +}; + +silk_DWORD_ALIGN const opus_int16 silk_Resampler_2_3_COEFS_LQ[ 2 + 2 * 2 ] = { + -2797, -6507, + 4697, 10739, + 1567, 8276, +}; + +/* Table with interplation fractions of 1/24, 3/24, 5/24, ... , 23/24 : 23/24 (46 Words) */ +silk_DWORD_ALIGN const opus_int16 silk_resampler_frac_FIR_12[ 12 ][ RESAMPLER_ORDER_FIR_12 / 2 ] = { + { 189, -600, 617, 30567 }, + { 117, -159, -1070, 29704 }, + { 52, 221, -2392, 28276 }, + { -4, 529, -3350, 26341 }, + { -48, 758, -3956, 23973 }, + { -80, 905, -4235, 21254 }, + { -99, 972, -4222, 18278 }, + { -107, 967, -3957, 15143 }, + { -103, 896, -3487, 11950 }, + { -91, 773, -2865, 8798 }, + { -71, 611, -2143, 5784 }, + { -46, 425, -1375, 2996 }, +}; diff --git a/port/third_party/opus/silk/resampler_rom.h b/port/third_party/opus/silk/resampler_rom.h new file mode 100644 index 00000000..490b3388 --- /dev/null +++ b/port/third_party/opus/silk/resampler_rom.h @@ -0,0 +1,68 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_FIX_RESAMPLER_ROM_H +#define SILK_FIX_RESAMPLER_ROM_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "typedef.h" +#include "resampler_structs.h" + +#define RESAMPLER_DOWN_ORDER_FIR0 18 +#define RESAMPLER_DOWN_ORDER_FIR1 24 +#define RESAMPLER_DOWN_ORDER_FIR2 36 +#define RESAMPLER_ORDER_FIR_12 8 + +/* Tables for 2x downsampler */ +static const opus_int16 silk_resampler_down2_0 = 9872; +static const opus_int16 silk_resampler_down2_1 = 39809 - 65536; + +/* Tables for 2x upsampler, high quality */ +static const opus_int16 silk_resampler_up2_hq_0[ 3 ] = { 1746, 14986, 39083 - 65536 }; +static const opus_int16 silk_resampler_up2_hq_1[ 3 ] = { 6854, 25769, 55542 - 65536 }; + +/* Tables with IIR and FIR coefficients for fractional downsamplers */ +extern const opus_int16 silk_Resampler_3_4_COEFS[ 2 + 3 * RESAMPLER_DOWN_ORDER_FIR0 / 2 ]; +extern const opus_int16 silk_Resampler_2_3_COEFS[ 2 + 2 * RESAMPLER_DOWN_ORDER_FIR0 / 2 ]; +extern const opus_int16 silk_Resampler_1_2_COEFS[ 2 + RESAMPLER_DOWN_ORDER_FIR1 / 2 ]; +extern const opus_int16 silk_Resampler_1_3_COEFS[ 2 + RESAMPLER_DOWN_ORDER_FIR2 / 2 ]; +extern const opus_int16 silk_Resampler_1_4_COEFS[ 2 + RESAMPLER_DOWN_ORDER_FIR2 / 2 ]; +extern const opus_int16 silk_Resampler_1_6_COEFS[ 2 + RESAMPLER_DOWN_ORDER_FIR2 / 2 ]; +extern const opus_int16 silk_Resampler_2_3_COEFS_LQ[ 2 + 2 * 2 ]; + +/* Table with interplation fractions of 1/24, 3/24, ..., 23/24 */ +extern const opus_int16 silk_resampler_frac_FIR_12[ 12 ][ RESAMPLER_ORDER_FIR_12 / 2 ]; + +#ifdef __cplusplus +} +#endif + +#endif /* SILK_FIX_RESAMPLER_ROM_H */ diff --git a/port/third_party/opus/silk/resampler_structs.h b/port/third_party/opus/silk/resampler_structs.h new file mode 100644 index 00000000..9e9457d1 --- /dev/null +++ b/port/third_party/opus/silk/resampler_structs.h @@ -0,0 +1,60 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_RESAMPLER_STRUCTS_H +#define SILK_RESAMPLER_STRUCTS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define SILK_RESAMPLER_MAX_FIR_ORDER 36 +#define SILK_RESAMPLER_MAX_IIR_ORDER 6 + +typedef struct _silk_resampler_state_struct{ + opus_int32 sIIR[ SILK_RESAMPLER_MAX_IIR_ORDER ]; /* this must be the first element of this struct */ + union{ + opus_int32 i32[ SILK_RESAMPLER_MAX_FIR_ORDER ]; + opus_int16 i16[ SILK_RESAMPLER_MAX_FIR_ORDER ]; + } sFIR; + opus_int16 delayBuf[ 48 ]; + opus_int resampler_function; + opus_int batchSize; + opus_int32 invRatio_Q16; + opus_int FIR_Order; + opus_int FIR_Fracs; + opus_int Fs_in_kHz; + opus_int Fs_out_kHz; + opus_int inputDelay; + const opus_int16 *Coefs; +} silk_resampler_state_struct; + +#ifdef __cplusplus +} +#endif +#endif /* SILK_RESAMPLER_STRUCTS_H */ + diff --git a/port/third_party/opus/silk/shell_coder.c b/port/third_party/opus/silk/shell_coder.c new file mode 100644 index 00000000..4af34147 --- /dev/null +++ b/port/third_party/opus/silk/shell_coder.c @@ -0,0 +1,151 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* shell coder; pulse-subframe length is hardcoded */ + +static OPUS_INLINE void combine_pulses( + opus_int *out, /* O combined pulses vector [len] */ + const opus_int *in, /* I input vector [2 * len] */ + const opus_int len /* I number of OUTPUT samples */ +) +{ + opus_int k; + for( k = 0; k < len; k++ ) { + out[ k ] = in[ 2 * k ] + in[ 2 * k + 1 ]; + } +} + +static OPUS_INLINE void encode_split( + ec_enc *psRangeEnc, /* I/O compressor data structure */ + const opus_int p_child1, /* I pulse amplitude of first child subframe */ + const opus_int p, /* I pulse amplitude of current subframe */ + const opus_uint8 *shell_table /* I table of shell cdfs */ +) +{ + if( p > 0 ) { + ec_enc_icdf( psRangeEnc, p_child1, &shell_table[ silk_shell_code_table_offsets[ p ] ], 8 ); + } +} + +static OPUS_INLINE void decode_split( + opus_int16 *p_child1, /* O pulse amplitude of first child subframe */ + opus_int16 *p_child2, /* O pulse amplitude of second child subframe */ + ec_dec *psRangeDec, /* I/O Compressor data structure */ + const opus_int p, /* I pulse amplitude of current subframe */ + const opus_uint8 *shell_table /* I table of shell cdfs */ +) +{ + if( p > 0 ) { + p_child1[ 0 ] = ec_dec_icdf( psRangeDec, &shell_table[ silk_shell_code_table_offsets[ p ] ], 8 ); + p_child2[ 0 ] = p - p_child1[ 0 ]; + } else { + p_child1[ 0 ] = 0; + p_child2[ 0 ] = 0; + } +} + +/* Shell encoder, operates on one shell code frame of 16 pulses */ +void silk_shell_encoder( + ec_enc *psRangeEnc, /* I/O compressor data structure */ + const opus_int *pulses0 /* I data: nonnegative pulse amplitudes */ +) +{ + opus_int pulses1[ 8 ], pulses2[ 4 ], pulses3[ 2 ], pulses4[ 1 ]; + + /* this function operates on one shell code frame of 16 pulses */ + silk_assert( SHELL_CODEC_FRAME_LENGTH == 16 ); + + /* tree representation per pulse-subframe */ + combine_pulses( pulses1, pulses0, 8 ); + combine_pulses( pulses2, pulses1, 4 ); + combine_pulses( pulses3, pulses2, 2 ); + combine_pulses( pulses4, pulses3, 1 ); + + encode_split( psRangeEnc, pulses3[ 0 ], pulses4[ 0 ], silk_shell_code_table3 ); + + encode_split( psRangeEnc, pulses2[ 0 ], pulses3[ 0 ], silk_shell_code_table2 ); + + encode_split( psRangeEnc, pulses1[ 0 ], pulses2[ 0 ], silk_shell_code_table1 ); + encode_split( psRangeEnc, pulses0[ 0 ], pulses1[ 0 ], silk_shell_code_table0 ); + encode_split( psRangeEnc, pulses0[ 2 ], pulses1[ 1 ], silk_shell_code_table0 ); + + encode_split( psRangeEnc, pulses1[ 2 ], pulses2[ 1 ], silk_shell_code_table1 ); + encode_split( psRangeEnc, pulses0[ 4 ], pulses1[ 2 ], silk_shell_code_table0 ); + encode_split( psRangeEnc, pulses0[ 6 ], pulses1[ 3 ], silk_shell_code_table0 ); + + encode_split( psRangeEnc, pulses2[ 2 ], pulses3[ 1 ], silk_shell_code_table2 ); + + encode_split( psRangeEnc, pulses1[ 4 ], pulses2[ 2 ], silk_shell_code_table1 ); + encode_split( psRangeEnc, pulses0[ 8 ], pulses1[ 4 ], silk_shell_code_table0 ); + encode_split( psRangeEnc, pulses0[ 10 ], pulses1[ 5 ], silk_shell_code_table0 ); + + encode_split( psRangeEnc, pulses1[ 6 ], pulses2[ 3 ], silk_shell_code_table1 ); + encode_split( psRangeEnc, pulses0[ 12 ], pulses1[ 6 ], silk_shell_code_table0 ); + encode_split( psRangeEnc, pulses0[ 14 ], pulses1[ 7 ], silk_shell_code_table0 ); +} + + +/* Shell decoder, operates on one shell code frame of 16 pulses */ +void silk_shell_decoder( + opus_int16 *pulses0, /* O data: nonnegative pulse amplitudes */ + ec_dec *psRangeDec, /* I/O Compressor data structure */ + const opus_int pulses4 /* I number of pulses per pulse-subframe */ +) +{ + opus_int16 pulses3[ 2 ], pulses2[ 4 ], pulses1[ 8 ]; + + /* this function operates on one shell code frame of 16 pulses */ + silk_assert( SHELL_CODEC_FRAME_LENGTH == 16 ); + + decode_split( &pulses3[ 0 ], &pulses3[ 1 ], psRangeDec, pulses4, silk_shell_code_table3 ); + + decode_split( &pulses2[ 0 ], &pulses2[ 1 ], psRangeDec, pulses3[ 0 ], silk_shell_code_table2 ); + + decode_split( &pulses1[ 0 ], &pulses1[ 1 ], psRangeDec, pulses2[ 0 ], silk_shell_code_table1 ); + decode_split( &pulses0[ 0 ], &pulses0[ 1 ], psRangeDec, pulses1[ 0 ], silk_shell_code_table0 ); + decode_split( &pulses0[ 2 ], &pulses0[ 3 ], psRangeDec, pulses1[ 1 ], silk_shell_code_table0 ); + + decode_split( &pulses1[ 2 ], &pulses1[ 3 ], psRangeDec, pulses2[ 1 ], silk_shell_code_table1 ); + decode_split( &pulses0[ 4 ], &pulses0[ 5 ], psRangeDec, pulses1[ 2 ], silk_shell_code_table0 ); + decode_split( &pulses0[ 6 ], &pulses0[ 7 ], psRangeDec, pulses1[ 3 ], silk_shell_code_table0 ); + + decode_split( &pulses2[ 2 ], &pulses2[ 3 ], psRangeDec, pulses3[ 1 ], silk_shell_code_table2 ); + + decode_split( &pulses1[ 4 ], &pulses1[ 5 ], psRangeDec, pulses2[ 2 ], silk_shell_code_table1 ); + decode_split( &pulses0[ 8 ], &pulses0[ 9 ], psRangeDec, pulses1[ 4 ], silk_shell_code_table0 ); + decode_split( &pulses0[ 10 ], &pulses0[ 11 ], psRangeDec, pulses1[ 5 ], silk_shell_code_table0 ); + + decode_split( &pulses1[ 6 ], &pulses1[ 7 ], psRangeDec, pulses2[ 3 ], silk_shell_code_table1 ); + decode_split( &pulses0[ 12 ], &pulses0[ 13 ], psRangeDec, pulses1[ 6 ], silk_shell_code_table0 ); + decode_split( &pulses0[ 14 ], &pulses0[ 15 ], psRangeDec, pulses1[ 7 ], silk_shell_code_table0 ); +} diff --git a/port/third_party/opus/silk/sigm_Q15.c b/port/third_party/opus/silk/sigm_Q15.c new file mode 100644 index 00000000..3c507d25 --- /dev/null +++ b/port/third_party/opus/silk/sigm_Q15.c @@ -0,0 +1,76 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/* Approximate sigmoid function */ + +#include "SigProc_FIX.h" + +/* fprintf(1, '%d, ', round(1024 * ([1 ./ (1 + exp(-(1:5))), 1] - 1 ./ (1 + exp(-(0:5)))))); */ +static const opus_int32 sigm_LUT_slope_Q10[ 6 ] = { + 237, 153, 73, 30, 12, 7 +}; +/* fprintf(1, '%d, ', round(32767 * 1 ./ (1 + exp(-(0:5))))); */ +static const opus_int32 sigm_LUT_pos_Q15[ 6 ] = { + 16384, 23955, 28861, 31213, 32178, 32548 +}; +/* fprintf(1, '%d, ', round(32767 * 1 ./ (1 + exp((0:5))))); */ +static const opus_int32 sigm_LUT_neg_Q15[ 6 ] = { + 16384, 8812, 3906, 1554, 589, 219 +}; + +opus_int silk_sigm_Q15( + opus_int in_Q5 /* I */ +) +{ + opus_int ind; + + if( in_Q5 < 0 ) { + /* Negative input */ + in_Q5 = -in_Q5; + if( in_Q5 >= 6 * 32 ) { + return 0; /* Clip */ + } else { + /* Linear interpolation of look up table */ + ind = silk_RSHIFT( in_Q5, 5 ); + return( sigm_LUT_neg_Q15[ ind ] - silk_SMULBB( sigm_LUT_slope_Q10[ ind ], in_Q5 & 0x1F ) ); + } + } else { + /* Positive input */ + if( in_Q5 >= 6 * 32 ) { + return 32767; /* clip */ + } else { + /* Linear interpolation of look up table */ + ind = silk_RSHIFT( in_Q5, 5 ); + return( sigm_LUT_pos_Q15[ ind ] + silk_SMULBB( sigm_LUT_slope_Q10[ ind ], in_Q5 & 0x1F ) ); + } + } +} + diff --git a/port/third_party/opus/silk/sort.c b/port/third_party/opus/silk/sort.c new file mode 100644 index 00000000..4fba16f8 --- /dev/null +++ b/port/third_party/opus/silk/sort.c @@ -0,0 +1,154 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/* Insertion sort (fast for already almost sorted arrays): */ +/* Best case: O(n) for an already sorted array */ +/* Worst case: O(n^2) for an inversely sorted array */ +/* */ +/* Shell short: https://en.wikipedia.org/wiki/Shell_sort */ + +#include "SigProc_FIX.h" + +void silk_insertion_sort_increasing( + opus_int32 *a, /* I/O Unsorted / Sorted vector */ + opus_int *idx, /* O Index vector for the sorted elements */ + const opus_int L, /* I Vector length */ + const opus_int K /* I Number of correctly sorted positions */ +) +{ + opus_int32 value; + opus_int i, j; + + /* Safety checks */ + celt_assert( K > 0 ); + celt_assert( L > 0 ); + celt_assert( L >= K ); + + /* Write start indices in index vector */ + for( i = 0; i < K; i++ ) { + idx[ i ] = i; + } + + /* Sort vector elements by value, increasing order */ + for( i = 1; i < K; i++ ) { + value = a[ i ]; + for( j = i - 1; ( j >= 0 ) && ( value < a[ j ] ); j-- ) { + a[ j + 1 ] = a[ j ]; /* Shift value */ + idx[ j + 1 ] = idx[ j ]; /* Shift index */ + } + a[ j + 1 ] = value; /* Write value */ + idx[ j + 1 ] = i; /* Write index */ + } + + /* If less than L values are asked for, check the remaining values, */ + /* but only spend CPU to ensure that the K first values are correct */ + for( i = K; i < L; i++ ) { + value = a[ i ]; + if( value < a[ K - 1 ] ) { + for( j = K - 2; ( j >= 0 ) && ( value < a[ j ] ); j-- ) { + a[ j + 1 ] = a[ j ]; /* Shift value */ + idx[ j + 1 ] = idx[ j ]; /* Shift index */ + } + a[ j + 1 ] = value; /* Write value */ + idx[ j + 1 ] = i; /* Write index */ + } + } +} + +#ifdef FIXED_POINT +/* This function is only used by the fixed-point build */ +void silk_insertion_sort_decreasing_int16( + opus_int16 *a, /* I/O Unsorted / Sorted vector */ + opus_int *idx, /* O Index vector for the sorted elements */ + const opus_int L, /* I Vector length */ + const opus_int K /* I Number of correctly sorted positions */ +) +{ + opus_int i, j; + opus_int value; + + /* Safety checks */ + celt_assert( K > 0 ); + celt_assert( L > 0 ); + celt_assert( L >= K ); + + /* Write start indices in index vector */ + for( i = 0; i < K; i++ ) { + idx[ i ] = i; + } + + /* Sort vector elements by value, decreasing order */ + for( i = 1; i < K; i++ ) { + value = a[ i ]; + for( j = i - 1; ( j >= 0 ) && ( value > a[ j ] ); j-- ) { + a[ j + 1 ] = a[ j ]; /* Shift value */ + idx[ j + 1 ] = idx[ j ]; /* Shift index */ + } + a[ j + 1 ] = value; /* Write value */ + idx[ j + 1 ] = i; /* Write index */ + } + + /* If less than L values are asked for, check the remaining values, */ + /* but only spend CPU to ensure that the K first values are correct */ + for( i = K; i < L; i++ ) { + value = a[ i ]; + if( value > a[ K - 1 ] ) { + for( j = K - 2; ( j >= 0 ) && ( value > a[ j ] ); j-- ) { + a[ j + 1 ] = a[ j ]; /* Shift value */ + idx[ j + 1 ] = idx[ j ]; /* Shift index */ + } + a[ j + 1 ] = value; /* Write value */ + idx[ j + 1 ] = i; /* Write index */ + } + } +} +#endif + +void silk_insertion_sort_increasing_all_values_int16( + opus_int16 *a, /* I/O Unsorted / Sorted vector */ + const opus_int L /* I Vector length */ +) +{ + opus_int value; + opus_int i, j; + + /* Safety checks */ + celt_assert( L > 0 ); + + /* Sort vector elements by value, increasing order */ + for( i = 1; i < L; i++ ) { + value = a[ i ]; + for( j = i - 1; ( j >= 0 ) && ( value < a[ j ] ); j-- ) { + a[ j + 1 ] = a[ j ]; /* Shift value */ + } + a[ j + 1 ] = value; /* Write value */ + } +} diff --git a/port/third_party/opus/silk/stereo_LR_to_MS.c b/port/third_party/opus/silk/stereo_LR_to_MS.c new file mode 100644 index 00000000..751452cb --- /dev/null +++ b/port/third_party/opus/silk/stereo_LR_to_MS.c @@ -0,0 +1,229 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" +#include "stack_alloc.h" + +/* Convert Left/Right stereo signal to adaptive Mid/Side representation */ +void silk_stereo_LR_to_MS( + stereo_enc_state *state, /* I/O State */ + opus_int16 x1[], /* I/O Left input signal, becomes mid signal */ + opus_int16 x2[], /* I/O Right input signal, becomes side signal */ + opus_int8 ix[ 2 ][ 3 ], /* O Quantization indices */ + opus_int8 *mid_only_flag, /* O Flag: only mid signal coded */ + opus_int32 mid_side_rates_bps[], /* O Bitrates for mid and side signals */ + opus_int32 total_rate_bps, /* I Total bitrate */ + opus_int prev_speech_act_Q8, /* I Speech activity level in previous frame */ + opus_int toMono, /* I Last frame before a stereo->mono transition */ + opus_int fs_kHz, /* I Sample rate (kHz) */ + opus_int frame_length /* I Number of samples */ +) +{ + opus_int n, is10msFrame, denom_Q16, delta0_Q13, delta1_Q13; + opus_int32 sum, diff, smooth_coef_Q16, pred_Q13[ 2 ], pred0_Q13, pred1_Q13; + opus_int32 LP_ratio_Q14, HP_ratio_Q14, frac_Q16, frac_3_Q16, min_mid_rate_bps, width_Q14, w_Q24, deltaw_Q24; + VARDECL( opus_int16, side ); + VARDECL( opus_int16, LP_mid ); + VARDECL( opus_int16, HP_mid ); + VARDECL( opus_int16, LP_side ); + VARDECL( opus_int16, HP_side ); + opus_int16 *mid = &x1[ -2 ]; + SAVE_STACK; + + ALLOC( side, frame_length + 2, opus_int16 ); + /* Convert to basic mid/side signals */ + for( n = 0; n < frame_length + 2; n++ ) { + sum = x1[ n - 2 ] + (opus_int32)x2[ n - 2 ]; + diff = x1[ n - 2 ] - (opus_int32)x2[ n - 2 ]; + mid[ n ] = (opus_int16)silk_RSHIFT_ROUND( sum, 1 ); + side[ n ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( diff, 1 ) ); + } + + /* Buffering */ + silk_memcpy( mid, state->sMid, 2 * sizeof( opus_int16 ) ); + silk_memcpy( side, state->sSide, 2 * sizeof( opus_int16 ) ); + silk_memcpy( state->sMid, &mid[ frame_length ], 2 * sizeof( opus_int16 ) ); + silk_memcpy( state->sSide, &side[ frame_length ], 2 * sizeof( opus_int16 ) ); + + /* LP and HP filter mid signal */ + ALLOC( LP_mid, frame_length, opus_int16 ); + ALLOC( HP_mid, frame_length, opus_int16 ); + for( n = 0; n < frame_length; n++ ) { + sum = silk_RSHIFT_ROUND( silk_ADD_LSHIFT32( mid[ n ] + (opus_int32)mid[ n + 2 ], mid[ n + 1 ], 1 ), 2 ); + LP_mid[ n ] = sum; + HP_mid[ n ] = mid[ n + 1 ] - sum; + } + + /* LP and HP filter side signal */ + ALLOC( LP_side, frame_length, opus_int16 ); + ALLOC( HP_side, frame_length, opus_int16 ); + for( n = 0; n < frame_length; n++ ) { + sum = silk_RSHIFT_ROUND( silk_ADD_LSHIFT32( side[ n ] + (opus_int32)side[ n + 2 ], side[ n + 1 ], 1 ), 2 ); + LP_side[ n ] = sum; + HP_side[ n ] = side[ n + 1 ] - sum; + } + + /* Find energies and predictors */ + is10msFrame = frame_length == 10 * fs_kHz; + smooth_coef_Q16 = is10msFrame ? + SILK_FIX_CONST( STEREO_RATIO_SMOOTH_COEF / 2, 16 ) : + SILK_FIX_CONST( STEREO_RATIO_SMOOTH_COEF, 16 ); + smooth_coef_Q16 = silk_SMULWB( silk_SMULBB( prev_speech_act_Q8, prev_speech_act_Q8 ), smooth_coef_Q16 ); + + pred_Q13[ 0 ] = silk_stereo_find_predictor( &LP_ratio_Q14, LP_mid, LP_side, &state->mid_side_amp_Q0[ 0 ], frame_length, smooth_coef_Q16 ); + pred_Q13[ 1 ] = silk_stereo_find_predictor( &HP_ratio_Q14, HP_mid, HP_side, &state->mid_side_amp_Q0[ 2 ], frame_length, smooth_coef_Q16 ); + /* Ratio of the norms of residual and mid signals */ + frac_Q16 = silk_SMLABB( HP_ratio_Q14, LP_ratio_Q14, 3 ); + frac_Q16 = silk_min( frac_Q16, SILK_FIX_CONST( 1, 16 ) ); + + /* Determine bitrate distribution between mid and side, and possibly reduce stereo width */ + total_rate_bps -= is10msFrame ? 1200 : 600; /* Subtract approximate bitrate for coding stereo parameters */ + if( total_rate_bps < 1 ) { + total_rate_bps = 1; + } + min_mid_rate_bps = silk_SMLABB( 2000, fs_kHz, 600 ); + silk_assert( min_mid_rate_bps < 32767 ); + /* Default bitrate distribution: 8 parts for Mid and (5+3*frac) parts for Side. so: mid_rate = ( 8 / ( 13 + 3 * frac ) ) * total_ rate */ + frac_3_Q16 = silk_MUL( 3, frac_Q16 ); + mid_side_rates_bps[ 0 ] = silk_DIV32_varQ( total_rate_bps, SILK_FIX_CONST( 8 + 5, 16 ) + frac_3_Q16, 16+3 ); + /* If Mid bitrate below minimum, reduce stereo width */ + if( mid_side_rates_bps[ 0 ] < min_mid_rate_bps ) { + mid_side_rates_bps[ 0 ] = min_mid_rate_bps; + mid_side_rates_bps[ 1 ] = total_rate_bps - mid_side_rates_bps[ 0 ]; + /* width = 4 * ( 2 * side_rate - min_rate ) / ( ( 1 + 3 * frac ) * min_rate ) */ + width_Q14 = silk_DIV32_varQ( silk_LSHIFT( mid_side_rates_bps[ 1 ], 1 ) - min_mid_rate_bps, + silk_SMULWB( SILK_FIX_CONST( 1, 16 ) + frac_3_Q16, min_mid_rate_bps ), 14+2 ); + width_Q14 = silk_LIMIT( width_Q14, 0, SILK_FIX_CONST( 1, 14 ) ); + } else { + mid_side_rates_bps[ 1 ] = total_rate_bps - mid_side_rates_bps[ 0 ]; + width_Q14 = SILK_FIX_CONST( 1, 14 ); + } + + /* Smoother */ + state->smth_width_Q14 = (opus_int16)silk_SMLAWB( state->smth_width_Q14, width_Q14 - state->smth_width_Q14, smooth_coef_Q16 ); + + /* At very low bitrates or for inputs that are nearly amplitude panned, switch to panned-mono coding */ + *mid_only_flag = 0; + if( toMono ) { + /* Last frame before stereo->mono transition; collapse stereo width */ + width_Q14 = 0; + pred_Q13[ 0 ] = 0; + pred_Q13[ 1 ] = 0; + silk_stereo_quant_pred( pred_Q13, ix ); + } else if( state->width_prev_Q14 == 0 && + ( 8 * total_rate_bps < 13 * min_mid_rate_bps || silk_SMULWB( frac_Q16, state->smth_width_Q14 ) < SILK_FIX_CONST( 0.05, 14 ) ) ) + { + /* Code as panned-mono; previous frame already had zero width */ + /* Scale down and quantize predictors */ + pred_Q13[ 0 ] = silk_RSHIFT( silk_SMULBB( state->smth_width_Q14, pred_Q13[ 0 ] ), 14 ); + pred_Q13[ 1 ] = silk_RSHIFT( silk_SMULBB( state->smth_width_Q14, pred_Q13[ 1 ] ), 14 ); + silk_stereo_quant_pred( pred_Q13, ix ); + /* Collapse stereo width */ + width_Q14 = 0; + pred_Q13[ 0 ] = 0; + pred_Q13[ 1 ] = 0; + mid_side_rates_bps[ 0 ] = total_rate_bps; + mid_side_rates_bps[ 1 ] = 0; + *mid_only_flag = 1; + } else if( state->width_prev_Q14 != 0 && + ( 8 * total_rate_bps < 11 * min_mid_rate_bps || silk_SMULWB( frac_Q16, state->smth_width_Q14 ) < SILK_FIX_CONST( 0.02, 14 ) ) ) + { + /* Transition to zero-width stereo */ + /* Scale down and quantize predictors */ + pred_Q13[ 0 ] = silk_RSHIFT( silk_SMULBB( state->smth_width_Q14, pred_Q13[ 0 ] ), 14 ); + pred_Q13[ 1 ] = silk_RSHIFT( silk_SMULBB( state->smth_width_Q14, pred_Q13[ 1 ] ), 14 ); + silk_stereo_quant_pred( pred_Q13, ix ); + /* Collapse stereo width */ + width_Q14 = 0; + pred_Q13[ 0 ] = 0; + pred_Q13[ 1 ] = 0; + } else if( state->smth_width_Q14 > SILK_FIX_CONST( 0.95, 14 ) ) { + /* Full-width stereo coding */ + silk_stereo_quant_pred( pred_Q13, ix ); + width_Q14 = SILK_FIX_CONST( 1, 14 ); + } else { + /* Reduced-width stereo coding; scale down and quantize predictors */ + pred_Q13[ 0 ] = silk_RSHIFT( silk_SMULBB( state->smth_width_Q14, pred_Q13[ 0 ] ), 14 ); + pred_Q13[ 1 ] = silk_RSHIFT( silk_SMULBB( state->smth_width_Q14, pred_Q13[ 1 ] ), 14 ); + silk_stereo_quant_pred( pred_Q13, ix ); + width_Q14 = state->smth_width_Q14; + } + + /* Make sure to keep on encoding until the tapered output has been transmitted */ + if( *mid_only_flag == 1 ) { + state->silent_side_len += frame_length - STEREO_INTERP_LEN_MS * fs_kHz; + if( state->silent_side_len < LA_SHAPE_MS * fs_kHz ) { + *mid_only_flag = 0; + } else { + /* Limit to avoid wrapping around */ + state->silent_side_len = 10000; + } + } else { + state->silent_side_len = 0; + } + + if( *mid_only_flag == 0 && mid_side_rates_bps[ 1 ] < 1 ) { + mid_side_rates_bps[ 1 ] = 1; + mid_side_rates_bps[ 0 ] = silk_max_int( 1, total_rate_bps - mid_side_rates_bps[ 1 ]); + } + + /* Interpolate predictors and subtract prediction from side channel */ + pred0_Q13 = -state->pred_prev_Q13[ 0 ]; + pred1_Q13 = -state->pred_prev_Q13[ 1 ]; + w_Q24 = silk_LSHIFT( state->width_prev_Q14, 10 ); + denom_Q16 = silk_DIV32_16( (opus_int32)1 << 16, STEREO_INTERP_LEN_MS * fs_kHz ); + delta0_Q13 = -silk_RSHIFT_ROUND( silk_SMULBB( pred_Q13[ 0 ] - state->pred_prev_Q13[ 0 ], denom_Q16 ), 16 ); + delta1_Q13 = -silk_RSHIFT_ROUND( silk_SMULBB( pred_Q13[ 1 ] - state->pred_prev_Q13[ 1 ], denom_Q16 ), 16 ); + deltaw_Q24 = silk_LSHIFT( silk_SMULWB( width_Q14 - state->width_prev_Q14, denom_Q16 ), 10 ); + for( n = 0; n < STEREO_INTERP_LEN_MS * fs_kHz; n++ ) { + pred0_Q13 += delta0_Q13; + pred1_Q13 += delta1_Q13; + w_Q24 += deltaw_Q24; + sum = silk_LSHIFT( silk_ADD_LSHIFT32( mid[ n ] + (opus_int32)mid[ n + 2 ], mid[ n + 1 ], 1 ), 9 ); /* Q11 */ + sum = silk_SMLAWB( silk_SMULWB( w_Q24, side[ n + 1 ] ), sum, pred0_Q13 ); /* Q8 */ + sum = silk_SMLAWB( sum, silk_LSHIFT( (opus_int32)mid[ n + 1 ], 11 ), pred1_Q13 ); /* Q8 */ + x2[ n - 1 ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( sum, 8 ) ); + } + + pred0_Q13 = -pred_Q13[ 0 ]; + pred1_Q13 = -pred_Q13[ 1 ]; + w_Q24 = silk_LSHIFT( width_Q14, 10 ); + for( n = STEREO_INTERP_LEN_MS * fs_kHz; n < frame_length; n++ ) { + sum = silk_LSHIFT( silk_ADD_LSHIFT32( mid[ n ] + (opus_int32)mid[ n + 2 ], mid[ n + 1 ], 1 ), 9 ); /* Q11 */ + sum = silk_SMLAWB( silk_SMULWB( w_Q24, side[ n + 1 ] ), sum, pred0_Q13 ); /* Q8 */ + sum = silk_SMLAWB( sum, silk_LSHIFT( (opus_int32)mid[ n + 1 ], 11 ), pred1_Q13 ); /* Q8 */ + x2[ n - 1 ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( sum, 8 ) ); + } + state->pred_prev_Q13[ 0 ] = (opus_int16)pred_Q13[ 0 ]; + state->pred_prev_Q13[ 1 ] = (opus_int16)pred_Q13[ 1 ]; + state->width_prev_Q14 = (opus_int16)width_Q14; + RESTORE_STACK; +} diff --git a/port/third_party/opus/silk/stereo_MS_to_LR.c b/port/third_party/opus/silk/stereo_MS_to_LR.c new file mode 100644 index 00000000..1e01bb6e --- /dev/null +++ b/port/third_party/opus/silk/stereo_MS_to_LR.c @@ -0,0 +1,85 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Convert adaptive Mid/Side representation to Left/Right stereo signal */ +void silk_stereo_MS_to_LR( + stereo_dec_state *state, /* I/O State */ + opus_int16 x1[], /* I/O Left input signal, becomes mid signal */ + opus_int16 x2[], /* I/O Right input signal, becomes side signal */ + const opus_int32 pred_Q13[], /* I Predictors */ + opus_int fs_kHz, /* I Samples rate (kHz) */ + opus_int frame_length /* I Number of samples */ +) +{ + opus_int n, denom_Q16, delta0_Q13, delta1_Q13; + opus_int32 sum, diff, pred0_Q13, pred1_Q13; + + /* Buffering */ + silk_memcpy( x1, state->sMid, 2 * sizeof( opus_int16 ) ); + silk_memcpy( x2, state->sSide, 2 * sizeof( opus_int16 ) ); + silk_memcpy( state->sMid, &x1[ frame_length ], 2 * sizeof( opus_int16 ) ); + silk_memcpy( state->sSide, &x2[ frame_length ], 2 * sizeof( opus_int16 ) ); + + /* Interpolate predictors and add prediction to side channel */ + pred0_Q13 = state->pred_prev_Q13[ 0 ]; + pred1_Q13 = state->pred_prev_Q13[ 1 ]; + denom_Q16 = silk_DIV32_16( (opus_int32)1 << 16, STEREO_INTERP_LEN_MS * fs_kHz ); + delta0_Q13 = silk_RSHIFT_ROUND( silk_SMULBB( pred_Q13[ 0 ] - state->pred_prev_Q13[ 0 ], denom_Q16 ), 16 ); + delta1_Q13 = silk_RSHIFT_ROUND( silk_SMULBB( pred_Q13[ 1 ] - state->pred_prev_Q13[ 1 ], denom_Q16 ), 16 ); + for( n = 0; n < STEREO_INTERP_LEN_MS * fs_kHz; n++ ) { + pred0_Q13 += delta0_Q13; + pred1_Q13 += delta1_Q13; + sum = silk_LSHIFT( silk_ADD_LSHIFT32( x1[ n ] + (opus_int32)x1[ n + 2 ], x1[ n + 1 ], 1 ), 9 ); /* Q11 */ + sum = silk_SMLAWB( silk_LSHIFT( (opus_int32)x2[ n + 1 ], 8 ), sum, pred0_Q13 ); /* Q8 */ + sum = silk_SMLAWB( sum, silk_LSHIFT( (opus_int32)x1[ n + 1 ], 11 ), pred1_Q13 ); /* Q8 */ + x2[ n + 1 ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( sum, 8 ) ); + } + pred0_Q13 = pred_Q13[ 0 ]; + pred1_Q13 = pred_Q13[ 1 ]; + for( n = STEREO_INTERP_LEN_MS * fs_kHz; n < frame_length; n++ ) { + sum = silk_LSHIFT( silk_ADD_LSHIFT32( x1[ n ] + (opus_int32)x1[ n + 2 ], x1[ n + 1 ], 1 ), 9 ); /* Q11 */ + sum = silk_SMLAWB( silk_LSHIFT( (opus_int32)x2[ n + 1 ], 8 ), sum, pred0_Q13 ); /* Q8 */ + sum = silk_SMLAWB( sum, silk_LSHIFT( (opus_int32)x1[ n + 1 ], 11 ), pred1_Q13 ); /* Q8 */ + x2[ n + 1 ] = (opus_int16)silk_SAT16( silk_RSHIFT_ROUND( sum, 8 ) ); + } + state->pred_prev_Q13[ 0 ] = pred_Q13[ 0 ]; + state->pred_prev_Q13[ 1 ] = pred_Q13[ 1 ]; + + /* Convert to left/right signals */ + for( n = 0; n < frame_length; n++ ) { + sum = x1[ n + 1 ] + (opus_int32)x2[ n + 1 ]; + diff = x1[ n + 1 ] - (opus_int32)x2[ n + 1 ]; + x1[ n + 1 ] = (opus_int16)silk_SAT16( sum ); + x2[ n + 1 ] = (opus_int16)silk_SAT16( diff ); + } +} diff --git a/port/third_party/opus/silk/stereo_decode_pred.c b/port/third_party/opus/silk/stereo_decode_pred.c new file mode 100644 index 00000000..56ba3925 --- /dev/null +++ b/port/third_party/opus/silk/stereo_decode_pred.c @@ -0,0 +1,73 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Decode mid/side predictors */ +void silk_stereo_decode_pred( + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int32 pred_Q13[] /* O Predictors */ +) +{ + opus_int n, ix[ 2 ][ 3 ]; + opus_int32 low_Q13, step_Q13; + + /* Entropy decoding */ + n = ec_dec_icdf( psRangeDec, silk_stereo_pred_joint_iCDF, 8 ); + ix[ 0 ][ 2 ] = silk_DIV32_16( n, 5 ); + ix[ 1 ][ 2 ] = n - 5 * ix[ 0 ][ 2 ]; + for( n = 0; n < 2; n++ ) { + ix[ n ][ 0 ] = ec_dec_icdf( psRangeDec, silk_uniform3_iCDF, 8 ); + ix[ n ][ 1 ] = ec_dec_icdf( psRangeDec, silk_uniform5_iCDF, 8 ); + } + + /* Dequantize */ + for( n = 0; n < 2; n++ ) { + ix[ n ][ 0 ] += 3 * ix[ n ][ 2 ]; + low_Q13 = silk_stereo_pred_quant_Q13[ ix[ n ][ 0 ] ]; + step_Q13 = silk_SMULWB( silk_stereo_pred_quant_Q13[ ix[ n ][ 0 ] + 1 ] - low_Q13, + SILK_FIX_CONST( 0.5 / STEREO_QUANT_SUB_STEPS, 16 ) ); + pred_Q13[ n ] = silk_SMLABB( low_Q13, step_Q13, 2 * ix[ n ][ 1 ] + 1 ); + } + + /* Subtract second from first predictor (helps when actually applying these) */ + pred_Q13[ 0 ] -= pred_Q13[ 1 ]; +} + +/* Decode mid-only flag */ +void silk_stereo_decode_mid_only( + ec_dec *psRangeDec, /* I/O Compressor data structure */ + opus_int *decode_only_mid /* O Flag that only mid channel has been coded */ +) +{ + /* Decode flag that only mid channel is coded */ + *decode_only_mid = ec_dec_icdf( psRangeDec, silk_stereo_only_code_mid_iCDF, 8 ); +} diff --git a/port/third_party/opus/silk/stereo_encode_pred.c b/port/third_party/opus/silk/stereo_encode_pred.c new file mode 100644 index 00000000..03becb67 --- /dev/null +++ b/port/third_party/opus/silk/stereo_encode_pred.c @@ -0,0 +1,62 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Entropy code the mid/side quantization indices */ +void silk_stereo_encode_pred( + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + opus_int8 ix[ 2 ][ 3 ] /* I Quantization indices */ +) +{ + opus_int n; + + /* Entropy coding */ + n = 5 * ix[ 0 ][ 2 ] + ix[ 1 ][ 2 ]; + celt_assert( n < 25 ); + ec_enc_icdf( psRangeEnc, n, silk_stereo_pred_joint_iCDF, 8 ); + for( n = 0; n < 2; n++ ) { + celt_assert( ix[ n ][ 0 ] < 3 ); + celt_assert( ix[ n ][ 1 ] < STEREO_QUANT_SUB_STEPS ); + ec_enc_icdf( psRangeEnc, ix[ n ][ 0 ], silk_uniform3_iCDF, 8 ); + ec_enc_icdf( psRangeEnc, ix[ n ][ 1 ], silk_uniform5_iCDF, 8 ); + } +} + +/* Entropy code the mid-only flag */ +void silk_stereo_encode_mid_only( + ec_enc *psRangeEnc, /* I/O Compressor data structure */ + opus_int8 mid_only_flag +) +{ + /* Encode flag that only mid channel is coded */ + ec_enc_icdf( psRangeEnc, mid_only_flag, silk_stereo_only_code_mid_iCDF, 8 ); +} diff --git a/port/third_party/opus/silk/stereo_find_predictor.c b/port/third_party/opus/silk/stereo_find_predictor.c new file mode 100644 index 00000000..e30e90bd --- /dev/null +++ b/port/third_party/opus/silk/stereo_find_predictor.c @@ -0,0 +1,79 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Find least-squares prediction gain for one signal based on another and quantize it */ +opus_int32 silk_stereo_find_predictor( /* O Returns predictor in Q13 */ + opus_int32 *ratio_Q14, /* O Ratio of residual and mid energies */ + const opus_int16 x[], /* I Basis signal */ + const opus_int16 y[], /* I Target signal */ + opus_int32 mid_res_amp_Q0[], /* I/O Smoothed mid, residual norms */ + opus_int length, /* I Number of samples */ + opus_int smooth_coef_Q16 /* I Smoothing coefficient */ +) +{ + opus_int scale, scale1, scale2; + opus_int32 nrgx, nrgy, corr, pred_Q13, pred2_Q10; + + /* Find predictor */ + silk_sum_sqr_shift( &nrgx, &scale1, x, length ); + silk_sum_sqr_shift( &nrgy, &scale2, y, length ); + scale = silk_max_int( scale1, scale2 ); + scale = scale + ( scale & 1 ); /* make even */ + nrgy = silk_RSHIFT32( nrgy, scale - scale2 ); + nrgx = silk_RSHIFT32( nrgx, scale - scale1 ); + nrgx = silk_max_int( nrgx, 1 ); + corr = silk_inner_prod_aligned_scale( x, y, scale, length ); + pred_Q13 = silk_DIV32_varQ( corr, nrgx, 13 ); + pred_Q13 = silk_LIMIT( pred_Q13, -(1 << 14), 1 << 14 ); + pred2_Q10 = silk_SMULWB( pred_Q13, pred_Q13 ); + + /* Faster update for signals with large prediction parameters */ + smooth_coef_Q16 = (opus_int)silk_max_int( smooth_coef_Q16, silk_abs( pred2_Q10 ) ); + + /* Smoothed mid and residual norms */ + silk_assert( smooth_coef_Q16 < 32768 ); + scale = silk_RSHIFT( scale, 1 ); + mid_res_amp_Q0[ 0 ] = silk_SMLAWB( mid_res_amp_Q0[ 0 ], silk_LSHIFT( silk_SQRT_APPROX( nrgx ), scale ) - mid_res_amp_Q0[ 0 ], + smooth_coef_Q16 ); + /* Residual energy = nrgy - 2 * pred * corr + pred^2 * nrgx */ + nrgy = silk_SUB_LSHIFT32( nrgy, silk_SMULWB( corr, pred_Q13 ), 3 + 1 ); + nrgy = silk_ADD_LSHIFT32( nrgy, silk_SMULWB( nrgx, pred2_Q10 ), 6 ); + mid_res_amp_Q0[ 1 ] = silk_SMLAWB( mid_res_amp_Q0[ 1 ], silk_LSHIFT( silk_SQRT_APPROX( nrgy ), scale ) - mid_res_amp_Q0[ 1 ], + smooth_coef_Q16 ); + + /* Ratio of smoothed residual and mid norms */ + *ratio_Q14 = silk_DIV32_varQ( mid_res_amp_Q0[ 1 ], silk_max( mid_res_amp_Q0[ 0 ], 1 ), 14 ); + *ratio_Q14 = silk_LIMIT( *ratio_Q14, 0, 32767 ); + + return pred_Q13; +} diff --git a/port/third_party/opus/silk/stereo_quant_pred.c b/port/third_party/opus/silk/stereo_quant_pred.c new file mode 100644 index 00000000..d4ced6c3 --- /dev/null +++ b/port/third_party/opus/silk/stereo_quant_pred.c @@ -0,0 +1,73 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "main.h" + +/* Quantize mid/side predictors */ +void silk_stereo_quant_pred( + opus_int32 pred_Q13[], /* I/O Predictors (out: quantized) */ + opus_int8 ix[ 2 ][ 3 ] /* O Quantization indices */ +) +{ + opus_int i, j, n; + opus_int32 low_Q13, step_Q13, lvl_Q13, err_min_Q13, err_Q13, quant_pred_Q13 = 0; + + /* Quantize */ + for( n = 0; n < 2; n++ ) { + /* Brute-force search over quantization levels */ + err_min_Q13 = silk_int32_MAX; + for( i = 0; i < STEREO_QUANT_TAB_SIZE - 1; i++ ) { + low_Q13 = silk_stereo_pred_quant_Q13[ i ]; + step_Q13 = silk_SMULWB( silk_stereo_pred_quant_Q13[ i + 1 ] - low_Q13, + SILK_FIX_CONST( 0.5 / STEREO_QUANT_SUB_STEPS, 16 ) ); + for( j = 0; j < STEREO_QUANT_SUB_STEPS; j++ ) { + lvl_Q13 = silk_SMLABB( low_Q13, step_Q13, 2 * j + 1 ); + err_Q13 = silk_abs( pred_Q13[ n ] - lvl_Q13 ); + if( err_Q13 < err_min_Q13 ) { + err_min_Q13 = err_Q13; + quant_pred_Q13 = lvl_Q13; + ix[ n ][ 0 ] = i; + ix[ n ][ 1 ] = j; + } else { + /* Error increasing, so we're past the optimum */ + goto done; + } + } + } + done: + ix[ n ][ 2 ] = silk_DIV32_16( ix[ n ][ 0 ], 3 ); + ix[ n ][ 0 ] -= ix[ n ][ 2 ] * 3; + pred_Q13[ n ] = quant_pred_Q13; + } + + /* Subtract second from first predictor (helps when actually applying these) */ + pred_Q13[ 0 ] -= pred_Q13[ 1 ]; +} diff --git a/port/third_party/opus/silk/structs.h b/port/third_party/opus/silk/structs.h new file mode 100644 index 00000000..38243be1 --- /dev/null +++ b/port/third_party/opus/silk/structs.h @@ -0,0 +1,357 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_STRUCTS_H +#define SILK_STRUCTS_H + +#include "typedef.h" +#include "SigProc_FIX.h" +#include "define.h" +#include "entenc.h" +#include "entdec.h" + +#ifdef ENABLE_DEEP_PLC +#include "lpcnet.h" +#include "lpcnet_private.h" +#endif + +#ifdef ENABLE_DRED +#include "dred_encoder.h" +#include "dred_decoder.h" +#endif + +#ifdef ENABLE_OSCE +#include "osce_config.h" +#include "osce_structs.h" +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +/************************************/ +/* Noise shaping quantization state */ +/************************************/ +typedef struct { + opus_int16 xq[ 2 * MAX_FRAME_LENGTH ]; /* Buffer for quantized output signal */ + opus_int32 sLTP_shp_Q14[ 2 * MAX_FRAME_LENGTH ]; + opus_int32 sLPC_Q14[ MAX_SUB_FRAME_LENGTH + NSQ_LPC_BUF_LENGTH ]; + opus_int32 sAR2_Q14[ MAX_SHAPE_LPC_ORDER ]; + opus_int32 sLF_AR_shp_Q14; + opus_int32 sDiff_shp_Q14; + opus_int lagPrev; + opus_int sLTP_buf_idx; + opus_int sLTP_shp_buf_idx; + opus_int32 rand_seed; + opus_int32 prev_gain_Q16; + opus_int rewhite_flag; +} silk_nsq_state; + +/********************************/ +/* VAD state */ +/********************************/ +typedef struct { + opus_int32 AnaState[ 2 ]; /* Analysis filterbank state: 0-8 kHz */ + opus_int32 AnaState1[ 2 ]; /* Analysis filterbank state: 0-4 kHz */ + opus_int32 AnaState2[ 2 ]; /* Analysis filterbank state: 0-2 kHz */ + opus_int32 XnrgSubfr[ VAD_N_BANDS ]; /* Subframe energies */ + opus_int32 NrgRatioSmth_Q8[ VAD_N_BANDS ]; /* Smoothed energy level in each band */ + opus_int16 HPstate; /* State of differentiator in the lowest band */ + opus_int32 NL[ VAD_N_BANDS ]; /* Noise energy level in each band */ + opus_int32 inv_NL[ VAD_N_BANDS ]; /* Inverse noise energy level in each band */ + opus_int32 NoiseLevelBias[ VAD_N_BANDS ]; /* Noise level estimator bias/offset */ + opus_int32 counter; /* Frame counter used in the initial phase */ +} silk_VAD_state; + +/* Variable cut-off low-pass filter state */ +typedef struct { + opus_int32 In_LP_State[ 2 ]; /* Low pass filter state */ + opus_int32 transition_frame_no; /* Counter which is mapped to a cut-off frequency */ + opus_int mode; /* Operating mode, <0: switch down, >0: switch up; 0: do nothing */ + opus_int32 saved_fs_kHz; /* If non-zero, holds the last sampling rate before a bandwidth switching reset. */ +} silk_LP_state; + +/* Structure containing NLSF codebook */ +typedef struct { + const opus_int16 nVectors; + const opus_int16 order; + const opus_int16 quantStepSize_Q16; + const opus_int16 invQuantStepSize_Q6; + const opus_uint8 *CB1_NLSF_Q8; + const opus_int16 *CB1_Wght_Q9; + const opus_uint8 *CB1_iCDF; + const opus_uint8 *pred_Q8; + const opus_uint8 *ec_sel; + const opus_uint8 *ec_iCDF; + const opus_uint8 *ec_Rates_Q5; + const opus_int16 *deltaMin_Q15; +} silk_NLSF_CB_struct; + +typedef struct { + opus_int16 pred_prev_Q13[ 2 ]; + opus_int16 sMid[ 2 ]; + opus_int16 sSide[ 2 ]; + opus_int32 mid_side_amp_Q0[ 4 ]; + opus_int16 smth_width_Q14; + opus_int16 width_prev_Q14; + opus_int16 silent_side_len; + opus_int8 predIx[ MAX_FRAMES_PER_PACKET ][ 2 ][ 3 ]; + opus_int8 mid_only_flags[ MAX_FRAMES_PER_PACKET ]; +} stereo_enc_state; + +typedef struct { + opus_int16 pred_prev_Q13[ 2 ]; + opus_int16 sMid[ 2 ]; + opus_int16 sSide[ 2 ]; +} stereo_dec_state; + +typedef struct { + opus_int8 GainsIndices[ MAX_NB_SUBFR ]; + opus_int8 LTPIndex[ MAX_NB_SUBFR ]; + opus_int8 NLSFIndices[ MAX_LPC_ORDER + 1 ]; + opus_int16 lagIndex; + opus_int8 contourIndex; + opus_int8 signalType; + opus_int8 quantOffsetType; + opus_int8 NLSFInterpCoef_Q2; + opus_int8 PERIndex; + opus_int8 LTP_scaleIndex; + opus_int8 Seed; +} SideInfoIndices; + +/********************************/ +/* Encoder state */ +/********************************/ +typedef struct { + opus_int32 In_HP_State[ 2 ]; /* High pass filter state */ + opus_int32 variable_HP_smth1_Q15; /* State of first smoother */ + opus_int32 variable_HP_smth2_Q15; /* State of second smoother */ + silk_LP_state sLP; /* Low pass filter state */ + silk_VAD_state sVAD; /* Voice activity detector state */ + silk_nsq_state sNSQ; /* Noise Shape Quantizer State */ + opus_int16 prev_NLSFq_Q15[ MAX_LPC_ORDER ]; /* Previously quantized NLSF vector */ + opus_int speech_activity_Q8; /* Speech activity */ + opus_int allow_bandwidth_switch; /* Flag indicating that switching of internal bandwidth is allowed */ + opus_int8 LBRRprevLastGainIndex; + opus_int8 prevSignalType; + opus_int prevLag; + opus_int pitch_LPC_win_length; + opus_int max_pitch_lag; /* Highest possible pitch lag (samples) */ + opus_int32 API_fs_Hz; /* API sampling frequency (Hz) */ + opus_int32 prev_API_fs_Hz; /* Previous API sampling frequency (Hz) */ + opus_int maxInternal_fs_Hz; /* Maximum internal sampling frequency (Hz) */ + opus_int minInternal_fs_Hz; /* Minimum internal sampling frequency (Hz) */ + opus_int desiredInternal_fs_Hz; /* Soft request for internal sampling frequency (Hz) */ + opus_int fs_kHz; /* Internal sampling frequency (kHz) */ + opus_int nb_subfr; /* Number of 5 ms subframes in a frame */ + opus_int frame_length; /* Frame length (samples) */ + opus_int subfr_length; /* Subframe length (samples) */ + opus_int ltp_mem_length; /* Length of LTP memory */ + opus_int la_pitch; /* Look-ahead for pitch analysis (samples) */ + opus_int la_shape; /* Look-ahead for noise shape analysis (samples) */ + opus_int shapeWinLength; /* Window length for noise shape analysis (samples) */ + opus_int32 TargetRate_bps; /* Target bitrate (bps) */ + opus_int PacketSize_ms; /* Number of milliseconds to put in each packet */ + opus_int PacketLoss_perc; /* Packet loss rate measured by farend */ + opus_int32 frameCounter; + opus_int Complexity; /* Complexity setting */ + opus_int nStatesDelayedDecision; /* Number of states in delayed decision quantization */ + opus_int useInterpolatedNLSFs; /* Flag for using NLSF interpolation */ + opus_int shapingLPCOrder; /* Filter order for noise shaping filters */ + opus_int predictLPCOrder; /* Filter order for prediction filters */ + opus_int pitchEstimationComplexity; /* Complexity level for pitch estimator */ + opus_int pitchEstimationLPCOrder; /* Whitening filter order for pitch estimator */ + opus_int32 pitchEstimationThreshold_Q16; /* Threshold for pitch estimator */ + opus_int32 sum_log_gain_Q7; /* Cumulative max prediction gain */ + opus_int NLSF_MSVQ_Survivors; /* Number of survivors in NLSF MSVQ */ + opus_int first_frame_after_reset; /* Flag for deactivating NLSF interpolation, pitch prediction */ + opus_int controlled_since_last_payload; /* Flag for ensuring codec_control only runs once per packet */ + opus_int warping_Q16; /* Warping parameter for warped noise shaping */ + opus_int useCBR; /* Flag to enable constant bitrate */ + opus_int prefillFlag; /* Flag to indicate that only buffers are prefilled, no coding */ + const opus_uint8 *pitch_lag_low_bits_iCDF; /* Pointer to iCDF table for low bits of pitch lag index */ + const opus_uint8 *pitch_contour_iCDF; /* Pointer to iCDF table for pitch contour index */ + const silk_NLSF_CB_struct *psNLSF_CB; /* Pointer to NLSF codebook */ + opus_int input_quality_bands_Q15[ VAD_N_BANDS ]; + opus_int input_tilt_Q15; + opus_int SNR_dB_Q7; /* Quality setting */ + + opus_int8 VAD_flags[ MAX_FRAMES_PER_PACKET ]; + opus_int8 LBRR_flag; + opus_int LBRR_flags[ MAX_FRAMES_PER_PACKET ]; + + SideInfoIndices indices; + opus_int8 pulses[ MAX_FRAME_LENGTH ]; + + int arch; + + /* Input/output buffering */ + opus_int16 inputBuf[ MAX_FRAME_LENGTH + 2 ]; /* Buffer containing input signal */ + opus_int inputBufIx; + opus_int nFramesPerPacket; + opus_int nFramesEncoded; /* Number of frames analyzed in current packet */ + + opus_int nChannelsAPI; + opus_int nChannelsInternal; + opus_int channelNb; + + /* Parameters For LTP scaling Control */ + opus_int frames_since_onset; + + /* Specifically for entropy coding */ + opus_int ec_prevSignalType; + opus_int16 ec_prevLagIndex; + + silk_resampler_state_struct resampler_state; + + /* DTX */ + opus_int useDTX; /* Flag to enable DTX */ + opus_int inDTX; /* Flag to signal DTX period */ + opus_int noSpeechCounter; /* Counts concecutive nonactive frames, used by DTX */ + + /* Inband Low Bitrate Redundancy (LBRR) data */ + opus_int useInBandFEC; /* Saves the API setting for query */ + opus_int LBRR_enabled; /* Depends on useInBandFRC, bitrate and packet loss rate */ + opus_int LBRR_GainIncreases; /* Gains increment for coding LBRR frames */ + SideInfoIndices indices_LBRR[ MAX_FRAMES_PER_PACKET ]; + opus_int8 pulses_LBRR[ MAX_FRAMES_PER_PACKET ][ MAX_FRAME_LENGTH ]; +} silk_encoder_state; + + +#ifdef ENABLE_OSCE +typedef struct { + OSCEFeatureState features; + OSCEState state; + int method; +} silk_OSCE_struct; +#endif + +/* Struct for Packet Loss Concealment */ +typedef struct { + opus_int32 pitchL_Q8; /* Pitch lag to use for voiced concealment */ + opus_int16 LTPCoef_Q14[ LTP_ORDER ]; /* LTP coeficients to use for voiced concealment */ + opus_int16 prevLPC_Q12[ MAX_LPC_ORDER ]; + opus_int last_frame_lost; /* Was previous frame lost */ + opus_int32 rand_seed; /* Seed for unvoiced signal generation */ + opus_int16 randScale_Q14; /* Scaling of unvoiced random signal */ + opus_int32 conc_energy; + opus_int conc_energy_shift; + opus_int16 prevLTP_scale_Q14; + opus_int32 prevGain_Q16[ 2 ]; + opus_int fs_kHz; + opus_int nb_subfr; + opus_int subfr_length; + opus_int enable_deep_plc; +} silk_PLC_struct; + +/* Struct for CNG */ +typedef struct { + opus_int32 CNG_exc_buf_Q14[ MAX_FRAME_LENGTH ]; + opus_int16 CNG_smth_NLSF_Q15[ MAX_LPC_ORDER ]; + opus_int32 CNG_synth_state[ MAX_LPC_ORDER ]; + opus_int32 CNG_smth_Gain_Q16; + opus_int32 rand_seed; + opus_int fs_kHz; +} silk_CNG_struct; + +/********************************/ +/* Decoder state */ +/********************************/ +typedef struct { +#ifdef ENABLE_OSCE + silk_OSCE_struct osce; +#endif +#define SILK_DECODER_STATE_RESET_START prev_gain_Q16 + opus_int32 prev_gain_Q16; + opus_int32 exc_Q14[ MAX_FRAME_LENGTH ]; + opus_int32 sLPC_Q14_buf[ MAX_LPC_ORDER ]; + opus_int16 outBuf[ MAX_FRAME_LENGTH + 2 * MAX_SUB_FRAME_LENGTH ]; /* Buffer for output signal */ + opus_int lagPrev; /* Previous Lag */ + opus_int8 LastGainIndex; /* Previous gain index */ + opus_int fs_kHz; /* Sampling frequency in kHz */ + opus_int32 fs_API_hz; /* API sample frequency (Hz) */ + opus_int nb_subfr; /* Number of 5 ms subframes in a frame */ + opus_int frame_length; /* Frame length (samples) */ + opus_int subfr_length; /* Subframe length (samples) */ + opus_int ltp_mem_length; /* Length of LTP memory */ + opus_int LPC_order; /* LPC order */ + opus_int16 prevNLSF_Q15[ MAX_LPC_ORDER ]; /* Used to interpolate LSFs */ + opus_int first_frame_after_reset; /* Flag for deactivating NLSF interpolation */ + const opus_uint8 *pitch_lag_low_bits_iCDF; /* Pointer to iCDF table for low bits of pitch lag index */ + const opus_uint8 *pitch_contour_iCDF; /* Pointer to iCDF table for pitch contour index */ + + /* For buffering payload in case of more frames per packet */ + opus_int nFramesDecoded; + opus_int nFramesPerPacket; + + /* Specifically for entropy coding */ + opus_int ec_prevSignalType; + opus_int16 ec_prevLagIndex; + + opus_int VAD_flags[ MAX_FRAMES_PER_PACKET ]; + opus_int LBRR_flag; + opus_int LBRR_flags[ MAX_FRAMES_PER_PACKET ]; + + silk_resampler_state_struct resampler_state; + + const silk_NLSF_CB_struct *psNLSF_CB; /* Pointer to NLSF codebook */ + + /* Quantization indices */ + SideInfoIndices indices; + + /* CNG state */ + silk_CNG_struct sCNG; + + /* Stuff used for PLC */ + opus_int lossCnt; + opus_int prevSignalType; + int arch; + + silk_PLC_struct sPLC; + +} silk_decoder_state; + +/************************/ +/* Decoder control */ +/************************/ +typedef struct { + /* Prediction and coding parameters */ + opus_int pitchL[ MAX_NB_SUBFR ]; + opus_int32 Gains_Q16[ MAX_NB_SUBFR ]; + /* Holds interpolated and final coefficients, 4-byte aligned */ + silk_DWORD_ALIGN opus_int16 PredCoef_Q12[ 2 ][ MAX_LPC_ORDER ]; + opus_int16 LTPCoef_Q14[ LTP_ORDER * MAX_NB_SUBFR ]; + opus_int LTP_scale_Q14; +} silk_decoder_control; + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/port/third_party/opus/silk/sum_sqr_shift.c b/port/third_party/opus/silk/sum_sqr_shift.c new file mode 100644 index 00000000..4fd0c3d7 --- /dev/null +++ b/port/third_party/opus/silk/sum_sqr_shift.c @@ -0,0 +1,83 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "SigProc_FIX.h" + +/* Compute number of bits to right shift the sum of squares of a vector */ +/* of int16s to make it fit in an int32 */ +void silk_sum_sqr_shift( + opus_int32 *energy, /* O Energy of x, after shifting to the right */ + opus_int *shift, /* O Number of bits right shift applied to energy */ + const opus_int16 *x, /* I Input vector */ + opus_int len /* I Length of input vector */ +) +{ + opus_int i, shft; + opus_uint32 nrg_tmp; + opus_int32 nrg; + + /* Do a first run with the maximum shift we could have. */ + shft = 31-silk_CLZ32(len); + /* Let's be conservative with rounding and start with nrg=len. */ + nrg = len; + for( i = 0; i < len - 1; i += 2 ) { + nrg_tmp = silk_SMULBB( x[ i ], x[ i ] ); + nrg_tmp = silk_SMLABB_ovflw( nrg_tmp, x[ i + 1 ], x[ i + 1 ] ); + nrg = (opus_int32)silk_ADD_RSHIFT_uint( nrg, nrg_tmp, shft ); + } + if( i < len ) { + /* One sample left to process */ + nrg_tmp = silk_SMULBB( x[ i ], x[ i ] ); + nrg = (opus_int32)silk_ADD_RSHIFT_uint( nrg, nrg_tmp, shft ); + } + silk_assert( nrg >= 0 ); + /* Make sure the result will fit in a 32-bit signed integer with two bits + of headroom. */ + shft = silk_max_32(0, shft+3 - silk_CLZ32(nrg)); + nrg = 0; + for( i = 0 ; i < len - 1; i += 2 ) { + nrg_tmp = silk_SMULBB( x[ i ], x[ i ] ); + nrg_tmp = silk_SMLABB_ovflw( nrg_tmp, x[ i + 1 ], x[ i + 1 ] ); + nrg = (opus_int32)silk_ADD_RSHIFT_uint( nrg, nrg_tmp, shft ); + } + if( i < len ) { + /* One sample left to process */ + nrg_tmp = silk_SMULBB( x[ i ], x[ i ] ); + nrg = (opus_int32)silk_ADD_RSHIFT_uint( nrg, nrg_tmp, shft ); + } + + silk_assert( nrg >= 0 ); + + /* Output arguments */ + *shift = shft; + *energy = nrg; +} + diff --git a/port/third_party/opus/silk/table_LSF_cos.c b/port/third_party/opus/silk/table_LSF_cos.c new file mode 100644 index 00000000..ec9dc639 --- /dev/null +++ b/port/third_party/opus/silk/table_LSF_cos.c @@ -0,0 +1,70 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "tables.h" + +/* Cosine approximation table for LSF conversion */ +/* Q12 values (even) */ +const opus_int16 silk_LSFCosTab_FIX_Q12[ LSF_COS_TAB_SZ_FIX + 1 ] = { + 8192, 8190, 8182, 8170, + 8152, 8130, 8104, 8072, + 8034, 7994, 7946, 7896, + 7840, 7778, 7714, 7644, + 7568, 7490, 7406, 7318, + 7226, 7128, 7026, 6922, + 6812, 6698, 6580, 6458, + 6332, 6204, 6070, 5934, + 5792, 5648, 5502, 5352, + 5198, 5040, 4880, 4718, + 4552, 4382, 4212, 4038, + 3862, 3684, 3502, 3320, + 3136, 2948, 2760, 2570, + 2378, 2186, 1990, 1794, + 1598, 1400, 1202, 1002, + 802, 602, 402, 202, + 0, -202, -402, -602, + -802, -1002, -1202, -1400, + -1598, -1794, -1990, -2186, + -2378, -2570, -2760, -2948, + -3136, -3320, -3502, -3684, + -3862, -4038, -4212, -4382, + -4552, -4718, -4880, -5040, + -5198, -5352, -5502, -5648, + -5792, -5934, -6070, -6204, + -6332, -6458, -6580, -6698, + -6812, -6922, -7026, -7128, + -7226, -7318, -7406, -7490, + -7568, -7644, -7714, -7778, + -7840, -7896, -7946, -7994, + -8034, -8072, -8104, -8130, + -8152, -8170, -8182, -8190, + -8192 +}; diff --git a/port/third_party/opus/silk/tables.h b/port/third_party/opus/silk/tables.h new file mode 100644 index 00000000..95230c45 --- /dev/null +++ b/port/third_party/opus/silk/tables.h @@ -0,0 +1,114 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_TABLES_H +#define SILK_TABLES_H + +#include "define.h" +#include "structs.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Entropy coding tables (with size in bytes indicated) */ +extern const opus_uint8 silk_gain_iCDF[ 3 ][ N_LEVELS_QGAIN / 8 ]; /* 24 */ +extern const opus_uint8 silk_delta_gain_iCDF[ MAX_DELTA_GAIN_QUANT - MIN_DELTA_GAIN_QUANT + 1 ]; /* 41 */ + +extern const opus_uint8 silk_pitch_lag_iCDF[ 2 * ( PITCH_EST_MAX_LAG_MS - PITCH_EST_MIN_LAG_MS ) ];/* 32 */ +extern const opus_uint8 silk_pitch_delta_iCDF[ 21 ]; /* 21 */ +extern const opus_uint8 silk_pitch_contour_iCDF[ 34 ]; /* 34 */ +extern const opus_uint8 silk_pitch_contour_NB_iCDF[ 11 ]; /* 11 */ +extern const opus_uint8 silk_pitch_contour_10_ms_iCDF[ 12 ]; /* 12 */ +extern const opus_uint8 silk_pitch_contour_10_ms_NB_iCDF[ 3 ]; /* 3 */ + +extern const opus_uint8 silk_pulses_per_block_iCDF[ N_RATE_LEVELS ][ SILK_MAX_PULSES + 2 ]; /* 180 */ +extern const opus_uint8 silk_pulses_per_block_BITS_Q5[ N_RATE_LEVELS - 1 ][ SILK_MAX_PULSES + 2 ]; /* 162 */ + +extern const opus_uint8 silk_rate_levels_iCDF[ 2 ][ N_RATE_LEVELS - 1 ]; /* 18 */ +extern const opus_uint8 silk_rate_levels_BITS_Q5[ 2 ][ N_RATE_LEVELS - 1 ]; /* 18 */ + +extern const opus_uint8 silk_max_pulses_table[ 4 ]; /* 4 */ + +extern const opus_uint8 silk_shell_code_table0[ 152 ]; /* 152 */ +extern const opus_uint8 silk_shell_code_table1[ 152 ]; /* 152 */ +extern const opus_uint8 silk_shell_code_table2[ 152 ]; /* 152 */ +extern const opus_uint8 silk_shell_code_table3[ 152 ]; /* 152 */ +extern const opus_uint8 silk_shell_code_table_offsets[ SILK_MAX_PULSES + 1 ]; /* 17 */ + +extern const opus_uint8 silk_lsb_iCDF[ 2 ]; /* 2 */ + +extern const opus_uint8 silk_sign_iCDF[ 42 ]; /* 42 */ + +extern const opus_uint8 silk_uniform3_iCDF[ 3 ]; /* 3 */ +extern const opus_uint8 silk_uniform4_iCDF[ 4 ]; /* 4 */ +extern const opus_uint8 silk_uniform5_iCDF[ 5 ]; /* 5 */ +extern const opus_uint8 silk_uniform6_iCDF[ 6 ]; /* 6 */ +extern const opus_uint8 silk_uniform8_iCDF[ 8 ]; /* 8 */ + +extern const opus_uint8 silk_NLSF_EXT_iCDF[ 7 ]; /* 7 */ + +extern const opus_uint8 silk_LTP_per_index_iCDF[ 3 ]; /* 3 */ +extern const opus_uint8 * const silk_LTP_gain_iCDF_ptrs[ NB_LTP_CBKS ]; /* 3 */ +extern const opus_uint8 * const silk_LTP_gain_BITS_Q5_ptrs[ NB_LTP_CBKS ]; /* 3 */ +extern const opus_int8 * const silk_LTP_vq_ptrs_Q7[ NB_LTP_CBKS ]; /* 168 */ +extern const opus_uint8 * const silk_LTP_vq_gain_ptrs_Q7[NB_LTP_CBKS]; +extern const opus_int8 silk_LTP_vq_sizes[ NB_LTP_CBKS ]; /* 3 */ + +extern const opus_uint8 silk_LTPscale_iCDF[ 3 ]; /* 4 */ +extern const opus_int16 silk_LTPScales_table_Q14[ 3 ]; /* 6 */ + +extern const opus_uint8 silk_type_offset_VAD_iCDF[ 4 ]; /* 4 */ +extern const opus_uint8 silk_type_offset_no_VAD_iCDF[ 2 ]; /* 2 */ + +extern const opus_int16 silk_stereo_pred_quant_Q13[ STEREO_QUANT_TAB_SIZE ]; /* 32 */ +extern const opus_uint8 silk_stereo_pred_joint_iCDF[ 25 ]; /* 25 */ +extern const opus_uint8 silk_stereo_only_code_mid_iCDF[ 2 ]; /* 2 */ + +extern const opus_uint8 * const silk_LBRR_flags_iCDF_ptr[ 2 ]; /* 10 */ + +extern const opus_uint8 silk_NLSF_interpolation_factor_iCDF[ 5 ]; /* 5 */ + +extern const silk_NLSF_CB_struct silk_NLSF_CB_WB; /* 1040 */ +extern const silk_NLSF_CB_struct silk_NLSF_CB_NB_MB; /* 728 */ + +/* Quantization offsets */ +extern const opus_int16 silk_Quantization_Offsets_Q10[ 2 ][ 2 ]; /* 8 */ + +/* Interpolation points for filter coefficients used in the bandwidth transition smoother */ +extern const opus_int32 silk_Transition_LP_B_Q28[ TRANSITION_INT_NUM ][ TRANSITION_NB ]; /* 60 */ +extern const opus_int32 silk_Transition_LP_A_Q28[ TRANSITION_INT_NUM ][ TRANSITION_NA ]; /* 60 */ + +/* Rom table with cosine values */ +extern const opus_int16 silk_LSFCosTab_FIX_Q12[ LSF_COS_TAB_SZ_FIX + 1 ]; /* 258 */ + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/port/third_party/opus/silk/tables_LTP.c b/port/third_party/opus/silk/tables_LTP.c new file mode 100644 index 00000000..5e12c864 --- /dev/null +++ b/port/third_party/opus/silk/tables_LTP.c @@ -0,0 +1,294 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "tables.h" + +const opus_uint8 silk_LTP_per_index_iCDF[3] = { + 179, 99, 0 +}; + +static const opus_uint8 silk_LTP_gain_iCDF_0[8] = { + 71, 56, 43, 30, 21, 12, 6, 0 +}; + +static const opus_uint8 silk_LTP_gain_iCDF_1[16] = { + 199, 165, 144, 124, 109, 96, 84, 71, + 61, 51, 42, 32, 23, 15, 8, 0 +}; + +static const opus_uint8 silk_LTP_gain_iCDF_2[32] = { + 241, 225, 211, 199, 187, 175, 164, 153, + 142, 132, 123, 114, 105, 96, 88, 80, + 72, 64, 57, 50, 44, 38, 33, 29, + 24, 20, 16, 12, 9, 5, 2, 0 +}; + +static const opus_uint8 silk_LTP_gain_BITS_Q5_0[8] = { + 15, 131, 138, 138, 155, 155, 173, 173 +}; + +static const opus_uint8 silk_LTP_gain_BITS_Q5_1[16] = { + 69, 93, 115, 118, 131, 138, 141, 138, + 150, 150, 155, 150, 155, 160, 166, 160 +}; + +static const opus_uint8 silk_LTP_gain_BITS_Q5_2[32] = { + 131, 128, 134, 141, 141, 141, 145, 145, + 145, 150, 155, 155, 155, 155, 160, 160, + 160, 160, 166, 166, 173, 173, 182, 192, + 182, 192, 192, 192, 205, 192, 205, 224 +}; + +const opus_uint8 * const silk_LTP_gain_iCDF_ptrs[NB_LTP_CBKS] = { + silk_LTP_gain_iCDF_0, + silk_LTP_gain_iCDF_1, + silk_LTP_gain_iCDF_2 +}; + +const opus_uint8 * const silk_LTP_gain_BITS_Q5_ptrs[NB_LTP_CBKS] = { + silk_LTP_gain_BITS_Q5_0, + silk_LTP_gain_BITS_Q5_1, + silk_LTP_gain_BITS_Q5_2 +}; + +static const opus_int8 silk_LTP_gain_vq_0[8][5] = +{ +{ + 4, 6, 24, 7, 5 +}, +{ + 0, 0, 2, 0, 0 +}, +{ + 12, 28, 41, 13, -4 +}, +{ + -9, 15, 42, 25, 14 +}, +{ + 1, -2, 62, 41, -9 +}, +{ + -10, 37, 65, -4, 3 +}, +{ + -6, 4, 66, 7, -8 +}, +{ + 16, 14, 38, -3, 33 +} +}; + +static const opus_int8 silk_LTP_gain_vq_1[16][5] = +{ +{ + 13, 22, 39, 23, 12 +}, +{ + -1, 36, 64, 27, -6 +}, +{ + -7, 10, 55, 43, 17 +}, +{ + 1, 1, 8, 1, 1 +}, +{ + 6, -11, 74, 53, -9 +}, +{ + -12, 55, 76, -12, 8 +}, +{ + -3, 3, 93, 27, -4 +}, +{ + 26, 39, 59, 3, -8 +}, +{ + 2, 0, 77, 11, 9 +}, +{ + -8, 22, 44, -6, 7 +}, +{ + 40, 9, 26, 3, 9 +}, +{ + -7, 20, 101, -7, 4 +}, +{ + 3, -8, 42, 26, 0 +}, +{ + -15, 33, 68, 2, 23 +}, +{ + -2, 55, 46, -2, 15 +}, +{ + 3, -1, 21, 16, 41 +} +}; + +static const opus_int8 silk_LTP_gain_vq_2[32][5] = +{ +{ + -6, 27, 61, 39, 5 +}, +{ + -11, 42, 88, 4, 1 +}, +{ + -2, 60, 65, 6, -4 +}, +{ + -1, -5, 73, 56, 1 +}, +{ + -9, 19, 94, 29, -9 +}, +{ + 0, 12, 99, 6, 4 +}, +{ + 8, -19, 102, 46, -13 +}, +{ + 3, 2, 13, 3, 2 +}, +{ + 9, -21, 84, 72, -18 +}, +{ + -11, 46, 104, -22, 8 +}, +{ + 18, 38, 48, 23, 0 +}, +{ + -16, 70, 83, -21, 11 +}, +{ + 5, -11, 117, 22, -8 +}, +{ + -6, 23, 117, -12, 3 +}, +{ + 3, -8, 95, 28, 4 +}, +{ + -10, 15, 77, 60, -15 +}, +{ + -1, 4, 124, 2, -4 +}, +{ + 3, 38, 84, 24, -25 +}, +{ + 2, 13, 42, 13, 31 +}, +{ + 21, -4, 56, 46, -1 +}, +{ + -1, 35, 79, -13, 19 +}, +{ + -7, 65, 88, -9, -14 +}, +{ + 20, 4, 81, 49, -29 +}, +{ + 20, 0, 75, 3, -17 +}, +{ + 5, -9, 44, 92, -8 +}, +{ + 1, -3, 22, 69, 31 +}, +{ + -6, 95, 41, -12, 5 +}, +{ + 39, 67, 16, -4, 1 +}, +{ + 0, -6, 120, 55, -36 +}, +{ + -13, 44, 122, 4, -24 +}, +{ + 81, 5, 11, 3, 7 +}, +{ + 2, 0, 9, 10, 88 +} +}; + +const opus_int8 * const silk_LTP_vq_ptrs_Q7[NB_LTP_CBKS] = { + (opus_int8 *)&silk_LTP_gain_vq_0[0][0], + (opus_int8 *)&silk_LTP_gain_vq_1[0][0], + (opus_int8 *)&silk_LTP_gain_vq_2[0][0] +}; + +/* Maximum frequency-dependent response of the pitch taps above, + computed as max(abs(freqz(taps))) */ +static const opus_uint8 silk_LTP_gain_vq_0_gain[8] = { + 46, 2, 90, 87, 93, 91, 82, 98 +}; + +static const opus_uint8 silk_LTP_gain_vq_1_gain[16] = { + 109, 120, 118, 12, 113, 115, 117, 119, + 99, 59, 87, 111, 63, 111, 112, 80 +}; + +static const opus_uint8 silk_LTP_gain_vq_2_gain[32] = { + 126, 124, 125, 124, 129, 121, 126, 23, + 132, 127, 127, 127, 126, 127, 122, 133, + 130, 134, 101, 118, 119, 145, 126, 86, + 124, 120, 123, 119, 170, 173, 107, 109 +}; + +const opus_uint8 * const silk_LTP_vq_gain_ptrs_Q7[NB_LTP_CBKS] = { + &silk_LTP_gain_vq_0_gain[0], + &silk_LTP_gain_vq_1_gain[0], + &silk_LTP_gain_vq_2_gain[0] +}; + +const opus_int8 silk_LTP_vq_sizes[NB_LTP_CBKS] = { + 8, 16, 32 +}; diff --git a/port/third_party/opus/silk/tables_NLSF_CB_NB_MB.c b/port/third_party/opus/silk/tables_NLSF_CB_NB_MB.c new file mode 100644 index 00000000..195d5b95 --- /dev/null +++ b/port/third_party/opus/silk/tables_NLSF_CB_NB_MB.c @@ -0,0 +1,195 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "tables.h" + +static const opus_uint8 silk_NLSF_CB1_NB_MB_Q8[ 320 ] = { + 12, 35, 60, 83, 108, 132, 157, 180, + 206, 228, 15, 32, 55, 77, 101, 125, + 151, 175, 201, 225, 19, 42, 66, 89, + 114, 137, 162, 184, 209, 230, 12, 25, + 50, 72, 97, 120, 147, 172, 200, 223, + 26, 44, 69, 90, 114, 135, 159, 180, + 205, 225, 13, 22, 53, 80, 106, 130, + 156, 180, 205, 228, 15, 25, 44, 64, + 90, 115, 142, 168, 196, 222, 19, 24, + 62, 82, 100, 120, 145, 168, 190, 214, + 22, 31, 50, 79, 103, 120, 151, 170, + 203, 227, 21, 29, 45, 65, 106, 124, + 150, 171, 196, 224, 30, 49, 75, 97, + 121, 142, 165, 186, 209, 229, 19, 25, + 52, 70, 93, 116, 143, 166, 192, 219, + 26, 34, 62, 75, 97, 118, 145, 167, + 194, 217, 25, 33, 56, 70, 91, 113, + 143, 165, 196, 223, 21, 34, 51, 72, + 97, 117, 145, 171, 196, 222, 20, 29, + 50, 67, 90, 117, 144, 168, 197, 221, + 22, 31, 48, 66, 95, 117, 146, 168, + 196, 222, 24, 33, 51, 77, 116, 134, + 158, 180, 200, 224, 21, 28, 70, 87, + 106, 124, 149, 170, 194, 217, 26, 33, + 53, 64, 83, 117, 152, 173, 204, 225, + 27, 34, 65, 95, 108, 129, 155, 174, + 210, 225, 20, 26, 72, 99, 113, 131, + 154, 176, 200, 219, 34, 43, 61, 78, + 93, 114, 155, 177, 205, 229, 23, 29, + 54, 97, 124, 138, 163, 179, 209, 229, + 30, 38, 56, 89, 118, 129, 158, 178, + 200, 231, 21, 29, 49, 63, 85, 111, + 142, 163, 193, 222, 27, 48, 77, 103, + 133, 158, 179, 196, 215, 232, 29, 47, + 74, 99, 124, 151, 176, 198, 220, 237, + 33, 42, 61, 76, 93, 121, 155, 174, + 207, 225, 29, 53, 87, 112, 136, 154, + 170, 188, 208, 227, 24, 30, 52, 84, + 131, 150, 166, 186, 203, 229, 37, 48, + 64, 84, 104, 118, 156, 177, 201, 230 +}; + +static const opus_int16 silk_NLSF_CB1_Wght_Q9[ 320 ] = { + 2897, 2314, 2314, 2314, 2287, 2287, 2314, 2300, 2327, 2287, + 2888, 2580, 2394, 2367, 2314, 2274, 2274, 2274, 2274, 2194, + 2487, 2340, 2340, 2314, 2314, 2314, 2340, 2340, 2367, 2354, + 3216, 2766, 2340, 2340, 2314, 2274, 2221, 2207, 2261, 2194, + 2460, 2474, 2367, 2394, 2394, 2394, 2394, 2367, 2407, 2314, + 3479, 3056, 2127, 2207, 2274, 2274, 2274, 2287, 2314, 2261, + 3282, 3141, 2580, 2394, 2247, 2221, 2207, 2194, 2194, 2114, + 4096, 3845, 2221, 2620, 2620, 2407, 2314, 2394, 2367, 2074, + 3178, 3244, 2367, 2221, 2553, 2434, 2340, 2314, 2167, 2221, + 3338, 3488, 2726, 2194, 2261, 2460, 2354, 2367, 2207, 2101, + 2354, 2420, 2327, 2367, 2394, 2420, 2420, 2420, 2460, 2367, + 3779, 3629, 2434, 2527, 2367, 2274, 2274, 2300, 2207, 2048, + 3254, 3225, 2713, 2846, 2447, 2327, 2300, 2300, 2274, 2127, + 3263, 3300, 2753, 2806, 2447, 2261, 2261, 2247, 2127, 2101, + 2873, 2981, 2633, 2367, 2407, 2354, 2194, 2247, 2247, 2114, + 3225, 3197, 2633, 2580, 2274, 2181, 2247, 2221, 2221, 2141, + 3178, 3310, 2740, 2407, 2274, 2274, 2274, 2287, 2194, 2114, + 3141, 3272, 2460, 2061, 2287, 2500, 2367, 2487, 2434, 2181, + 3507, 3282, 2314, 2700, 2647, 2474, 2367, 2394, 2340, 2127, + 3423, 3535, 3038, 3056, 2300, 1950, 2221, 2274, 2274, 2274, + 3404, 3366, 2087, 2687, 2873, 2354, 2420, 2274, 2474, 2540, + 3760, 3488, 1950, 2660, 2897, 2527, 2394, 2367, 2460, 2261, + 3028, 3272, 2740, 2888, 2740, 2154, 2127, 2287, 2234, 2247, + 3695, 3657, 2025, 1969, 2660, 2700, 2580, 2500, 2327, 2367, + 3207, 3413, 2354, 2074, 2888, 2888, 2340, 2487, 2247, 2167, + 3338, 3366, 2846, 2780, 2327, 2154, 2274, 2287, 2114, 2061, + 2327, 2300, 2181, 2167, 2181, 2367, 2633, 2700, 2700, 2553, + 2407, 2434, 2221, 2261, 2221, 2221, 2340, 2420, 2607, 2700, + 3038, 3244, 2806, 2888, 2474, 2074, 2300, 2314, 2354, 2380, + 2221, 2154, 2127, 2287, 2500, 2793, 2793, 2620, 2580, 2367, + 3676, 3713, 2234, 1838, 2181, 2753, 2726, 2673, 2513, 2207, + 2793, 3160, 2726, 2553, 2846, 2513, 2181, 2394, 2221, 2181 +}; + +static const opus_uint8 silk_NLSF_CB1_iCDF_NB_MB[ 64 ] = { + 212, 178, 148, 129, 108, 96, 85, 82, + 79, 77, 61, 59, 57, 56, 51, 49, + 48, 45, 42, 41, 40, 38, 36, 34, + 31, 30, 21, 12, 10, 3, 1, 0, + 255, 245, 244, 236, 233, 225, 217, 203, + 190, 176, 175, 161, 149, 136, 125, 114, + 102, 91, 81, 71, 60, 52, 43, 35, + 28, 20, 19, 18, 12, 11, 5, 0 +}; + +static const opus_uint8 silk_NLSF_CB2_SELECT_NB_MB[ 160 ] = { + 16, 0, 0, 0, 0, 99, 66, 36, + 36, 34, 36, 34, 34, 34, 34, 83, + 69, 36, 52, 34, 116, 102, 70, 68, + 68, 176, 102, 68, 68, 34, 65, 85, + 68, 84, 36, 116, 141, 152, 139, 170, + 132, 187, 184, 216, 137, 132, 249, 168, + 185, 139, 104, 102, 100, 68, 68, 178, + 218, 185, 185, 170, 244, 216, 187, 187, + 170, 244, 187, 187, 219, 138, 103, 155, + 184, 185, 137, 116, 183, 155, 152, 136, + 132, 217, 184, 184, 170, 164, 217, 171, + 155, 139, 244, 169, 184, 185, 170, 164, + 216, 223, 218, 138, 214, 143, 188, 218, + 168, 244, 141, 136, 155, 170, 168, 138, + 220, 219, 139, 164, 219, 202, 216, 137, + 168, 186, 246, 185, 139, 116, 185, 219, + 185, 138, 100, 100, 134, 100, 102, 34, + 68, 68, 100, 68, 168, 203, 221, 218, + 168, 167, 154, 136, 104, 70, 164, 246, + 171, 137, 139, 137, 155, 218, 219, 139 +}; + +static const opus_uint8 silk_NLSF_CB2_iCDF_NB_MB[ 72 ] = { + 255, 254, 253, 238, 14, 3, 2, 1, + 0, 255, 254, 252, 218, 35, 3, 2, + 1, 0, 255, 254, 250, 208, 59, 4, + 2, 1, 0, 255, 254, 246, 194, 71, + 10, 2, 1, 0, 255, 252, 236, 183, + 82, 8, 2, 1, 0, 255, 252, 235, + 180, 90, 17, 2, 1, 0, 255, 248, + 224, 171, 97, 30, 4, 1, 0, 255, + 254, 236, 173, 95, 37, 7, 1, 0 +}; + +static const opus_uint8 silk_NLSF_CB2_BITS_NB_MB_Q5[ 72 ] = { + 255, 255, 255, 131, 6, 145, 255, 255, + 255, 255, 255, 236, 93, 15, 96, 255, + 255, 255, 255, 255, 194, 83, 25, 71, + 221, 255, 255, 255, 255, 162, 73, 34, + 66, 162, 255, 255, 255, 210, 126, 73, + 43, 57, 173, 255, 255, 255, 201, 125, + 71, 48, 58, 130, 255, 255, 255, 166, + 110, 73, 57, 62, 104, 210, 255, 255, + 251, 123, 65, 55, 68, 100, 171, 255 +}; + +static const opus_uint8 silk_NLSF_PRED_NB_MB_Q8[ 18 ] = { + 179, 138, 140, 148, 151, 149, 153, 151, + 163, 116, 67, 82, 59, 92, 72, 100, + 89, 92 +}; + +static const opus_int16 silk_NLSF_DELTA_MIN_NB_MB_Q15[ 11 ] = { + 250, 3, 6, 3, 3, 3, 4, 3, + 3, 3, 461 +}; + +const silk_NLSF_CB_struct silk_NLSF_CB_NB_MB = +{ + 32, + 10, + SILK_FIX_CONST( 0.18, 16 ), + SILK_FIX_CONST( 1.0 / 0.18, 6 ), + silk_NLSF_CB1_NB_MB_Q8, + silk_NLSF_CB1_Wght_Q9, + silk_NLSF_CB1_iCDF_NB_MB, + silk_NLSF_PRED_NB_MB_Q8, + silk_NLSF_CB2_SELECT_NB_MB, + silk_NLSF_CB2_iCDF_NB_MB, + silk_NLSF_CB2_BITS_NB_MB_Q5, + silk_NLSF_DELTA_MIN_NB_MB_Q15, +}; diff --git a/port/third_party/opus/silk/tables_NLSF_CB_WB.c b/port/third_party/opus/silk/tables_NLSF_CB_WB.c new file mode 100644 index 00000000..5cc9f57b --- /dev/null +++ b/port/third_party/opus/silk/tables_NLSF_CB_WB.c @@ -0,0 +1,234 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "tables.h" + +static const opus_uint8 silk_NLSF_CB1_WB_Q8[ 512 ] = { + 7, 23, 38, 54, 69, 85, 100, 116, + 131, 147, 162, 178, 193, 208, 223, 239, + 13, 25, 41, 55, 69, 83, 98, 112, + 127, 142, 157, 171, 187, 203, 220, 236, + 15, 21, 34, 51, 61, 78, 92, 106, + 126, 136, 152, 167, 185, 205, 225, 240, + 10, 21, 36, 50, 63, 79, 95, 110, + 126, 141, 157, 173, 189, 205, 221, 237, + 17, 20, 37, 51, 59, 78, 89, 107, + 123, 134, 150, 164, 184, 205, 224, 240, + 10, 15, 32, 51, 67, 81, 96, 112, + 129, 142, 158, 173, 189, 204, 220, 236, + 8, 21, 37, 51, 65, 79, 98, 113, + 126, 138, 155, 168, 179, 192, 209, 218, + 12, 15, 34, 55, 63, 78, 87, 108, + 118, 131, 148, 167, 185, 203, 219, 236, + 16, 19, 32, 36, 56, 79, 91, 108, + 118, 136, 154, 171, 186, 204, 220, 237, + 11, 28, 43, 58, 74, 89, 105, 120, + 135, 150, 165, 180, 196, 211, 226, 241, + 6, 16, 33, 46, 60, 75, 92, 107, + 123, 137, 156, 169, 185, 199, 214, 225, + 11, 19, 30, 44, 57, 74, 89, 105, + 121, 135, 152, 169, 186, 202, 218, 234, + 12, 19, 29, 46, 57, 71, 88, 100, + 120, 132, 148, 165, 182, 199, 216, 233, + 17, 23, 35, 46, 56, 77, 92, 106, + 123, 134, 152, 167, 185, 204, 222, 237, + 14, 17, 45, 53, 63, 75, 89, 107, + 115, 132, 151, 171, 188, 206, 221, 240, + 9, 16, 29, 40, 56, 71, 88, 103, + 119, 137, 154, 171, 189, 205, 222, 237, + 16, 19, 36, 48, 57, 76, 87, 105, + 118, 132, 150, 167, 185, 202, 218, 236, + 12, 17, 29, 54, 71, 81, 94, 104, + 126, 136, 149, 164, 182, 201, 221, 237, + 15, 28, 47, 62, 79, 97, 115, 129, + 142, 155, 168, 180, 194, 208, 223, 238, + 8, 14, 30, 45, 62, 78, 94, 111, + 127, 143, 159, 175, 192, 207, 223, 239, + 17, 30, 49, 62, 79, 92, 107, 119, + 132, 145, 160, 174, 190, 204, 220, 235, + 14, 19, 36, 45, 61, 76, 91, 108, + 121, 138, 154, 172, 189, 205, 222, 238, + 12, 18, 31, 45, 60, 76, 91, 107, + 123, 138, 154, 171, 187, 204, 221, 236, + 13, 17, 31, 43, 53, 70, 83, 103, + 114, 131, 149, 167, 185, 203, 220, 237, + 17, 22, 35, 42, 58, 78, 93, 110, + 125, 139, 155, 170, 188, 206, 224, 240, + 8, 15, 34, 50, 67, 83, 99, 115, + 131, 146, 162, 178, 193, 209, 224, 239, + 13, 16, 41, 66, 73, 86, 95, 111, + 128, 137, 150, 163, 183, 206, 225, 241, + 17, 25, 37, 52, 63, 75, 92, 102, + 119, 132, 144, 160, 175, 191, 212, 231, + 19, 31, 49, 65, 83, 100, 117, 133, + 147, 161, 174, 187, 200, 213, 227, 242, + 18, 31, 52, 68, 88, 103, 117, 126, + 138, 149, 163, 177, 192, 207, 223, 239, + 16, 29, 47, 61, 76, 90, 106, 119, + 133, 147, 161, 176, 193, 209, 224, 240, + 15, 21, 35, 50, 61, 73, 86, 97, + 110, 119, 129, 141, 175, 198, 218, 237 +}; + +static const opus_int16 silk_NLSF_CB1_WB_Wght_Q9[ 512 ] = { + 3657, 2925, 2925, 2925, 2925, 2925, 2925, 2925, 2925, 2925, 2925, 2925, 2963, 2963, 2925, 2846, + 3216, 3085, 2972, 3056, 3056, 3010, 3010, 3010, 2963, 2963, 3010, 2972, 2888, 2846, 2846, 2726, + 3920, 4014, 2981, 3207, 3207, 2934, 3056, 2846, 3122, 3244, 2925, 2846, 2620, 2553, 2780, 2925, + 3516, 3197, 3010, 3103, 3019, 2888, 2925, 2925, 2925, 2925, 2888, 2888, 2888, 2888, 2888, 2753, + 5054, 5054, 2934, 3573, 3385, 3056, 3085, 2793, 3160, 3160, 2972, 2846, 2513, 2540, 2753, 2888, + 4428, 4149, 2700, 2753, 2972, 3010, 2925, 2846, 2981, 3019, 2925, 2925, 2925, 2925, 2888, 2726, + 3620, 3019, 2972, 3056, 3056, 2873, 2806, 3056, 3216, 3047, 2981, 3291, 3291, 2981, 3310, 2991, + 5227, 5014, 2540, 3338, 3526, 3385, 3197, 3094, 3376, 2981, 2700, 2647, 2687, 2793, 2846, 2673, + 5081, 5174, 4615, 4428, 2460, 2897, 3047, 3207, 3169, 2687, 2740, 2888, 2846, 2793, 2846, 2700, + 3122, 2888, 2963, 2925, 2925, 2925, 2925, 2963, 2963, 2963, 2963, 2925, 2925, 2963, 2963, 2963, + 4202, 3207, 2981, 3103, 3010, 2888, 2888, 2925, 2972, 2873, 2916, 3019, 2972, 3010, 3197, 2873, + 3760, 3760, 3244, 3103, 2981, 2888, 2925, 2888, 2972, 2934, 2793, 2793, 2846, 2888, 2888, 2660, + 3854, 4014, 3207, 3122, 3244, 2934, 3047, 2963, 2963, 3085, 2846, 2793, 2793, 2793, 2793, 2580, + 3845, 4080, 3357, 3516, 3094, 2740, 3010, 2934, 3122, 3085, 2846, 2846, 2647, 2647, 2846, 2806, + 5147, 4894, 3225, 3845, 3441, 3169, 2897, 3413, 3451, 2700, 2580, 2673, 2740, 2846, 2806, 2753, + 4109, 3789, 3291, 3160, 2925, 2888, 2888, 2925, 2793, 2740, 2793, 2740, 2793, 2846, 2888, 2806, + 5081, 5054, 3047, 3545, 3244, 3056, 3085, 2944, 3103, 2897, 2740, 2740, 2740, 2846, 2793, 2620, + 4309, 4309, 2860, 2527, 3207, 3376, 3376, 3075, 3075, 3376, 3056, 2846, 2647, 2580, 2726, 2753, + 3056, 2916, 2806, 2888, 2740, 2687, 2897, 3103, 3150, 3150, 3216, 3169, 3056, 3010, 2963, 2846, + 4375, 3882, 2925, 2888, 2846, 2888, 2846, 2846, 2888, 2888, 2888, 2846, 2888, 2925, 2888, 2846, + 2981, 2916, 2916, 2981, 2981, 3056, 3122, 3216, 3150, 3056, 3010, 2972, 2972, 2972, 2925, 2740, + 4229, 4149, 3310, 3347, 2925, 2963, 2888, 2981, 2981, 2846, 2793, 2740, 2846, 2846, 2846, 2793, + 4080, 4014, 3103, 3010, 2925, 2925, 2925, 2888, 2925, 2925, 2846, 2846, 2846, 2793, 2888, 2780, + 4615, 4575, 3169, 3441, 3207, 2981, 2897, 3038, 3122, 2740, 2687, 2687, 2687, 2740, 2793, 2700, + 4149, 4269, 3789, 3657, 2726, 2780, 2888, 2888, 3010, 2972, 2925, 2846, 2687, 2687, 2793, 2888, + 4215, 3554, 2753, 2846, 2846, 2888, 2888, 2888, 2925, 2925, 2888, 2925, 2925, 2925, 2963, 2888, + 5174, 4921, 2261, 3432, 3789, 3479, 3347, 2846, 3310, 3479, 3150, 2897, 2460, 2487, 2753, 2925, + 3451, 3685, 3122, 3197, 3357, 3047, 3207, 3207, 2981, 3216, 3085, 2925, 2925, 2687, 2540, 2434, + 2981, 3010, 2793, 2793, 2740, 2793, 2846, 2972, 3056, 3103, 3150, 3150, 3150, 3103, 3010, 3010, + 2944, 2873, 2687, 2726, 2780, 3010, 3432, 3545, 3357, 3244, 3056, 3010, 2963, 2925, 2888, 2846, + 3019, 2944, 2897, 3010, 3010, 2972, 3019, 3103, 3056, 3056, 3010, 2888, 2846, 2925, 2925, 2888, + 3920, 3967, 3010, 3197, 3357, 3216, 3291, 3291, 3479, 3704, 3441, 2726, 2181, 2460, 2580, 2607 +}; + +static const opus_uint8 silk_NLSF_CB1_iCDF_WB[ 64 ] = { + 225, 204, 201, 184, 183, 175, 158, 154, + 153, 135, 119, 115, 113, 110, 109, 99, + 98, 95, 79, 68, 52, 50, 48, 45, + 43, 32, 31, 27, 18, 10, 3, 0, + 255, 251, 235, 230, 212, 201, 196, 182, + 167, 166, 163, 151, 138, 124, 110, 104, + 90, 78, 76, 70, 69, 57, 45, 34, + 24, 21, 11, 6, 5, 4, 3, 0 +}; + +static const opus_uint8 silk_NLSF_CB2_SELECT_WB[ 256 ] = { + 0, 0, 0, 0, 0, 0, 0, 1, + 100, 102, 102, 68, 68, 36, 34, 96, + 164, 107, 158, 185, 180, 185, 139, 102, + 64, 66, 36, 34, 34, 0, 1, 32, + 208, 139, 141, 191, 152, 185, 155, 104, + 96, 171, 104, 166, 102, 102, 102, 132, + 1, 0, 0, 0, 0, 16, 16, 0, + 80, 109, 78, 107, 185, 139, 103, 101, + 208, 212, 141, 139, 173, 153, 123, 103, + 36, 0, 0, 0, 0, 0, 0, 1, + 48, 0, 0, 0, 0, 0, 0, 32, + 68, 135, 123, 119, 119, 103, 69, 98, + 68, 103, 120, 118, 118, 102, 71, 98, + 134, 136, 157, 184, 182, 153, 139, 134, + 208, 168, 248, 75, 189, 143, 121, 107, + 32, 49, 34, 34, 34, 0, 17, 2, + 210, 235, 139, 123, 185, 137, 105, 134, + 98, 135, 104, 182, 100, 183, 171, 134, + 100, 70, 68, 70, 66, 66, 34, 131, + 64, 166, 102, 68, 36, 2, 1, 0, + 134, 166, 102, 68, 34, 34, 66, 132, + 212, 246, 158, 139, 107, 107, 87, 102, + 100, 219, 125, 122, 137, 118, 103, 132, + 114, 135, 137, 105, 171, 106, 50, 34, + 164, 214, 141, 143, 185, 151, 121, 103, + 192, 34, 0, 0, 0, 0, 0, 1, + 208, 109, 74, 187, 134, 249, 159, 137, + 102, 110, 154, 118, 87, 101, 119, 101, + 0, 2, 0, 36, 36, 66, 68, 35, + 96, 164, 102, 100, 36, 0, 2, 33, + 167, 138, 174, 102, 100, 84, 2, 2, + 100, 107, 120, 119, 36, 197, 24, 0 +}; + +static const opus_uint8 silk_NLSF_CB2_iCDF_WB[ 72 ] = { + 255, 254, 253, 244, 12, 3, 2, 1, + 0, 255, 254, 252, 224, 38, 3, 2, + 1, 0, 255, 254, 251, 209, 57, 4, + 2, 1, 0, 255, 254, 244, 195, 69, + 4, 2, 1, 0, 255, 251, 232, 184, + 84, 7, 2, 1, 0, 255, 254, 240, + 186, 86, 14, 2, 1, 0, 255, 254, + 239, 178, 91, 30, 5, 1, 0, 255, + 248, 227, 177, 100, 19, 2, 1, 0 +}; + +static const opus_uint8 silk_NLSF_CB2_BITS_WB_Q5[ 72 ] = { + 255, 255, 255, 156, 4, 154, 255, 255, + 255, 255, 255, 227, 102, 15, 92, 255, + 255, 255, 255, 255, 213, 83, 24, 72, + 236, 255, 255, 255, 255, 150, 76, 33, + 63, 214, 255, 255, 255, 190, 121, 77, + 43, 55, 185, 255, 255, 255, 245, 137, + 71, 43, 59, 139, 255, 255, 255, 255, + 131, 66, 50, 66, 107, 194, 255, 255, + 166, 116, 76, 55, 53, 125, 255, 255 +}; + +static const opus_uint8 silk_NLSF_PRED_WB_Q8[ 30 ] = { + 175, 148, 160, 176, 178, 173, 174, 164, + 177, 174, 196, 182, 198, 192, 182, 68, + 62, 66, 60, 72, 117, 85, 90, 118, + 136, 151, 142, 160, 142, 155 +}; + +static const opus_int16 silk_NLSF_DELTA_MIN_WB_Q15[ 17 ] = { + 100, 3, 40, 3, 3, 3, 5, 14, + 14, 10, 11, 3, 8, 9, 7, 3, + 347 +}; + +const silk_NLSF_CB_struct silk_NLSF_CB_WB = +{ + 32, + 16, + SILK_FIX_CONST( 0.15, 16 ), + SILK_FIX_CONST( 1.0 / 0.15, 6 ), + silk_NLSF_CB1_WB_Q8, + silk_NLSF_CB1_WB_Wght_Q9, + silk_NLSF_CB1_iCDF_WB, + silk_NLSF_PRED_WB_Q8, + silk_NLSF_CB2_SELECT_WB, + silk_NLSF_CB2_iCDF_WB, + silk_NLSF_CB2_BITS_WB_Q5, + silk_NLSF_DELTA_MIN_WB_Q15, +}; + diff --git a/port/third_party/opus/silk/tables_gain.c b/port/third_party/opus/silk/tables_gain.c new file mode 100644 index 00000000..37e41d89 --- /dev/null +++ b/port/third_party/opus/silk/tables_gain.c @@ -0,0 +1,63 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "tables.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +const opus_uint8 silk_gain_iCDF[ 3 ][ N_LEVELS_QGAIN / 8 ] = +{ +{ + 224, 112, 44, 15, 3, 2, 1, 0 +}, +{ + 254, 237, 192, 132, 70, 23, 4, 0 +}, +{ + 255, 252, 226, 155, 61, 11, 2, 0 +} +}; + +const opus_uint8 silk_delta_gain_iCDF[ MAX_DELTA_GAIN_QUANT - MIN_DELTA_GAIN_QUANT + 1 ] = { + 250, 245, 234, 203, 71, 50, 42, 38, + 35, 33, 31, 29, 28, 27, 26, 25, + 24, 23, 22, 21, 20, 19, 18, 17, + 16, 15, 14, 13, 12, 11, 10, 9, + 8, 7, 6, 5, 4, 3, 2, 1, + 0 +}; + +#ifdef __cplusplus +} +#endif diff --git a/port/third_party/opus/silk/tables_other.c b/port/third_party/opus/silk/tables_other.c new file mode 100644 index 00000000..e34d9077 --- /dev/null +++ b/port/third_party/opus/silk/tables_other.c @@ -0,0 +1,124 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "structs.h" +#include "define.h" +#include "tables.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Tables for stereo predictor coding */ +const opus_int16 silk_stereo_pred_quant_Q13[ STEREO_QUANT_TAB_SIZE ] = { + -13732, -10050, -8266, -7526, -6500, -5000, -2950, -820, + 820, 2950, 5000, 6500, 7526, 8266, 10050, 13732 +}; +const opus_uint8 silk_stereo_pred_joint_iCDF[ 25 ] = { + 249, 247, 246, 245, 244, + 234, 210, 202, 201, 200, + 197, 174, 82, 59, 56, + 55, 54, 46, 22, 12, + 11, 10, 9, 7, 0 +}; +const opus_uint8 silk_stereo_only_code_mid_iCDF[ 2 ] = { 64, 0 }; + +/* Tables for LBRR flags */ +static const opus_uint8 silk_LBRR_flags_2_iCDF[ 3 ] = { 203, 150, 0 }; +static const opus_uint8 silk_LBRR_flags_3_iCDF[ 7 ] = { 215, 195, 166, 125, 110, 82, 0 }; +const opus_uint8 * const silk_LBRR_flags_iCDF_ptr[ 2 ] = { + silk_LBRR_flags_2_iCDF, + silk_LBRR_flags_3_iCDF +}; + +/* Table for LSB coding */ +const opus_uint8 silk_lsb_iCDF[ 2 ] = { 120, 0 }; + +/* Tables for LTPScale */ +const opus_uint8 silk_LTPscale_iCDF[ 3 ] = { 128, 64, 0 }; + +/* Tables for signal type and offset coding */ +const opus_uint8 silk_type_offset_VAD_iCDF[ 4 ] = { + 232, 158, 10, 0 +}; +const opus_uint8 silk_type_offset_no_VAD_iCDF[ 2 ] = { + 230, 0 +}; + +/* Tables for NLSF interpolation factor */ +const opus_uint8 silk_NLSF_interpolation_factor_iCDF[ 5 ] = { 243, 221, 192, 181, 0 }; + +/* Quantization offsets */ +const opus_int16 silk_Quantization_Offsets_Q10[ 2 ][ 2 ] = { + { OFFSET_UVL_Q10, OFFSET_UVH_Q10 }, { OFFSET_VL_Q10, OFFSET_VH_Q10 } +}; + +/* Table for LTPScale */ +const opus_int16 silk_LTPScales_table_Q14[ 3 ] = { 15565, 12288, 8192 }; + +/* Uniform entropy tables */ +const opus_uint8 silk_uniform3_iCDF[ 3 ] = { 171, 85, 0 }; +const opus_uint8 silk_uniform4_iCDF[ 4 ] = { 192, 128, 64, 0 }; +const opus_uint8 silk_uniform5_iCDF[ 5 ] = { 205, 154, 102, 51, 0 }; +const opus_uint8 silk_uniform6_iCDF[ 6 ] = { 213, 171, 128, 85, 43, 0 }; +const opus_uint8 silk_uniform8_iCDF[ 8 ] = { 224, 192, 160, 128, 96, 64, 32, 0 }; + +const opus_uint8 silk_NLSF_EXT_iCDF[ 7 ] = { 100, 40, 16, 7, 3, 1, 0 }; + +/* Elliptic/Cauer filters designed with 0.1 dB passband ripple, + 80 dB minimum stopband attenuation, and + [0.95 : 0.15 : 0.35] normalized cut off frequencies. */ + +/* Interpolation points for filter coefficients used in the bandwidth transition smoother */ +const opus_int32 silk_Transition_LP_B_Q28[ TRANSITION_INT_NUM ][ TRANSITION_NB ] = +{ +{ 250767114, 501534038, 250767114 }, +{ 209867381, 419732057, 209867381 }, +{ 170987846, 341967853, 170987846 }, +{ 131531482, 263046905, 131531482 }, +{ 89306658, 178584282, 89306658 } +}; + +/* Interpolation points for filter coefficients used in the bandwidth transition smoother */ +const opus_int32 silk_Transition_LP_A_Q28[ TRANSITION_INT_NUM ][ TRANSITION_NA ] = +{ +{ 506393414, 239854379 }, +{ 411067935, 169683996 }, +{ 306733530, 116694253 }, +{ 185807084, 77959395 }, +{ 35497197, 57401098 } +}; + +#ifdef __cplusplus +} +#endif + diff --git a/port/third_party/opus/silk/tables_pitch_lag.c b/port/third_party/opus/silk/tables_pitch_lag.c new file mode 100644 index 00000000..e80cc59a --- /dev/null +++ b/port/third_party/opus/silk/tables_pitch_lag.c @@ -0,0 +1,69 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "tables.h" + +const opus_uint8 silk_pitch_lag_iCDF[ 2 * ( PITCH_EST_MAX_LAG_MS - PITCH_EST_MIN_LAG_MS ) ] = { + 253, 250, 244, 233, 212, 182, 150, 131, + 120, 110, 98, 85, 72, 60, 49, 40, + 32, 25, 19, 15, 13, 11, 9, 8, + 7, 6, 5, 4, 3, 2, 1, 0 +}; + +const opus_uint8 silk_pitch_delta_iCDF[21] = { + 210, 208, 206, 203, 199, 193, 183, 168, + 142, 104, 74, 52, 37, 27, 20, 14, + 10, 6, 4, 2, 0 +}; + +const opus_uint8 silk_pitch_contour_iCDF[34] = { + 223, 201, 183, 167, 152, 138, 124, 111, + 98, 88, 79, 70, 62, 56, 50, 44, + 39, 35, 31, 27, 24, 21, 18, 16, + 14, 12, 10, 8, 6, 4, 3, 2, + 1, 0 +}; + +const opus_uint8 silk_pitch_contour_NB_iCDF[11] = { + 188, 176, 155, 138, 119, 97, 67, 43, + 26, 10, 0 +}; + +const opus_uint8 silk_pitch_contour_10_ms_iCDF[12] = { + 165, 119, 80, 61, 47, 35, 27, 20, + 14, 9, 4, 0 +}; + +const opus_uint8 silk_pitch_contour_10_ms_NB_iCDF[3] = { + 113, 63, 0 +}; + + diff --git a/port/third_party/opus/silk/tables_pulses_per_block.c b/port/third_party/opus/silk/tables_pulses_per_block.c new file mode 100644 index 00000000..c7c01c88 --- /dev/null +++ b/port/third_party/opus/silk/tables_pulses_per_block.c @@ -0,0 +1,264 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "tables.h" + +const opus_uint8 silk_max_pulses_table[ 4 ] = { + 8, 10, 12, 16 +}; + +const opus_uint8 silk_pulses_per_block_iCDF[ 10 ][ 18 ] = { +{ + 125, 51, 26, 18, 15, 12, 11, 10, + 9, 8, 7, 6, 5, 4, 3, 2, + 1, 0 +}, +{ + 198, 105, 45, 22, 15, 12, 11, 10, + 9, 8, 7, 6, 5, 4, 3, 2, + 1, 0 +}, +{ + 213, 162, 116, 83, 59, 43, 32, 24, + 18, 15, 12, 9, 7, 6, 5, 3, + 2, 0 +}, +{ + 239, 187, 116, 59, 28, 16, 11, 10, + 9, 8, 7, 6, 5, 4, 3, 2, + 1, 0 +}, +{ + 250, 229, 188, 135, 86, 51, 30, 19, + 13, 10, 8, 6, 5, 4, 3, 2, + 1, 0 +}, +{ + 249, 235, 213, 185, 156, 128, 103, 83, + 66, 53, 42, 33, 26, 21, 17, 13, + 10, 0 +}, +{ + 254, 249, 235, 206, 164, 118, 77, 46, + 27, 16, 10, 7, 5, 4, 3, 2, + 1, 0 +}, +{ + 255, 253, 249, 239, 220, 191, 156, 119, + 85, 57, 37, 23, 15, 10, 6, 4, + 2, 0 +}, +{ + 255, 253, 251, 246, 237, 223, 203, 179, + 152, 124, 98, 75, 55, 40, 29, 21, + 15, 0 +}, +{ + 255, 254, 253, 247, 220, 162, 106, 67, + 42, 28, 18, 12, 9, 6, 4, 3, + 2, 0 +} +}; + +const opus_uint8 silk_pulses_per_block_BITS_Q5[ 9 ][ 18 ] = { +{ + 31, 57, 107, 160, 205, 205, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255 +}, +{ + 69, 47, 67, 111, 166, 205, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255 +}, +{ + 82, 74, 79, 95, 109, 128, 145, 160, + 173, 205, 205, 205, 224, 255, 255, 224, + 255, 224 +}, +{ + 125, 74, 59, 69, 97, 141, 182, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255 +}, +{ + 173, 115, 85, 73, 76, 92, 115, 145, + 173, 205, 224, 224, 255, 255, 255, 255, + 255, 255 +}, +{ + 166, 134, 113, 102, 101, 102, 107, 118, + 125, 138, 145, 155, 166, 182, 192, 192, + 205, 150 +}, +{ + 224, 182, 134, 101, 83, 79, 85, 97, + 120, 145, 173, 205, 224, 255, 255, 255, + 255, 255 +}, +{ + 255, 224, 192, 150, 120, 101, 92, 89, + 93, 102, 118, 134, 160, 182, 192, 224, + 224, 224 +}, +{ + 255, 224, 224, 182, 155, 134, 118, 109, + 104, 102, 106, 111, 118, 131, 145, 160, + 173, 131 +} +}; + +const opus_uint8 silk_rate_levels_iCDF[ 2 ][ 9 ] = +{ +{ + 241, 190, 178, 132, 87, 74, 41, 14, + 0 +}, +{ + 223, 193, 157, 140, 106, 57, 39, 18, + 0 +} +}; + +const opus_uint8 silk_rate_levels_BITS_Q5[ 2 ][ 9 ] = +{ +{ + 131, 74, 141, 79, 80, 138, 95, 104, + 134 +}, +{ + 95, 99, 91, 125, 93, 76, 123, 115, + 123 +} +}; + +const opus_uint8 silk_shell_code_table0[ 152 ] = { + 128, 0, 214, 42, 0, 235, 128, 21, + 0, 244, 184, 72, 11, 0, 248, 214, + 128, 42, 7, 0, 248, 225, 170, 80, + 25, 5, 0, 251, 236, 198, 126, 54, + 18, 3, 0, 250, 238, 211, 159, 82, + 35, 15, 5, 0, 250, 231, 203, 168, + 128, 88, 53, 25, 6, 0, 252, 238, + 216, 185, 148, 108, 71, 40, 18, 4, + 0, 253, 243, 225, 199, 166, 128, 90, + 57, 31, 13, 3, 0, 254, 246, 233, + 212, 183, 147, 109, 73, 44, 23, 10, + 2, 0, 255, 250, 240, 223, 198, 166, + 128, 90, 58, 33, 16, 6, 1, 0, + 255, 251, 244, 231, 210, 181, 146, 110, + 75, 46, 25, 12, 5, 1, 0, 255, + 253, 248, 238, 221, 196, 164, 128, 92, + 60, 35, 18, 8, 3, 1, 0, 255, + 253, 249, 242, 229, 208, 180, 146, 110, + 76, 48, 27, 14, 7, 3, 1, 0 +}; + +const opus_uint8 silk_shell_code_table1[ 152 ] = { + 129, 0, 207, 50, 0, 236, 129, 20, + 0, 245, 185, 72, 10, 0, 249, 213, + 129, 42, 6, 0, 250, 226, 169, 87, + 27, 4, 0, 251, 233, 194, 130, 62, + 20, 4, 0, 250, 236, 207, 160, 99, + 47, 17, 3, 0, 255, 240, 217, 182, + 131, 81, 41, 11, 1, 0, 255, 254, + 233, 201, 159, 107, 61, 20, 2, 1, + 0, 255, 249, 233, 206, 170, 128, 86, + 50, 23, 7, 1, 0, 255, 250, 238, + 217, 186, 148, 108, 70, 39, 18, 6, + 1, 0, 255, 252, 243, 226, 200, 166, + 128, 90, 56, 30, 13, 4, 1, 0, + 255, 252, 245, 231, 209, 180, 146, 110, + 76, 47, 25, 11, 4, 1, 0, 255, + 253, 248, 237, 219, 194, 163, 128, 93, + 62, 37, 19, 8, 3, 1, 0, 255, + 254, 250, 241, 226, 205, 177, 145, 111, + 79, 51, 30, 15, 6, 2, 1, 0 +}; + +const opus_uint8 silk_shell_code_table2[ 152 ] = { + 129, 0, 203, 54, 0, 234, 129, 23, + 0, 245, 184, 73, 10, 0, 250, 215, + 129, 41, 5, 0, 252, 232, 173, 86, + 24, 3, 0, 253, 240, 200, 129, 56, + 15, 2, 0, 253, 244, 217, 164, 94, + 38, 10, 1, 0, 253, 245, 226, 189, + 132, 71, 27, 7, 1, 0, 253, 246, + 231, 203, 159, 105, 56, 23, 6, 1, + 0, 255, 248, 235, 213, 179, 133, 85, + 47, 19, 5, 1, 0, 255, 254, 243, + 221, 194, 159, 117, 70, 37, 12, 2, + 1, 0, 255, 254, 248, 234, 208, 171, + 128, 85, 48, 22, 8, 2, 1, 0, + 255, 254, 250, 240, 220, 189, 149, 107, + 67, 36, 16, 6, 2, 1, 0, 255, + 254, 251, 243, 227, 201, 166, 128, 90, + 55, 29, 13, 5, 2, 1, 0, 255, + 254, 252, 246, 234, 213, 183, 147, 109, + 73, 43, 22, 10, 4, 2, 1, 0 +}; + +const opus_uint8 silk_shell_code_table3[ 152 ] = { + 130, 0, 200, 58, 0, 231, 130, 26, + 0, 244, 184, 76, 12, 0, 249, 214, + 130, 43, 6, 0, 252, 232, 173, 87, + 24, 3, 0, 253, 241, 203, 131, 56, + 14, 2, 0, 254, 246, 221, 167, 94, + 35, 8, 1, 0, 254, 249, 232, 193, + 130, 65, 23, 5, 1, 0, 255, 251, + 239, 211, 162, 99, 45, 15, 4, 1, + 0, 255, 251, 243, 223, 186, 131, 74, + 33, 11, 3, 1, 0, 255, 252, 245, + 230, 202, 158, 105, 57, 24, 8, 2, + 1, 0, 255, 253, 247, 235, 214, 179, + 132, 84, 44, 19, 7, 2, 1, 0, + 255, 254, 250, 240, 223, 196, 159, 112, + 69, 36, 15, 6, 2, 1, 0, 255, + 254, 253, 245, 231, 209, 176, 136, 93, + 55, 27, 11, 3, 2, 1, 0, 255, + 254, 253, 252, 239, 221, 194, 158, 117, + 76, 42, 18, 4, 3, 2, 1, 0 +}; + +const opus_uint8 silk_shell_code_table_offsets[ 17 ] = { + 0, 0, 2, 5, 9, 14, 20, 27, + 35, 44, 54, 65, 77, 90, 104, 119, + 135 +}; + +const opus_uint8 silk_sign_iCDF[ 42 ] = { + 254, 49, 67, 77, 82, 93, 99, + 198, 11, 18, 24, 31, 36, 45, + 255, 46, 66, 78, 87, 94, 104, + 208, 14, 21, 32, 42, 51, 66, + 255, 94, 104, 109, 112, 115, 118, + 248, 53, 69, 80, 88, 95, 102 +}; diff --git a/port/third_party/opus/silk/tuning_parameters.h b/port/third_party/opus/silk/tuning_parameters.h new file mode 100644 index 00000000..d70275fd --- /dev/null +++ b/port/third_party/opus/silk/tuning_parameters.h @@ -0,0 +1,155 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_TUNING_PARAMETERS_H +#define SILK_TUNING_PARAMETERS_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Decay time for bitreservoir */ +#define BITRESERVOIR_DECAY_TIME_MS 500 + +/*******************/ +/* Pitch estimator */ +/*******************/ + +/* Level of noise floor for whitening filter LPC analysis in pitch analysis */ +#define FIND_PITCH_WHITE_NOISE_FRACTION 1e-3f + +/* Bandwidth expansion for whitening filter in pitch analysis */ +#define FIND_PITCH_BANDWIDTH_EXPANSION 0.99f + +/*********************/ +/* Linear prediction */ +/*********************/ + +/* LPC analysis regularization */ +#define FIND_LPC_COND_FAC 1e-5f + +/* Max cumulative LTP gain */ +#define MAX_SUM_LOG_GAIN_DB 250.0f + +/* LTP analysis defines */ +#define LTP_CORR_INV_MAX 0.03f + +/***********************/ +/* High pass filtering */ +/***********************/ + +/* Smoothing parameters for low end of pitch frequency range estimation */ +#define VARIABLE_HP_SMTH_COEF1 0.1f +#define VARIABLE_HP_SMTH_COEF2 0.015f +#define VARIABLE_HP_MAX_DELTA_FREQ 0.4f + +/* Min and max cut-off frequency values (-3 dB points) */ +#define VARIABLE_HP_MIN_CUTOFF_HZ 60 +#define VARIABLE_HP_MAX_CUTOFF_HZ 100 + +/***********/ +/* Various */ +/***********/ + +/* VAD threshold */ +#define SPEECH_ACTIVITY_DTX_THRES 0.05f + +/* Speech Activity LBRR enable threshold */ +#define LBRR_SPEECH_ACTIVITY_THRES 0.3f + +/*************************/ +/* Perceptual parameters */ +/*************************/ + +/* reduction in coding SNR during low speech activity */ +#define BG_SNR_DECR_dB 2.0f + +/* factor for reducing quantization noise during voiced speech */ +#define HARM_SNR_INCR_dB 2.0f + +/* factor for reducing quantization noise for unvoiced sparse signals */ +#define SPARSE_SNR_INCR_dB 2.0f + +/* threshold for sparseness measure above which to use lower quantization offset during unvoiced */ +#define ENERGY_VARIATION_THRESHOLD_QNT_OFFSET 0.6f + +/* warping control */ +#define WARPING_MULTIPLIER 0.015f + +/* fraction added to first autocorrelation value */ +#define SHAPE_WHITE_NOISE_FRACTION 3e-5f + +/* noise shaping filter chirp factor */ +#define BANDWIDTH_EXPANSION 0.94f + +/* harmonic noise shaping */ +#define HARMONIC_SHAPING 0.3f + +/* extra harmonic noise shaping for high bitrates or noisy input */ +#define HIGH_RATE_OR_LOW_QUALITY_HARMONIC_SHAPING 0.2f + +/* parameter for shaping noise towards higher frequencies */ +#define HP_NOISE_COEF 0.25f + +/* parameter for shaping noise even more towards higher frequencies during voiced speech */ +#define HARM_HP_NOISE_COEF 0.35f + +/* parameter for applying a high-pass tilt to the input signal */ +#define INPUT_TILT 0.05f + +/* parameter for extra high-pass tilt to the input signal at high rates */ +#define HIGH_RATE_INPUT_TILT 0.1f + +/* parameter for reducing noise at the very low frequencies */ +#define LOW_FREQ_SHAPING 4.0f + +/* less reduction of noise at the very low frequencies for signals with low SNR at low frequencies */ +#define LOW_QUALITY_LOW_FREQ_SHAPING_DECR 0.5f + +/* subframe smoothing coefficient for HarmBoost, HarmShapeGain, Tilt (lower -> more smoothing) */ +#define SUBFR_SMTH_COEF 0.4f + +/* parameters defining the R/D tradeoff in the residual quantizer */ +#define LAMBDA_OFFSET 1.2f +#define LAMBDA_SPEECH_ACT -0.2f +#define LAMBDA_DELAYED_DECISIONS -0.05f +#define LAMBDA_INPUT_QUALITY -0.1f +#define LAMBDA_CODING_QUALITY -0.2f +#define LAMBDA_QUANT_OFFSET 0.8f + +/* Compensation in bitrate calculations for 10 ms modes */ +#define REDUCE_BITRATE_10_MS_BPS 2200 + +/* Maximum time before allowing a bandwidth transition */ +#define MAX_BANDWIDTH_SWITCH_DELAY_MS 5000 + +#ifdef __cplusplus +} +#endif + +#endif /* SILK_TUNING_PARAMETERS_H */ diff --git a/port/third_party/opus/silk/typedef.h b/port/third_party/opus/silk/typedef.h new file mode 100644 index 00000000..793d2c0c --- /dev/null +++ b/port/third_party/opus/silk/typedef.h @@ -0,0 +1,81 @@ +/*********************************************************************** +Copyright (c) 2006-2011, Skype Limited. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +***********************************************************************/ + +#ifndef SILK_TYPEDEF_H +#define SILK_TYPEDEF_H + +#include "opus_types.h" +#include "opus_defines.h" + +#ifndef FIXED_POINT +# include +# define silk_float float +# define silk_float_MAX FLT_MAX +#endif + +#define silk_int64_MAX ((opus_int64)0x7FFFFFFFFFFFFFFFLL) /* 2^63 - 1 */ +#define silk_int64_MIN ((opus_int64)0x8000000000000000LL) /* -2^63 */ +#define silk_int32_MAX 0x7FFFFFFF /* 2^31 - 1 = 2147483647 */ +#define silk_int32_MIN ((opus_int32)0x80000000) /* -2^31 = -2147483648 */ +#define silk_int16_MAX 0x7FFF /* 2^15 - 1 = 32767 */ +#define silk_int16_MIN ((opus_int16)0x8000) /* -2^15 = -32768 */ +#define silk_int8_MAX 0x7F /* 2^7 - 1 = 127 */ +#define silk_int8_MIN ((opus_int8)0x80) /* -2^7 = -128 */ +#define silk_uint8_MAX 0xFF /* 2^8 - 1 = 255 */ + +#define silk_TRUE 1 +#define silk_FALSE 0 + +/* assertions */ +#if (defined _WIN32 && !defined _WINCE && !defined(__GNUC__) && !defined(NO_ASSERTS)) +# ifndef silk_assert +# include /* ASSERTE() */ +# define silk_assert(COND) _ASSERTE(COND) +# endif +#else +# ifdef ENABLE_ASSERTIONS +# include +# include +#define silk_fatal(str) _silk_fatal(str, __FILE__, __LINE__); +#ifdef __GNUC__ +__attribute__((noreturn)) +#endif +static OPUS_INLINE void _silk_fatal(const char *str, const char *file, int line) +{ + fprintf (stderr, "Fatal (internal) error in %s, line %d: %s\n", file, line, str); +#if defined(_MSC_VER) + _set_abort_behavior( 0, _WRITE_ABORT_MSG); +#endif + abort(); +} +# define silk_assert(COND) {if (!(COND)) {silk_fatal("assertion failed: " #COND);}} +# else +# define silk_assert(COND) +# endif +#endif + +#endif /* SILK_TYPEDEF_H */ diff --git a/port/third_party/opus/sources.txt b/port/third_party/opus/sources.txt new file mode 100644 index 00000000..e7115dcb --- /dev/null +++ b/port/third_party/opus/sources.txt @@ -0,0 +1,131 @@ +src/opus.c +src/opus_decoder.c +src/opus_encoder.c +src/extensions.c +src/repacketizer.c +src/analysis.c +src/mlp.c +src/mlp_data.c +celt/bands.c +celt/celt.c +celt/celt_encoder.c +celt/celt_decoder.c +celt/cwrs.c +celt/entcode.c +celt/entdec.c +celt/entenc.c +celt/kiss_fft.c +celt/laplace.c +celt/mathops.c +celt/mdct.c +celt/modes.c +celt/pitch.c +celt/celt_lpc.c +celt/quant_bands.c +celt/rate.c +celt/vq.c +silk/CNG.c +silk/code_signs.c +silk/init_decoder.c +silk/decode_core.c +silk/decode_frame.c +silk/decode_parameters.c +silk/decode_indices.c +silk/decode_pulses.c +silk/decoder_set_fs.c +silk/dec_API.c +silk/enc_API.c +silk/encode_indices.c +silk/encode_pulses.c +silk/gain_quant.c +silk/interpolate.c +silk/LP_variable_cutoff.c +silk/NLSF_decode.c +silk/NSQ.c +silk/NSQ_del_dec.c +silk/PLC.c +silk/shell_coder.c +silk/tables_gain.c +silk/tables_LTP.c +silk/tables_NLSF_CB_NB_MB.c +silk/tables_NLSF_CB_WB.c +silk/tables_other.c +silk/tables_pitch_lag.c +silk/tables_pulses_per_block.c +silk/VAD.c +silk/control_audio_bandwidth.c +silk/quant_LTP_gains.c +silk/VQ_WMat_EC.c +silk/HP_variable_cutoff.c +silk/NLSF_encode.c +silk/NLSF_VQ.c +silk/NLSF_unpack.c +silk/NLSF_del_dec_quant.c +silk/process_NLSFs.c +silk/stereo_LR_to_MS.c +silk/stereo_MS_to_LR.c +silk/check_control_input.c +silk/control_SNR.c +silk/init_encoder.c +silk/control_codec.c +silk/A2NLSF.c +silk/ana_filt_bank_1.c +silk/biquad_alt.c +silk/bwexpander_32.c +silk/bwexpander.c +silk/debug.c +silk/decode_pitch.c +silk/inner_prod_aligned.c +silk/lin2log.c +silk/log2lin.c +silk/LPC_analysis_filter.c +silk/LPC_inv_pred_gain.c +silk/table_LSF_cos.c +silk/NLSF2A.c +silk/NLSF_stabilize.c +silk/NLSF_VQ_weights_laroia.c +silk/pitch_est_tables.c +silk/resampler.c +silk/resampler_down2_3.c +silk/resampler_down2.c +silk/resampler_private_AR2.c +silk/resampler_private_down_FIR.c +silk/resampler_private_IIR_FIR.c +silk/resampler_private_up2_HQ.c +silk/resampler_rom.c +silk/sigm_Q15.c +silk/sort.c +silk/sum_sqr_shift.c +silk/stereo_decode_pred.c +silk/stereo_encode_pred.c +silk/stereo_find_predictor.c +silk/stereo_quant_pred.c +silk/LPC_fit.c +silk/float/apply_sine_window_FLP.c +silk/float/corrMatrix_FLP.c +silk/float/encode_frame_FLP.c +silk/float/find_LPC_FLP.c +silk/float/find_LTP_FLP.c +silk/float/find_pitch_lags_FLP.c +silk/float/find_pred_coefs_FLP.c +silk/float/LPC_analysis_filter_FLP.c +silk/float/LTP_analysis_filter_FLP.c +silk/float/LTP_scale_ctrl_FLP.c +silk/float/noise_shape_analysis_FLP.c +silk/float/process_gains_FLP.c +silk/float/regularize_correlations_FLP.c +silk/float/residual_energy_FLP.c +silk/float/warped_autocorrelation_FLP.c +silk/float/wrappers_FLP.c +silk/float/autocorrelation_FLP.c +silk/float/burg_modified_FLP.c +silk/float/bwexpander_FLP.c +silk/float/energy_FLP.c +silk/float/inner_product_FLP.c +silk/float/k2a_FLP.c +silk/float/LPC_inv_pred_gain_FLP.c +silk/float/pitch_analysis_core_FLP.c +silk/float/scale_copy_vector_FLP.c +silk/float/scale_vector_FLP.c +silk/float/schur_FLP.c +silk/float/sort_FLP.c diff --git a/port/third_party/opus/src/analysis.c b/port/third_party/opus/src/analysis.c new file mode 100644 index 00000000..1f580138 --- /dev/null +++ b/port/third_party/opus/src/analysis.c @@ -0,0 +1,983 @@ +/* Copyright (c) 2011 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#define ANALYSIS_C + +#ifdef MLP_TRAINING +#include +#endif + +#include "mathops.h" +#include "kiss_fft.h" +#include "celt.h" +#include "modes.h" +#include "arch.h" +#include "quant_bands.h" +#include "analysis.h" +#include "mlp.h" +#include "stack_alloc.h" +#include "float_cast.h" + +#ifndef M_PI +#define M_PI 3.141592653 +#endif + +#ifndef DISABLE_FLOAT_API + +#define TRANSITION_PENALTY 10 + +static const float dct_table[128] = { + 0.250000f, 0.250000f, 0.250000f, 0.250000f, 0.250000f, 0.250000f, 0.250000f, 0.250000f, + 0.250000f, 0.250000f, 0.250000f, 0.250000f, 0.250000f, 0.250000f, 0.250000f, 0.250000f, + 0.351851f, 0.338330f, 0.311806f, 0.273300f, 0.224292f, 0.166664f, 0.102631f, 0.034654f, + -0.034654f,-0.102631f,-0.166664f,-0.224292f,-0.273300f,-0.311806f,-0.338330f,-0.351851f, + 0.346760f, 0.293969f, 0.196424f, 0.068975f,-0.068975f,-0.196424f,-0.293969f,-0.346760f, + -0.346760f,-0.293969f,-0.196424f,-0.068975f, 0.068975f, 0.196424f, 0.293969f, 0.346760f, + 0.338330f, 0.224292f, 0.034654f,-0.166664f,-0.311806f,-0.351851f,-0.273300f,-0.102631f, + 0.102631f, 0.273300f, 0.351851f, 0.311806f, 0.166664f,-0.034654f,-0.224292f,-0.338330f, + 0.326641f, 0.135299f,-0.135299f,-0.326641f,-0.326641f,-0.135299f, 0.135299f, 0.326641f, + 0.326641f, 0.135299f,-0.135299f,-0.326641f,-0.326641f,-0.135299f, 0.135299f, 0.326641f, + 0.311806f, 0.034654f,-0.273300f,-0.338330f,-0.102631f, 0.224292f, 0.351851f, 0.166664f, + -0.166664f,-0.351851f,-0.224292f, 0.102631f, 0.338330f, 0.273300f,-0.034654f,-0.311806f, + 0.293969f,-0.068975f,-0.346760f,-0.196424f, 0.196424f, 0.346760f, 0.068975f,-0.293969f, + -0.293969f, 0.068975f, 0.346760f, 0.196424f,-0.196424f,-0.346760f,-0.068975f, 0.293969f, + 0.273300f,-0.166664f,-0.338330f, 0.034654f, 0.351851f, 0.102631f,-0.311806f,-0.224292f, + 0.224292f, 0.311806f,-0.102631f,-0.351851f,-0.034654f, 0.338330f, 0.166664f,-0.273300f, +}; + +static const float analysis_window[240] = { + 0.000043f, 0.000171f, 0.000385f, 0.000685f, 0.001071f, 0.001541f, 0.002098f, 0.002739f, + 0.003466f, 0.004278f, 0.005174f, 0.006156f, 0.007222f, 0.008373f, 0.009607f, 0.010926f, + 0.012329f, 0.013815f, 0.015385f, 0.017037f, 0.018772f, 0.020590f, 0.022490f, 0.024472f, + 0.026535f, 0.028679f, 0.030904f, 0.033210f, 0.035595f, 0.038060f, 0.040604f, 0.043227f, + 0.045928f, 0.048707f, 0.051564f, 0.054497f, 0.057506f, 0.060591f, 0.063752f, 0.066987f, + 0.070297f, 0.073680f, 0.077136f, 0.080665f, 0.084265f, 0.087937f, 0.091679f, 0.095492f, + 0.099373f, 0.103323f, 0.107342f, 0.111427f, 0.115579f, 0.119797f, 0.124080f, 0.128428f, + 0.132839f, 0.137313f, 0.141849f, 0.146447f, 0.151105f, 0.155823f, 0.160600f, 0.165435f, + 0.170327f, 0.175276f, 0.180280f, 0.185340f, 0.190453f, 0.195619f, 0.200838f, 0.206107f, + 0.211427f, 0.216797f, 0.222215f, 0.227680f, 0.233193f, 0.238751f, 0.244353f, 0.250000f, + 0.255689f, 0.261421f, 0.267193f, 0.273005f, 0.278856f, 0.284744f, 0.290670f, 0.296632f, + 0.302628f, 0.308658f, 0.314721f, 0.320816f, 0.326941f, 0.333097f, 0.339280f, 0.345492f, + 0.351729f, 0.357992f, 0.364280f, 0.370590f, 0.376923f, 0.383277f, 0.389651f, 0.396044f, + 0.402455f, 0.408882f, 0.415325f, 0.421783f, 0.428254f, 0.434737f, 0.441231f, 0.447736f, + 0.454249f, 0.460770f, 0.467298f, 0.473832f, 0.480370f, 0.486912f, 0.493455f, 0.500000f, + 0.506545f, 0.513088f, 0.519630f, 0.526168f, 0.532702f, 0.539230f, 0.545751f, 0.552264f, + 0.558769f, 0.565263f, 0.571746f, 0.578217f, 0.584675f, 0.591118f, 0.597545f, 0.603956f, + 0.610349f, 0.616723f, 0.623077f, 0.629410f, 0.635720f, 0.642008f, 0.648271f, 0.654508f, + 0.660720f, 0.666903f, 0.673059f, 0.679184f, 0.685279f, 0.691342f, 0.697372f, 0.703368f, + 0.709330f, 0.715256f, 0.721144f, 0.726995f, 0.732807f, 0.738579f, 0.744311f, 0.750000f, + 0.755647f, 0.761249f, 0.766807f, 0.772320f, 0.777785f, 0.783203f, 0.788573f, 0.793893f, + 0.799162f, 0.804381f, 0.809547f, 0.814660f, 0.819720f, 0.824724f, 0.829673f, 0.834565f, + 0.839400f, 0.844177f, 0.848895f, 0.853553f, 0.858151f, 0.862687f, 0.867161f, 0.871572f, + 0.875920f, 0.880203f, 0.884421f, 0.888573f, 0.892658f, 0.896677f, 0.900627f, 0.904508f, + 0.908321f, 0.912063f, 0.915735f, 0.919335f, 0.922864f, 0.926320f, 0.929703f, 0.933013f, + 0.936248f, 0.939409f, 0.942494f, 0.945503f, 0.948436f, 0.951293f, 0.954072f, 0.956773f, + 0.959396f, 0.961940f, 0.964405f, 0.966790f, 0.969096f, 0.971321f, 0.973465f, 0.975528f, + 0.977510f, 0.979410f, 0.981228f, 0.982963f, 0.984615f, 0.986185f, 0.987671f, 0.989074f, + 0.990393f, 0.991627f, 0.992778f, 0.993844f, 0.994826f, 0.995722f, 0.996534f, 0.997261f, + 0.997902f, 0.998459f, 0.998929f, 0.999315f, 0.999615f, 0.999829f, 0.999957f, 1.000000f, +}; + +static const int tbands[NB_TBANDS+1] = { + 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 136, 160, 192, 240 +}; + +#define NB_TONAL_SKIP_BANDS 9 + +static opus_val32 silk_resampler_down2_hp( + opus_val32 *S, /* I/O State vector [ 2 ] */ + opus_val32 *out, /* O Output signal [ floor(len/2) ] */ + const opus_val32 *in, /* I Input signal [ len ] */ + int inLen /* I Number of input samples */ +) +{ + int k, len2 = inLen/2; + opus_val32 in32, out32, out32_hp, Y, X; + opus_val64 hp_ener = 0; + /* Internal variables and state are in Q10 format */ + for( k = 0; k < len2; k++ ) { + /* Convert to Q10 */ + in32 = in[ 2 * k ]; + + /* All-pass section for even input sample */ + Y = SUB32( in32, S[ 0 ] ); + X = MULT16_32_Q15(QCONST16(0.6074371f, 15), Y); + out32 = ADD32( S[ 0 ], X ); + S[ 0 ] = ADD32( in32, X ); + out32_hp = out32; + /* Convert to Q10 */ + in32 = in[ 2 * k + 1 ]; + + /* All-pass section for odd input sample, and add to output of previous section */ + Y = SUB32( in32, S[ 1 ] ); + X = MULT16_32_Q15(QCONST16(0.15063f, 15), Y); + out32 = ADD32( out32, S[ 1 ] ); + out32 = ADD32( out32, X ); + S[ 1 ] = ADD32( in32, X ); + + Y = SUB32( -in32, S[ 2 ] ); + X = MULT16_32_Q15(QCONST16(0.15063f, 15), Y); + out32_hp = ADD32( out32_hp, S[ 2 ] ); + out32_hp = ADD32( out32_hp, X ); + S[ 2 ] = ADD32( -in32, X ); + + hp_ener += out32_hp*(opus_val64)out32_hp; + /* Add, convert back to int16 and store to output */ + out[ k ] = HALF32(out32); + } +#ifdef FIXED_POINT + /* len2 can be up to 480, so we shift by 8 more to make it fit. */ + hp_ener = hp_ener >> (2*SIG_SHIFT + 8); +#endif + return (opus_val32)hp_ener; +} + +static opus_val32 downmix_and_resample(downmix_func downmix, const void *_x, opus_val32 *y, opus_val32 S[3], int subframe, int offset, int c1, int c2, int C, int Fs) +{ + VARDECL(opus_val32, tmp); + opus_val32 scale; + int j; + opus_val32 ret = 0; + SAVE_STACK; + + if (subframe==0) return 0; + if (Fs == 48000) + { + subframe *= 2; + offset *= 2; + } else if (Fs == 16000) { + subframe = subframe*2/3; + offset = offset*2/3; + } + ALLOC(tmp, subframe, opus_val32); + + downmix(_x, tmp, subframe, offset, c1, c2, C); +#ifdef FIXED_POINT + scale = (1<-1) + scale /= 2; + for (j=0;jarch = opus_select_arch(); + tonal->Fs = Fs; + /* Clear remaining fields. */ + tonality_analysis_reset(tonal); +} + +void tonality_analysis_reset(TonalityAnalysisState *tonal) +{ + /* Clear non-reusable fields. */ + char *start = (char*)&tonal->TONALITY_ANALYSIS_RESET_START; + OPUS_CLEAR(start, sizeof(TonalityAnalysisState) - (start - (char*)tonal)); +} + +void tonality_get_info(TonalityAnalysisState *tonal, AnalysisInfo *info_out, int len) +{ + int pos; + int curr_lookahead; + float tonality_max; + float tonality_avg; + int tonality_count; + int i; + int pos0; + float prob_avg; + float prob_count; + float prob_min, prob_max; + float vad_prob; + int mpos, vpos; + int bandwidth_span; + + pos = tonal->read_pos; + curr_lookahead = tonal->write_pos-tonal->read_pos; + if (curr_lookahead<0) + curr_lookahead += DETECT_SIZE; + + tonal->read_subframe += len/(tonal->Fs/400); + while (tonal->read_subframe>=8) + { + tonal->read_subframe -= 8; + tonal->read_pos++; + } + if (tonal->read_pos>=DETECT_SIZE) + tonal->read_pos-=DETECT_SIZE; + + /* On long frames, look at the second analysis window rather than the first. */ + if (len > tonal->Fs/50 && pos != tonal->write_pos) + { + pos++; + if (pos==DETECT_SIZE) + pos=0; + } + if (pos == tonal->write_pos) + pos--; + if (pos<0) + pos = DETECT_SIZE-1; + pos0 = pos; + OPUS_COPY(info_out, &tonal->info[pos], 1); + if (!info_out->valid) + return; + tonality_max = tonality_avg = info_out->tonality; + tonality_count = 1; + /* Look at the neighbouring frames and pick largest bandwidth found (to be safe). */ + bandwidth_span = 6; + /* If possible, look ahead for a tone to compensate for the delay in the tone detector. */ + for (i=0;i<3;i++) + { + pos++; + if (pos==DETECT_SIZE) + pos = 0; + if (pos == tonal->write_pos) + break; + tonality_max = MAX32(tonality_max, tonal->info[pos].tonality); + tonality_avg += tonal->info[pos].tonality; + tonality_count++; + info_out->bandwidth = IMAX(info_out->bandwidth, tonal->info[pos].bandwidth); + bandwidth_span--; + } + pos = pos0; + /* Look back in time to see if any has a wider bandwidth than the current frame. */ + for (i=0;iwrite_pos) + break; + info_out->bandwidth = IMAX(info_out->bandwidth, tonal->info[pos].bandwidth); + } + info_out->tonality = MAX32(tonality_avg/tonality_count, tonality_max-.2f); + + mpos = vpos = pos0; + /* If we have enough look-ahead, compensate for the ~5-frame delay in the music prob and + ~1 frame delay in the VAD prob. */ + if (curr_lookahead > 15) + { + mpos += 5; + if (mpos>=DETECT_SIZE) + mpos -= DETECT_SIZE; + vpos += 1; + if (vpos>=DETECT_SIZE) + vpos -= DETECT_SIZE; + } + + /* The following calculations attempt to minimize a "badness function" + for the transition. When switching from speech to music, the badness + of switching at frame k is + b_k = S*v_k + \sum_{i=0}^{k-1} v_i*(p_i - T) + where + v_i is the activity probability (VAD) at frame i, + p_i is the music probability at frame i + T is the probability threshold for switching + S is the penalty for switching during active audio rather than silence + the current frame has index i=0 + + Rather than apply badness to directly decide when to switch, what we compute + instead is the threshold for which the optimal switching point is now. When + considering whether to switch now (frame 0) or at frame k, we have: + S*v_0 = S*v_k + \sum_{i=0}^{k-1} v_i*(p_i - T) + which gives us: + T = ( \sum_{i=0}^{k-1} v_i*p_i + S*(v_k-v_0) ) / ( \sum_{i=0}^{k-1} v_i ) + We take the min threshold across all positive values of k (up to the maximum + amount of lookahead we have) to give us the threshold for which the current + frame is the optimal switch point. + + The last step is that we need to consider whether we want to switch at all. + For that we use the average of the music probability over the entire window. + If the threshold is higher than that average we're not going to + switch, so we compute a min with the average as well. The result of all these + min operations is music_prob_min, which gives the threshold for switching to music + if we're currently encoding for speech. + + We do the exact opposite to compute music_prob_max which is used for switching + from music to speech. + */ + prob_min = 1.f; + prob_max = 0.f; + vad_prob = tonal->info[vpos].activity_probability; + prob_count = MAX16(.1f, vad_prob); + prob_avg = MAX16(.1f, vad_prob)*tonal->info[mpos].music_prob; + while (1) + { + float pos_vad; + mpos++; + if (mpos==DETECT_SIZE) + mpos = 0; + if (mpos == tonal->write_pos) + break; + vpos++; + if (vpos==DETECT_SIZE) + vpos = 0; + if (vpos == tonal->write_pos) + break; + pos_vad = tonal->info[vpos].activity_probability; + prob_min = MIN16((prob_avg - TRANSITION_PENALTY*(vad_prob - pos_vad))/prob_count, prob_min); + prob_max = MAX16((prob_avg + TRANSITION_PENALTY*(vad_prob - pos_vad))/prob_count, prob_max); + prob_count += MAX16(.1f, pos_vad); + prob_avg += MAX16(.1f, pos_vad)*tonal->info[mpos].music_prob; + } + info_out->music_prob = prob_avg/prob_count; + prob_min = MIN16(prob_avg/prob_count, prob_min); + prob_max = MAX16(prob_avg/prob_count, prob_max); + prob_min = MAX16(prob_min, 0.f); + prob_max = MIN16(prob_max, 1.f); + + /* If we don't have enough look-ahead, do our best to make a decent decision. */ + if (curr_lookahead < 10) + { + float pmin, pmax; + pmin = prob_min; + pmax = prob_max; + pos = pos0; + /* Look for min/max in the past. */ + for (i=0;icount-1, 15);i++) + { + pos--; + if (pos < 0) + pos = DETECT_SIZE-1; + pmin = MIN16(pmin, tonal->info[pos].music_prob); + pmax = MAX16(pmax, tonal->info[pos].music_prob); + } + /* Bias against switching on active audio. */ + pmin = MAX16(0.f, pmin - .1f*vad_prob); + pmax = MIN16(1.f, pmax + .1f*vad_prob); + prob_min += (1.f-.1f*curr_lookahead)*(pmin - prob_min); + prob_max += (1.f-.1f*curr_lookahead)*(pmax - prob_max); + } + info_out->music_prob_min = prob_min; + info_out->music_prob_max = prob_max; + + /* printf("%f %f %f %f %f\n", prob_min, prob_max, prob_avg/prob_count, vad_prob, info_out->music_prob); */ +} + +static const float std_feature_bias[9] = { + 5.684947f, 3.475288f, 1.770634f, 1.599784f, 3.773215f, + 2.163313f, 1.260756f, 1.116868f, 1.918795f +}; + +#define LEAKAGE_OFFSET 2.5f +#define LEAKAGE_SLOPE 2.f + +#ifdef FIXED_POINT +/* For fixed-point, the input is +/-2^15 shifted up by SIG_SHIFT, so we need to + compensate for that in the energy. */ +#define SCALE_COMPENS (1.f/((opus_int32)1<<(15+SIG_SHIFT))) +#define SCALE_ENER(e) ((SCALE_COMPENS*SCALE_COMPENS)*(e)) +#else +#define SCALE_ENER(e) (e) +#endif + +#ifdef FIXED_POINT +static int is_digital_silence32(const opus_val32* pcm, int frame_size, int channels, int lsb_depth) +{ + int silence = 0; + opus_val32 sample_max = 0; +#ifdef MLP_TRAINING + return 0; +#endif + sample_max = celt_maxabs32(pcm, frame_size*channels); + + silence = (sample_max == 0); + (void)lsb_depth; + return silence; +} +#else +#define is_digital_silence32(pcm, frame_size, channels, lsb_depth) is_digital_silence(pcm, frame_size, channels, lsb_depth) +#endif + +static void tonality_analysis(TonalityAnalysisState *tonal, const CELTMode *celt_mode, const void *x, int len, int offset, int c1, int c2, int C, int lsb_depth, downmix_func downmix) +{ + int i, b; + const kiss_fft_state *kfft; + VARDECL(kiss_fft_cpx, in); + VARDECL(kiss_fft_cpx, out); + int N = 480, N2=240; + float * OPUS_RESTRICT A = tonal->angle; + float * OPUS_RESTRICT dA = tonal->d_angle; + float * OPUS_RESTRICT d2A = tonal->d2_angle; + VARDECL(float, tonality); + VARDECL(float, noisiness); + float band_tonality[NB_TBANDS]; + float logE[NB_TBANDS]; + float BFCC[8]; + float features[25]; + float frame_tonality; + float max_frame_tonality; + /*float tw_sum=0;*/ + float frame_noisiness; + const float pi4 = (float)(M_PI*M_PI*M_PI*M_PI); + float slope=0; + float frame_stationarity; + float relativeE; + float frame_probs[2]; + float alpha, alphaE, alphaE2; + float frame_loudness; + float bandwidth_mask; + int is_masked[NB_TBANDS+1]; + int bandwidth=0; + float maxE = 0; + float noise_floor; + int remaining; + AnalysisInfo *info; + float hp_ener; + float tonality2[240]; + float midE[8]; + float spec_variability=0; + float band_log2[NB_TBANDS+1]; + float leakage_from[NB_TBANDS+1]; + float leakage_to[NB_TBANDS+1]; + float layer_out[MAX_NEURONS]; + float below_max_pitch; + float above_max_pitch; + int is_silence; + SAVE_STACK; + + if (!tonal->initialized) + { + tonal->mem_fill = 240; + tonal->initialized = 1; + } + alpha = 1.f/IMIN(10, 1+tonal->count); + alphaE = 1.f/IMIN(25, 1+tonal->count); + /* Noise floor related decay for bandwidth detection: -2.2 dB/second */ + alphaE2 = 1.f/IMIN(100, 1+tonal->count); + if (tonal->count <= 1) alphaE2 = 1; + + if (tonal->Fs == 48000) + { + /* len and offset are now at 24 kHz. */ + len/= 2; + offset /= 2; + } else if (tonal->Fs == 16000) { + len = 3*len/2; + offset = 3*offset/2; + } + + kfft = celt_mode->mdct.kfft[0]; + tonal->hp_ener_accum += (float)downmix_and_resample(downmix, x, + &tonal->inmem[tonal->mem_fill], tonal->downmix_state, + IMIN(len, ANALYSIS_BUF_SIZE-tonal->mem_fill), offset, c1, c2, C, tonal->Fs); + if (tonal->mem_fill+len < ANALYSIS_BUF_SIZE) + { + tonal->mem_fill += len; + /* Don't have enough to update the analysis */ + RESTORE_STACK; + return; + } + hp_ener = tonal->hp_ener_accum; + info = &tonal->info[tonal->write_pos++]; + if (tonal->write_pos>=DETECT_SIZE) + tonal->write_pos-=DETECT_SIZE; + + is_silence = is_digital_silence32(tonal->inmem, ANALYSIS_BUF_SIZE, 1, lsb_depth); + + ALLOC(in, 480, kiss_fft_cpx); + ALLOC(out, 480, kiss_fft_cpx); + ALLOC(tonality, 240, float); + ALLOC(noisiness, 240, float); + for (i=0;iinmem[i]); + in[i].i = (kiss_fft_scalar)(w*tonal->inmem[N2+i]); + in[N-i-1].r = (kiss_fft_scalar)(w*tonal->inmem[N-i-1]); + in[N-i-1].i = (kiss_fft_scalar)(w*tonal->inmem[N+N2-i-1]); + } + OPUS_MOVE(tonal->inmem, tonal->inmem+ANALYSIS_BUF_SIZE-240, 240); + remaining = len - (ANALYSIS_BUF_SIZE-tonal->mem_fill); + tonal->hp_ener_accum = (float)downmix_and_resample(downmix, x, + &tonal->inmem[240], tonal->downmix_state, remaining, + offset+ANALYSIS_BUF_SIZE-tonal->mem_fill, c1, c2, C, tonal->Fs); + tonal->mem_fill = 240 + remaining; + if (is_silence) + { + /* On silence, copy the previous analysis. */ + int prev_pos = tonal->write_pos-2; + if (prev_pos < 0) + prev_pos += DETECT_SIZE; + OPUS_COPY(info, &tonal->info[prev_pos], 1); + RESTORE_STACK; + return; + } + opus_fft(kfft, in, out, tonal->arch); +#ifndef FIXED_POINT + /* If there's any NaN on the input, the entire output will be NaN, so we only need to check one value. */ + if (celt_isnan(out[0].r)) + { + info->valid = 0; + RESTORE_STACK; + return; + } +#endif + + for (i=1;iactivity = 0; + frame_noisiness = 0; + frame_stationarity = 0; + if (!tonal->count) + { + for (b=0;blowE[b] = 1e10; + tonal->highE[b] = -1e10; + } + } + relativeE = 0; + frame_loudness = 0; + /* The energy of the very first band is special because of DC. */ + { + float E = 0; + float X1r, X2r; + X1r = 2*(float)out[0].r; + X2r = 2*(float)out[0].i; + E = X1r*X1r + X2r*X2r; + for (i=1;i<4;i++) + { + float binE = out[i].r*(float)out[i].r + out[N-i].r*(float)out[N-i].r + + out[i].i*(float)out[i].i + out[N-i].i*(float)out[N-i].i; + E += binE; + } + E = SCALE_ENER(E); + band_log2[0] = .5f*1.442695f*(float)log(E+1e-10f); + } + for (b=0;bvalid = 0; + RESTORE_STACK; + return; + } +#endif + + tonal->E[tonal->E_count][b] = E; + frame_noisiness += nE/(1e-15f+E); + + frame_loudness += (float)sqrt(E+1e-10f); + logE[b] = (float)log(E+1e-10f); + band_log2[b+1] = .5f*1.442695f*(float)log(E+1e-10f); + tonal->logE[tonal->E_count][b] = logE[b]; + if (tonal->count==0) + tonal->highE[b] = tonal->lowE[b] = logE[b]; + if (tonal->highE[b] > tonal->lowE[b] + 7.5) + { + if (tonal->highE[b] - logE[b] > logE[b] - tonal->lowE[b]) + tonal->highE[b] -= .01f; + else + tonal->lowE[b] += .01f; + } + if (logE[b] > tonal->highE[b]) + { + tonal->highE[b] = logE[b]; + tonal->lowE[b] = MAX32(tonal->highE[b]-15, tonal->lowE[b]); + } else if (logE[b] < tonal->lowE[b]) + { + tonal->lowE[b] = logE[b]; + tonal->highE[b] = MIN32(tonal->lowE[b]+15, tonal->highE[b]); + } + relativeE += (logE[b]-tonal->lowE[b])/(1e-5f + (tonal->highE[b]-tonal->lowE[b])); + + L1=L2=0; + for (i=0;iE[i][b]); + L2 += tonal->E[i][b]; + } + + stationarity = MIN16(0.99f,L1/(float)sqrt(1e-15+NB_FRAMES*L2)); + stationarity *= stationarity; + stationarity *= stationarity; + frame_stationarity += stationarity; + /*band_tonality[b] = tE/(1e-15+E)*/; + band_tonality[b] = MAX16(tE/(1e-15f+E), stationarity*tonal->prev_band_tonality[b]); +#if 0 + if (b>=NB_TONAL_SKIP_BANDS) + { + frame_tonality += tweight[b]*band_tonality[b]; + tw_sum += tweight[b]; + } +#else + frame_tonality += band_tonality[b]; + if (b>=NB_TBANDS-NB_TONAL_SKIP_BANDS) + frame_tonality -= band_tonality[b-NB_TBANDS+NB_TONAL_SKIP_BANDS]; +#endif + max_frame_tonality = MAX16(max_frame_tonality, (1.f+.03f*(b-NB_TBANDS))*frame_tonality); + slope += band_tonality[b]*(b-8); + /*printf("%f %f ", band_tonality[b], stationarity);*/ + tonal->prev_band_tonality[b] = band_tonality[b]; + } + + leakage_from[0] = band_log2[0]; + leakage_to[0] = band_log2[0] - LEAKAGE_OFFSET; + for (b=1;b=0;b--) + { + float leak_slope = LEAKAGE_SLOPE*(tbands[b+1]-tbands[b])/4; + leakage_from[b] = MIN16(leakage_from[b+1]+leak_slope, leakage_from[b]); + leakage_to[b] = MAX16(leakage_to[b+1]-leak_slope, leakage_to[b]); + } + celt_assert(NB_TBANDS+1 <= LEAK_BANDS); + for (b=0;bleak_boost[b] = IMIN(255, (int)floor(.5 + 64.f*boost)); + } + for (;bleak_boost[b] = 0; + + for (i=0;ilogE[i][k] - tonal->logE[j][k]; + dist += tmp*tmp; + } + if (j!=i) + mindist = MIN32(mindist, dist); + } + spec_variability += mindist; + } + spec_variability = (float)sqrt(spec_variability/NB_FRAMES/NB_TBANDS); + bandwidth_mask = 0; + bandwidth = 0; + maxE = 0; + noise_floor = 5.7e-4f/(1<<(IMAX(0,lsb_depth-8))); + noise_floor *= noise_floor; + below_max_pitch=0; + above_max_pitch=0; + for (b=0;bmeanE[b] = MAX32((1-alphaE2)*tonal->meanE[b], E); + Em = MAX32(E, tonal->meanE[b]); + /* Consider the band "active" only if all these conditions are met: + 1) less than 90 dB below the peak band (maximal masking possible considering + both the ATH and the loudness-dependent slope of the spreading function) + 2) above the PCM quantization noise floor + We use b+1 because the first CELT band isn't included in tbands[] + */ + if (E*1e9f > maxE && (Em > 3*noise_floor*(band_end-band_start) || E > noise_floor*(band_end-band_start))) + bandwidth = b+1; + /* Check if the band is masked (see below). */ + is_masked[b] = E < (tonal->prev_bandwidth >= b+1 ? .01f : .05f)*bandwidth_mask; + /* Use a simple follower with 13 dB/Bark slope for spreading function. */ + bandwidth_mask = MAX32(.05f*bandwidth_mask, E); + } + /* Special case for the last two bands, for which we don't have spectrum but only + the energy above 12 kHz. The difficulty here is that the high-pass we use + leaks some LF energy, so we need to increase the threshold without accidentally cutting + off the band. */ + if (tonal->Fs == 48000) { + float noise_ratio; + float Em; + float E = hp_ener*(1.f/(60*60)); + noise_ratio = tonal->prev_bandwidth==20 ? 10.f : 30.f; + +#ifdef FIXED_POINT + /* silk_resampler_down2_hp() shifted right by an extra 8 bits. */ + E *= 256.f*(1.f/Q15ONE)*(1.f/Q15ONE); +#endif + above_max_pitch += E; + tonal->meanE[b] = MAX32((1-alphaE2)*tonal->meanE[b], E); + Em = MAX32(E, tonal->meanE[b]); + if (Em > 3*noise_ratio*noise_floor*160 || E > noise_ratio*noise_floor*160) + bandwidth = 20; + /* Check if the band is masked (see below). */ + is_masked[b] = E < (tonal->prev_bandwidth == 20 ? .01f : .05f)*bandwidth_mask; + } + if (above_max_pitch > below_max_pitch) + info->max_pitch_ratio = below_max_pitch/above_max_pitch; + else + info->max_pitch_ratio = 1; + /* In some cases, resampling aliasing can create a small amount of energy in the first band + being cut. So if the last band is masked, we don't include it. */ + if (bandwidth == 20 && is_masked[NB_TBANDS]) + bandwidth-=2; + else if (bandwidth > 0 && bandwidth <= NB_TBANDS && is_masked[bandwidth-1]) + bandwidth--; + if (tonal->count<=2) + bandwidth = 20; + frame_loudness = 20*(float)log10(frame_loudness); + tonal->Etracker = MAX32(tonal->Etracker-.003f, frame_loudness); + tonal->lowECount *= (1-alphaE); + if (frame_loudness < tonal->Etracker-30) + tonal->lowECount += alphaE; + + for (i=0;i<8;i++) + { + float sum=0; + for (b=0;b<16;b++) + sum += dct_table[i*16+b]*logE[b]; + BFCC[i] = sum; + } + for (i=0;i<8;i++) + { + float sum=0; + for (b=0;b<16;b++) + sum += dct_table[i*16+b]*.5f*(tonal->highE[b]+tonal->lowE[b]); + midE[i] = sum; + } + + frame_stationarity /= NB_TBANDS; + relativeE /= NB_TBANDS; + if (tonal->count<10) + relativeE = .5f; + frame_noisiness /= NB_TBANDS; +#if 1 + info->activity = frame_noisiness + (1-frame_noisiness)*relativeE; +#else + info->activity = .5*(1+frame_noisiness-frame_stationarity); +#endif + frame_tonality = (max_frame_tonality/(NB_TBANDS-NB_TONAL_SKIP_BANDS)); + frame_tonality = MAX16(frame_tonality, tonal->prev_tonality*.8f); + tonal->prev_tonality = frame_tonality; + + slope /= 8*8; + info->tonality_slope = slope; + + tonal->E_count = (tonal->E_count+1)%NB_FRAMES; + tonal->count = IMIN(tonal->count+1, ANALYSIS_COUNT_MAX); + info->tonality = frame_tonality; + + for (i=0;i<4;i++) + features[i] = -0.12299f*(BFCC[i]+tonal->mem[i+24]) + 0.49195f*(tonal->mem[i]+tonal->mem[i+16]) + 0.69693f*tonal->mem[i+8] - 1.4349f*tonal->cmean[i]; + + for (i=0;i<4;i++) + tonal->cmean[i] = (1-alpha)*tonal->cmean[i] + alpha*BFCC[i]; + + for (i=0;i<4;i++) + features[4+i] = 0.63246f*(BFCC[i]-tonal->mem[i+24]) + 0.31623f*(tonal->mem[i]-tonal->mem[i+16]); + for (i=0;i<3;i++) + features[8+i] = 0.53452f*(BFCC[i]+tonal->mem[i+24]) - 0.26726f*(tonal->mem[i]+tonal->mem[i+16]) -0.53452f*tonal->mem[i+8]; + + if (tonal->count > 5) + { + for (i=0;i<9;i++) + tonal->std[i] = (1-alpha)*tonal->std[i] + alpha*features[i]*features[i]; + } + for (i=0;i<4;i++) + features[i] = BFCC[i]-midE[i]; + + for (i=0;i<8;i++) + { + tonal->mem[i+24] = tonal->mem[i+16]; + tonal->mem[i+16] = tonal->mem[i+8]; + tonal->mem[i+8] = tonal->mem[i]; + tonal->mem[i] = BFCC[i]; + } + for (i=0;i<9;i++) + features[11+i] = (float)sqrt(tonal->std[i]) - std_feature_bias[i]; + features[18] = spec_variability - 0.78f; + features[20] = info->tonality - 0.154723f; + features[21] = info->activity - 0.724643f; + features[22] = frame_stationarity - 0.743717f; + features[23] = info->tonality_slope + 0.069216f; + features[24] = tonal->lowECount - 0.067930f; + + analysis_compute_dense(&layer0, layer_out, features); + analysis_compute_gru(&layer1, tonal->rnn_state, layer_out); + analysis_compute_dense(&layer2, frame_probs, tonal->rnn_state); + + /* Probability of speech or music vs noise */ + info->activity_probability = frame_probs[1]; + info->music_prob = frame_probs[0]; + + /*printf("%f %f %f\n", frame_probs[0], frame_probs[1], info->music_prob);*/ +#ifdef MLP_TRAINING + for (i=0;i<25;i++) + printf("%f ", features[i]); + printf("\n"); +#endif + + info->bandwidth = bandwidth; + tonal->prev_bandwidth = bandwidth; + /*printf("%d %d\n", info->bandwidth, info->opus_bandwidth);*/ + info->noisiness = frame_noisiness; + info->valid = 1; + RESTORE_STACK; +} + +void run_analysis(TonalityAnalysisState *analysis, const CELTMode *celt_mode, const void *analysis_pcm, + int analysis_frame_size, int frame_size, int c1, int c2, int C, opus_int32 Fs, + int lsb_depth, downmix_func downmix, AnalysisInfo *analysis_info) +{ + int offset; + int pcm_len; + + analysis_frame_size -= analysis_frame_size&1; + if (analysis_pcm != NULL) + { + /* Avoid overflow/wrap-around of the analysis buffer */ + analysis_frame_size = IMIN((DETECT_SIZE-5)*Fs/50, analysis_frame_size); + + pcm_len = analysis_frame_size - analysis->analysis_offset; + offset = analysis->analysis_offset; + while (pcm_len>0) { + tonality_analysis(analysis, celt_mode, analysis_pcm, IMIN(Fs/50, pcm_len), offset, c1, c2, C, lsb_depth, downmix); + offset += Fs/50; + pcm_len -= Fs/50; + } + analysis->analysis_offset = analysis_frame_size; + + analysis->analysis_offset -= frame_size; + } + + tonality_get_info(analysis, analysis_info, frame_size); +} + +#endif /* DISABLE_FLOAT_API */ diff --git a/port/third_party/opus/src/analysis.h b/port/third_party/opus/src/analysis.h new file mode 100644 index 00000000..0b66555f --- /dev/null +++ b/port/third_party/opus/src/analysis.h @@ -0,0 +1,103 @@ +/* Copyright (c) 2011 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef ANALYSIS_H +#define ANALYSIS_H + +#include "celt.h" +#include "opus_private.h" +#include "mlp.h" + +#define NB_FRAMES 8 +#define NB_TBANDS 18 +#define ANALYSIS_BUF_SIZE 720 /* 30 ms at 24 kHz */ + +/* At that point we can stop counting frames because it no longer matters. */ +#define ANALYSIS_COUNT_MAX 10000 + +#define DETECT_SIZE 100 + +/* Uncomment this to print the MLP features on stdout. */ +/*#define MLP_TRAINING*/ + +typedef struct { + int arch; + int application; + opus_int32 Fs; +#define TONALITY_ANALYSIS_RESET_START angle + float angle[240]; + float d_angle[240]; + float d2_angle[240]; + opus_val32 inmem[ANALYSIS_BUF_SIZE]; + int mem_fill; /* number of usable samples in the buffer */ + float prev_band_tonality[NB_TBANDS]; + float prev_tonality; + int prev_bandwidth; + float E[NB_FRAMES][NB_TBANDS]; + float logE[NB_FRAMES][NB_TBANDS]; + float lowE[NB_TBANDS]; + float highE[NB_TBANDS]; + float meanE[NB_TBANDS+1]; + float mem[32]; + float cmean[8]; + float std[9]; + float Etracker; + float lowECount; + int E_count; + int count; + int analysis_offset; + int write_pos; + int read_pos; + int read_subframe; + float hp_ener_accum; + int initialized; + float rnn_state[MAX_NEURONS]; + opus_val32 downmix_state[3]; + AnalysisInfo info[DETECT_SIZE]; +} TonalityAnalysisState; + +/** Initialize a TonalityAnalysisState struct. + * + * This performs some possibly slow initialization steps which should + * not be repeated every analysis step. No allocated memory is retained + * by the state struct, so no cleanup call is required. + */ +void tonality_analysis_init(TonalityAnalysisState *analysis, opus_int32 Fs); + +/** Reset a TonalityAnalysisState stuct. + * + * Call this when there's a discontinuity in the data. + */ +void tonality_analysis_reset(TonalityAnalysisState *analysis); + +void tonality_get_info(TonalityAnalysisState *tonal, AnalysisInfo *info_out, int len); + +void run_analysis(TonalityAnalysisState *analysis, const CELTMode *celt_mode, const void *analysis_pcm, + int analysis_frame_size, int frame_size, int c1, int c2, int C, opus_int32 Fs, + int lsb_depth, downmix_func downmix, AnalysisInfo *analysis_info); + +#endif diff --git a/port/third_party/opus/src/extensions.c b/port/third_party/opus/src/extensions.c new file mode 100644 index 00000000..bb6c0b02 --- /dev/null +++ b/port/third_party/opus/src/extensions.c @@ -0,0 +1,315 @@ +/* Copyright (c) 2022 Amazon */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + + +#include "opus_types.h" +#include "opus_defines.h" +#include "arch.h" +#include "os_support.h" +#include "opus_private.h" + + +/* Given an extension payload, advance data to the next extension and return the + length of the remaining extensions. */ +opus_int32 skip_extension(const unsigned char **data, opus_int32 len, opus_int32 *header_size) +{ + int id, L; + if (len==0) + return 0; + id = **data>>1; + L = **data&1; + if (id == 0 && L == 1) + { + *header_size = 1; + if (len < 1) + return -1; + (*data)++; + len--; + return len; + } else if (id > 0 && id < 32) + { + if (len < 1+L) + return -1; + *data += 1+L; + len -= 1+L; + *header_size = 1; + return len; + } else { + if (L==0) + { + *data += len; + *header_size = 1; + return 0; + } else { + opus_int32 bytes=0; + *header_size = 1; + do { + (*data)++; + len--; + if (len == 0) + return -1; + bytes += **data; + (*header_size)++; + } while (**data == 255); + (*data)++; + len--; + if (bytes <= len) + { + len -= bytes; + *data += bytes; + } else { + return -1; + } + return len; + } + } +} + +/* Count the number of extensions, excluding real padding and separators. */ +opus_int32 opus_packet_extensions_count(const unsigned char *data, opus_int32 len) +{ + opus_int32 curr_len; + opus_int32 count=0; + const unsigned char *curr_data = data; + + celt_assert(len >= 0); + celt_assert(data != NULL || len == 0); + + curr_len = len; + while (curr_len > 0) + { + int id; + opus_int32 header_size; + id = *curr_data>>1; + curr_len = skip_extension(&curr_data, curr_len, &header_size); + if (curr_len < 0) + return OPUS_INVALID_PACKET; + if (id > 1) + count++; + } + return count; +} + +/* Extract extensions from Opus padding (excluding real padding and separators) */ +opus_int32 opus_packet_extensions_parse(const unsigned char *data, opus_int32 len, opus_extension_data *extensions, opus_int32 *nb_extensions) +{ + const unsigned char *curr_data; + opus_int32 curr_len; + int curr_frame=0; + opus_int32 count=0; + + celt_assert(len >= 0); + celt_assert(data != NULL || len == 0); + celt_assert(nb_extensions != NULL); + celt_assert(extensions != NULL || *nb_extensions == 0); + + curr_data = data; + curr_len = len; + while (curr_len > 0) + { + int id; + opus_int32 header_size; + opus_extension_data curr_ext; + id = *curr_data>>1; + if (id > 1) + { + curr_ext.id = id; + curr_ext.frame = curr_frame; + curr_ext.data = curr_data; + } else if (id == 1) + { + int L = *curr_data&1; + if (L==0) + curr_frame++; + else { + if (curr_len >= 2) + curr_frame += curr_data[1]; + /* Else we're at the end and it doesn't matter. */ + } + if (curr_frame >= 48) + { + *nb_extensions = count; + return OPUS_INVALID_PACKET; + } + } + curr_len = skip_extension(&curr_data, curr_len, &header_size); + /* printf("curr_len = %d, header_size = %d\n", curr_len, header_size); */ + if (curr_len < 0) + { + *nb_extensions = count; + return OPUS_INVALID_PACKET; + } + celt_assert(curr_data - data == len - curr_len); + if (id > 1) + { + if (count == *nb_extensions) + { + return OPUS_BUFFER_TOO_SMALL; + } + curr_ext.len = curr_data - curr_ext.data - header_size; + curr_ext.data += header_size; + extensions[count++] = curr_ext; + } + } + celt_assert(curr_len == 0); + *nb_extensions = count; + return OPUS_OK; +} + +opus_int32 opus_packet_extensions_generate(unsigned char *data, opus_int32 len, const opus_extension_data *extensions, opus_int32 nb_extensions, int pad) +{ + int max_frame=0; + opus_int32 i; + int frame; + int curr_frame = 0; + opus_int32 pos = 0; + opus_int32 written = 0; + + celt_assert(len >= 0); + + for (i=0;i 127) + return OPUS_BAD_ARG; + } + if (max_frame >= 48) return OPUS_BAD_ARG; + for (frame=0;frame<=max_frame;frame++) + { + for (i=0;i 1) + return OPUS_BAD_ARG; + if (len-pos < extensions[i].len+1) + return OPUS_BUFFER_TOO_SMALL; + if (data) data[pos] = (extensions[i].id<<1) + extensions[i].len; + pos++; + if (extensions[i].len > 0) { + if (data) data[pos] = extensions[i].data[0]; + pos++; + } + } else { + int last; + opus_int32 length_bytes; + if (extensions[i].len < 0) + return OPUS_BAD_ARG; + last = (written == nb_extensions - 1); + length_bytes = 1 + extensions[i].len/255; + if (last) + length_bytes = 0; + if (len-pos < 1 + length_bytes + extensions[i].len) + return OPUS_BUFFER_TOO_SMALL; + if (data) data[pos] = (extensions[i].id<<1) + !last; + pos++; + if (!last) + { + opus_int32 j; + for (j=0;j +int main() +{ + opus_extension_data ext[] = {{2, 0, (const unsigned char *)"a", 1}, + {32, 10, (const unsigned char *)"DRED", 4}, + {33, 1, (const unsigned char *)"NOT DRED", 8}, + {3, 4, (const unsigned char *)NULL, 0} + }; + opus_extension_data ext2[10]; + int i, len; + int nb_ext = 10; + unsigned char packet[10000]; + len = opus_packet_extensions_generate(packet, 32, ext, 4, 1); + for (i=0;i +#include "opus_types.h" +#include "opus_defines.h" +#include "arch.h" +#include "mlp.h" + +#define fmadd(a, b, c) ((a)*(b)+(c)) +static OPUS_INLINE float tansig_approx(float x) +{ + const float N0 = 952.52801514f; + const float N1 = 96.39235687f; + const float N2 = 0.60863042f; + const float D0 = 952.72399902f; + const float D1 = 413.36801147f; + const float D2 = 11.88600922f; + float X2, num, den; + X2 = x*x; + num = fmadd(fmadd(N2, X2, N1), X2, N0); + den = fmadd(fmadd(D2, X2, D1), X2, D0); + num = num*x/den; + return MAX32(-1.f, MIN32(1.f, num)); +} + +static OPUS_INLINE float sigmoid_approx(float x) +{ + return .5f + .5f*tansig_approx(.5f*x); +} + +static void gemm_accum(float *out, const opus_int8 *weights, int rows, int cols, int col_stride, const float *x) +{ + int i, j; + for (i=0;inb_inputs; + N = layer->nb_neurons; + stride = N; + for (i=0;ibias[i]; + gemm_accum(output, layer->input_weights, N, M, stride, input); + for (i=0;isigmoid) { + for (i=0;inb_inputs; + N = gru->nb_neurons; + stride = 3*N; + /* Compute update gate. */ + for (i=0;ibias[i]; + gemm_accum(z, gru->input_weights, N, M, stride, input); + gemm_accum(z, gru->recurrent_weights, N, N, stride, state); + for (i=0;ibias[N + i]; + gemm_accum(r, &gru->input_weights[N], N, M, stride, input); + gemm_accum(r, &gru->recurrent_weights[N], N, N, stride, state); + for (i=0;ibias[2*N + i]; + for (i=0;iinput_weights[2*N], N, M, stride, input); + gemm_accum(h, &gru->recurrent_weights[2*N], N, N, stride, tmp); + for (i=0;i=0) + break; + x[i*C] = x[i*C]+a*x[i*C]*x[i*C]; + } + + curr=0; + x0 = x[0]; + while(1) + { + int start, end; + float maxval; + int special=0; + int peak_pos; + for (i=curr;i1 || x[i*C]<-1) + break; + } + if (i==N) + { + a=0; + break; + } + peak_pos = i; + start=end=i; + maxval=ABS16(x[i*C]); + /* Look for first zero crossing before clipping */ + while (start>0 && x[i*C]*x[(start-1)*C]>=0) + start--; + /* Look for first zero crossing after clipping */ + while (end=0) + { + /* Look for other peaks until the next zero-crossing. */ + if (ABS16(x[end*C])>maxval) + { + maxval = ABS16(x[end*C]); + peak_pos = end; + } + end++; + } + /* Detect the special case where we clip before the first zero crossing */ + special = (start==0 && x[i*C]*x[0]>=0); + + /* Compute a such that maxval + a*maxval^2 = 1 */ + a=(maxval-1)/(maxval*maxval); + /* Slightly boost "a" by 2^-22. This is just enough to ensure -ffast-math + does not cause output values larger than +/-1, but small enough not + to matter even for 24-bit output. */ + a += a*2.4e-7f; + if (x[i*C]>0) + a = -a; + /* Apply soft clipping */ + for (i=start;i=2) + { + /* Add a linear ramp from the first sample to the signal peak. + This avoids a discontinuity at the beginning of the frame. */ + float delta; + float offset = x0-x[0]; + delta = offset / peak_pos; + for (i=curr;i>2; + return 2; + } +} + +static int parse_size(const unsigned char *data, opus_int32 len, opus_int16 *size) +{ + if (len<1) + { + *size = -1; + return -1; + } else if (data[0]<252) + { + *size = data[0]; + return 1; + } else if (len<2) + { + *size = -1; + return -1; + } else { + *size = 4*data[1] + data[0]; + return 2; + } +} + +int opus_packet_get_samples_per_frame(const unsigned char *data, + opus_int32 Fs) +{ + int audiosize; + if (data[0]&0x80) + { + audiosize = ((data[0]>>3)&0x3); + audiosize = (Fs<>3)&0x3); + if (audiosize == 3) + audiosize = Fs*60/1000; + else + audiosize = (Fs< len) + return OPUS_INVALID_PACKET; + data += bytes; + last_size = len-size[0]; + break; + /* Multiple CBR/VBR frames (from 0 to 120 ms) */ + default: /*case 3:*/ + if (len<1) + return OPUS_INVALID_PACKET; + /* Number of frames encoded in bits 0 to 5 */ + ch = *data++; + count = ch&0x3F; + if (count <= 0 || framesize*(opus_int32)count > 5760) + return OPUS_INVALID_PACKET; + len--; + /* Padding flag is bit 6 */ + if (ch&0x40) + { + int p; + do { + int tmp; + if (len<=0) + return OPUS_INVALID_PACKET; + p = *data++; + len--; + tmp = p==255 ? 254: p; + len -= tmp; + pad += tmp; + } while (p==255); + } + if (len<0) + return OPUS_INVALID_PACKET; + /* VBR flag is bit 7 */ + cbr = !(ch&0x80); + if (!cbr) + { + /* VBR case */ + last_size = len; + for (i=0;i len) + return OPUS_INVALID_PACKET; + data += bytes; + last_size -= bytes+size[i]; + } + if (last_size<0) + return OPUS_INVALID_PACKET; + } else if (!self_delimited) + { + /* CBR case */ + last_size = len/count; + if (last_size*count!=len) + return OPUS_INVALID_PACKET; + for (i=0;i len) + return OPUS_INVALID_PACKET; + data += bytes; + /* For CBR packets, apply the size to all the frames. */ + if (cbr) + { + if (size[count-1]*count > len) + return OPUS_INVALID_PACKET; + for (i=0;i last_size) + return OPUS_INVALID_PACKET; + } else + { + /* Because it's not encoded explicitly, it's possible the size of the + last packet (or all the packets, for the CBR case) is larger than + 1275. Reject them here.*/ + if (last_size > 1275) + return OPUS_INVALID_PACKET; + size[count-1] = (opus_int16)last_size; + } + + if (payload_offset) + *payload_offset = (int)(data-data0); + + for (i=0;i= 2) && !defined(__OPTIMIZE__) && !defined(OPUS_WILL_BE_SLOW) +# pragma message "You appear to be compiling without optimization, if so opus will be very slow." +#endif + +#include +#include "celt.h" +#include "opus.h" +#include "entdec.h" +#include "modes.h" +#include "API.h" +#include "stack_alloc.h" +#include "float_cast.h" +#include "opus_private.h" +#include "os_support.h" +#include "structs.h" +#include "define.h" +#include "mathops.h" +#include "cpu_support.h" + +#ifdef ENABLE_DEEP_PLC +#include "dred_rdovae_dec_data.h" +#include "dred_rdovae_dec.h" +#endif + +#ifdef ENABLE_OSCE +#include "osce.h" +#endif + +struct OpusDecoder { + int celt_dec_offset; + int silk_dec_offset; + int channels; + opus_int32 Fs; /** Sampling rate (at the API level) */ + silk_DecControlStruct DecControl; + int decode_gain; + int complexity; + int arch; +#ifdef ENABLE_DEEP_PLC + LPCNetPLCState lpcnet; +#endif + + /* Everything beyond this point gets cleared on a reset */ +#define OPUS_DECODER_RESET_START stream_channels + int stream_channels; + + int bandwidth; + int mode; + int prev_mode; + int frame_size; + int prev_redundancy; + int last_packet_duration; +#ifndef FIXED_POINT + opus_val16 softclip_mem[2]; +#endif + + opus_uint32 rangeFinal; +}; + +#if defined(ENABLE_HARDENING) || defined(ENABLE_ASSERTIONS) +static void validate_opus_decoder(OpusDecoder *st) +{ + celt_assert(st->channels == 1 || st->channels == 2); + celt_assert(st->Fs == 48000 || st->Fs == 24000 || st->Fs == 16000 || st->Fs == 12000 || st->Fs == 8000); + celt_assert(st->DecControl.API_sampleRate == st->Fs); + celt_assert(st->DecControl.internalSampleRate == 0 || st->DecControl.internalSampleRate == 16000 || st->DecControl.internalSampleRate == 12000 || st->DecControl.internalSampleRate == 8000); + celt_assert(st->DecControl.nChannelsAPI == st->channels); + celt_assert(st->DecControl.nChannelsInternal == 0 || st->DecControl.nChannelsInternal == 1 || st->DecControl.nChannelsInternal == 2); + celt_assert(st->DecControl.payloadSize_ms == 0 || st->DecControl.payloadSize_ms == 10 || st->DecControl.payloadSize_ms == 20 || st->DecControl.payloadSize_ms == 40 || st->DecControl.payloadSize_ms == 60); +#ifdef OPUS_ARCHMASK + celt_assert(st->arch >= 0); + celt_assert(st->arch <= OPUS_ARCHMASK); +#endif + celt_assert(st->stream_channels == 1 || st->stream_channels == 2); +} +#define VALIDATE_OPUS_DECODER(st) validate_opus_decoder(st) +#else +#define VALIDATE_OPUS_DECODER(st) +#endif + +int opus_decoder_get_size(int channels) +{ + int silkDecSizeBytes, celtDecSizeBytes; + int ret; + if (channels<1 || channels > 2) + return 0; + ret = silk_Get_Decoder_Size( &silkDecSizeBytes ); + if(ret) + return 0; + silkDecSizeBytes = align(silkDecSizeBytes); + celtDecSizeBytes = celt_decoder_get_size(channels); + return align(sizeof(OpusDecoder))+silkDecSizeBytes+celtDecSizeBytes; +} + +int opus_decoder_init(OpusDecoder *st, opus_int32 Fs, int channels) +{ + void *silk_dec; + CELTDecoder *celt_dec; + int ret, silkDecSizeBytes; + + if ((Fs!=48000&&Fs!=24000&&Fs!=16000&&Fs!=12000&&Fs!=8000) + || (channels!=1&&channels!=2)) + return OPUS_BAD_ARG; + + OPUS_CLEAR((char*)st, opus_decoder_get_size(channels)); + /* Initialize SILK decoder */ + ret = silk_Get_Decoder_Size(&silkDecSizeBytes); + if (ret) + return OPUS_INTERNAL_ERROR; + + silkDecSizeBytes = align(silkDecSizeBytes); + st->silk_dec_offset = align(sizeof(OpusDecoder)); + st->celt_dec_offset = st->silk_dec_offset+silkDecSizeBytes; + silk_dec = (char*)st+st->silk_dec_offset; + celt_dec = (CELTDecoder*)((char*)st+st->celt_dec_offset); + st->stream_channels = st->channels = channels; + st->complexity = 0; + + st->Fs = Fs; + st->DecControl.API_sampleRate = st->Fs; + st->DecControl.nChannelsAPI = st->channels; + + /* Reset decoder */ + ret = silk_InitDecoder( silk_dec ); + if(ret)return OPUS_INTERNAL_ERROR; + + /* Initialize CELT decoder */ + ret = celt_decoder_init(celt_dec, Fs, channels); + if(ret!=OPUS_OK)return OPUS_INTERNAL_ERROR; + + celt_decoder_ctl(celt_dec, CELT_SET_SIGNALLING(0)); + + st->prev_mode = 0; + st->frame_size = Fs/400; +#ifdef ENABLE_DEEP_PLC + lpcnet_plc_init( &st->lpcnet); +#endif + st->arch = opus_select_arch(); + return OPUS_OK; +} + +OpusDecoder *opus_decoder_create(opus_int32 Fs, int channels, int *error) +{ + int ret; + OpusDecoder *st; + if ((Fs!=48000&&Fs!=24000&&Fs!=16000&&Fs!=12000&&Fs!=8000) + || (channels!=1&&channels!=2)) + { + if (error) + *error = OPUS_BAD_ARG; + return NULL; + } + st = (OpusDecoder *)opus_alloc(opus_decoder_get_size(channels)); + if (st == NULL) + { + if (error) + *error = OPUS_ALLOC_FAIL; + return NULL; + } + ret = opus_decoder_init(st, Fs, channels); + if (error) + *error = ret; + if (ret != OPUS_OK) + { + opus_free(st); + st = NULL; + } + return st; +} + +static void smooth_fade(const opus_val16 *in1, const opus_val16 *in2, + opus_val16 *out, int overlap, int channels, + const opus_val16 *window, opus_int32 Fs) +{ + int i, c; + int inc = 48000/Fs; + for (c=0;csilk_dec_offset; + celt_dec = (CELTDecoder*)((char*)st+st->celt_dec_offset); + F20 = st->Fs/50; + F10 = F20>>1; + F5 = F10>>1; + F2_5 = F5>>1; + if (frame_size < F2_5) + { + RESTORE_STACK; + return OPUS_BUFFER_TOO_SMALL; + } + /* Limit frame_size to avoid excessive stack allocations. */ + frame_size = IMIN(frame_size, st->Fs/25*3); + /* Payloads of 1 (2 including ToC) or 0 trigger the PLC/DTX */ + if (len<=1) + { + data = NULL; + /* In that case, don't conceal more than what the ToC says */ + frame_size = IMIN(frame_size, st->frame_size); + } + if (data != NULL) + { + audiosize = st->frame_size; + mode = st->mode; + bandwidth = st->bandwidth; + ec_dec_init(&dec,(unsigned char*)data,len); + } else { + audiosize = frame_size; + /* Run PLC using last used mode (CELT if we ended with CELT redundancy) */ + mode = st->prev_redundancy ? MODE_CELT_ONLY : st->prev_mode; + bandwidth = 0; + + if (mode == 0) + { + /* If we haven't got any packet yet, all we can do is return zeros */ + for (i=0;ichannels;i++) + pcm[i] = 0; + RESTORE_STACK; + return audiosize; + } + + /* Avoids trying to run the PLC on sizes other than 2.5 (CELT), 5 (CELT), + 10, or 20 (e.g. 12.5 or 30 ms). */ + if (audiosize > F20) + { + do { + int ret = opus_decode_frame(st, NULL, 0, pcm, IMIN(audiosize, F20), 0); + if (ret<0) + { + RESTORE_STACK; + return ret; + } + pcm += ret*st->channels; + audiosize -= ret; + } while (audiosize > 0); + RESTORE_STACK; + return frame_size; + } else if (audiosize < F20) + { + if (audiosize > F10) + audiosize = F10; + else if (mode != MODE_SILK_ONLY && audiosize > F5 && audiosize < F10) + audiosize = F5; + } + } + + /* In fixed-point, we can tell CELT to do the accumulation on top of the + SILK PCM buffer. This saves some stack space. */ +#ifdef FIXED_POINT + celt_accum = (mode != MODE_CELT_ONLY) && (frame_size >= F10); +#else + celt_accum = 0; +#endif + + pcm_transition_silk_size = ALLOC_NONE; + pcm_transition_celt_size = ALLOC_NONE; + if (data!=NULL && st->prev_mode > 0 && ( + (mode == MODE_CELT_ONLY && st->prev_mode != MODE_CELT_ONLY && !st->prev_redundancy) + || (mode != MODE_CELT_ONLY && st->prev_mode == MODE_CELT_ONLY) ) + ) + { + transition = 1; + /* Decide where to allocate the stack memory for pcm_transition */ + if (mode == MODE_CELT_ONLY) + pcm_transition_celt_size = F5*st->channels; + else + pcm_transition_silk_size = F5*st->channels; + } + ALLOC(pcm_transition_celt, pcm_transition_celt_size, opus_val16); + if (transition && mode == MODE_CELT_ONLY) + { + pcm_transition = pcm_transition_celt; + opus_decode_frame(st, NULL, 0, pcm_transition, IMIN(F5, audiosize), 0); + } + if (audiosize > frame_size) + { + /*fprintf(stderr, "PCM buffer too small: %d vs %d (mode = %d)\n", audiosize, frame_size, mode);*/ + RESTORE_STACK; + return OPUS_BAD_ARG; + } else { + frame_size = audiosize; + } + + /* Don't allocate any memory when in CELT-only mode */ + pcm_silk_size = (mode != MODE_CELT_ONLY && !celt_accum) ? IMAX(F10, frame_size)*st->channels : ALLOC_NONE; + ALLOC(pcm_silk, pcm_silk_size, opus_int16); + + /* SILK processing */ + if (mode != MODE_CELT_ONLY) + { + int lost_flag, decoded_samples; + opus_int16 *pcm_ptr; +#ifdef FIXED_POINT + if (celt_accum) + pcm_ptr = pcm; + else +#endif + pcm_ptr = pcm_silk; + + if (st->prev_mode==MODE_CELT_ONLY) + silk_ResetDecoder( silk_dec ); + + /* The SILK PLC cannot produce frames of less than 10 ms */ + st->DecControl.payloadSize_ms = IMAX(10, 1000 * audiosize / st->Fs); + + if (data != NULL) + { + st->DecControl.nChannelsInternal = st->stream_channels; + if( mode == MODE_SILK_ONLY ) { + if( bandwidth == OPUS_BANDWIDTH_NARROWBAND ) { + st->DecControl.internalSampleRate = 8000; + } else if( bandwidth == OPUS_BANDWIDTH_MEDIUMBAND ) { + st->DecControl.internalSampleRate = 12000; + } else if( bandwidth == OPUS_BANDWIDTH_WIDEBAND ) { + st->DecControl.internalSampleRate = 16000; + } else { + st->DecControl.internalSampleRate = 16000; + celt_assert( 0 ); + } + } else { + /* Hybrid mode */ + st->DecControl.internalSampleRate = 16000; + } + } + st->DecControl.enable_deep_plc = st->complexity >= 5; +#ifdef ENABLE_OSCE + st->DecControl.osce_method = OSCE_METHOD_NONE; +#ifndef DISABLE_LACE + if (st->complexity >= 6) {st->DecControl.osce_method = OSCE_METHOD_LACE;} +#endif +#ifndef DISABLE_NOLACE + if (st->complexity >= 7) {st->DecControl.osce_method = OSCE_METHOD_NOLACE;} +#endif +#endif + + lost_flag = data == NULL ? 1 : 2 * !!decode_fec; + decoded_samples = 0; + do { + /* Call SILK decoder */ + int first_frame = decoded_samples == 0; + silk_ret = silk_Decode( silk_dec, &st->DecControl, + lost_flag, first_frame, &dec, pcm_ptr, &silk_frame_size, +#ifdef ENABLE_DEEP_PLC + &st->lpcnet, +#endif + st->arch ); + if( silk_ret ) { + if (lost_flag) { + /* PLC failure should not be fatal */ + silk_frame_size = frame_size; + for (i=0;ichannels;i++) + pcm_ptr[i] = 0; + } else { + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } + } + pcm_ptr += silk_frame_size * st->channels; + decoded_samples += silk_frame_size; + } while( decoded_samples < frame_size ); + } + + start_band = 0; + if (!decode_fec && mode != MODE_CELT_ONLY && data != NULL + && ec_tell(&dec)+17+20*(mode == MODE_HYBRID) <= 8*len) + { + /* Check if we have a redundant 0-8 kHz band */ + if (mode == MODE_HYBRID) + redundancy = ec_dec_bit_logp(&dec, 12); + else + redundancy = 1; + if (redundancy) + { + celt_to_silk = ec_dec_bit_logp(&dec, 1); + /* redundancy_bytes will be at least two, in the non-hybrid + case due to the ec_tell() check above */ + redundancy_bytes = mode==MODE_HYBRID ? + (opus_int32)ec_dec_uint(&dec, 256)+2 : + len-((ec_tell(&dec)+7)>>3); + len -= redundancy_bytes; + /* This is a sanity check. It should never happen for a valid + packet, so the exact behaviour is not normative. */ + if (len*8 < ec_tell(&dec)) + { + len = 0; + redundancy_bytes = 0; + redundancy = 0; + } + /* Shrink decoder because of raw bits */ + dec.storage -= redundancy_bytes; + } + } + if (mode != MODE_CELT_ONLY) + start_band = 17; + + if (redundancy) + { + transition = 0; + pcm_transition_silk_size=ALLOC_NONE; + } + + ALLOC(pcm_transition_silk, pcm_transition_silk_size, opus_val16); + + if (transition && mode != MODE_CELT_ONLY) + { + pcm_transition = pcm_transition_silk; + opus_decode_frame(st, NULL, 0, pcm_transition, IMIN(F5, audiosize), 0); + } + + + if (bandwidth) + { + int endband=21; + + switch(bandwidth) + { + case OPUS_BANDWIDTH_NARROWBAND: + endband = 13; + break; + case OPUS_BANDWIDTH_MEDIUMBAND: + case OPUS_BANDWIDTH_WIDEBAND: + endband = 17; + break; + case OPUS_BANDWIDTH_SUPERWIDEBAND: + endband = 19; + break; + case OPUS_BANDWIDTH_FULLBAND: + endband = 21; + break; + default: + celt_assert(0); + break; + } + MUST_SUCCEED(celt_decoder_ctl(celt_dec, CELT_SET_END_BAND(endband))); + } + MUST_SUCCEED(celt_decoder_ctl(celt_dec, CELT_SET_CHANNELS(st->stream_channels))); + + /* Only allocation memory for redundancy if/when needed */ + redundant_audio_size = redundancy ? F5*st->channels : ALLOC_NONE; + ALLOC(redundant_audio, redundant_audio_size, opus_val16); + + /* 5 ms redundant frame for CELT->SILK*/ + if (redundancy && celt_to_silk) + { + /* If the previous frame did not use CELT (the first redundancy frame in + a transition from SILK may have been lost) then the CELT decoder is + stale at this point and the redundancy audio is not useful, however + the final range is still needed (for testing), so the redundancy is + always decoded but the decoded audio may not be used */ + MUST_SUCCEED(celt_decoder_ctl(celt_dec, CELT_SET_START_BAND(0))); + celt_decode_with_ec(celt_dec, data+len, redundancy_bytes, + redundant_audio, F5, NULL, 0); + MUST_SUCCEED(celt_decoder_ctl(celt_dec, OPUS_GET_FINAL_RANGE(&redundant_rng))); + } + + /* MUST be after PLC */ + MUST_SUCCEED(celt_decoder_ctl(celt_dec, CELT_SET_START_BAND(start_band))); + + if (mode != MODE_SILK_ONLY) + { + int celt_frame_size = IMIN(F20, frame_size); + /* Make sure to discard any previous CELT state */ + if (mode != st->prev_mode && st->prev_mode > 0 && !st->prev_redundancy) + MUST_SUCCEED(celt_decoder_ctl(celt_dec, OPUS_RESET_STATE)); + /* Decode CELT */ + celt_ret = celt_decode_with_ec_dred(celt_dec, decode_fec ? NULL : data, + len, pcm, celt_frame_size, &dec, celt_accum +#ifdef ENABLE_DEEP_PLC + , &st->lpcnet +#endif + ); + } else { + unsigned char silence[2] = {0xFF, 0xFF}; + if (!celt_accum) + { + for (i=0;ichannels;i++) + pcm[i] = 0; + } + /* For hybrid -> SILK transitions, we let the CELT MDCT + do a fade-out by decoding a silence frame */ + if (st->prev_mode == MODE_HYBRID && !(redundancy && celt_to_silk && st->prev_redundancy) ) + { + MUST_SUCCEED(celt_decoder_ctl(celt_dec, CELT_SET_START_BAND(0))); + celt_decode_with_ec(celt_dec, silence, 2, pcm, F2_5, NULL, celt_accum); + } + } + + if (mode != MODE_CELT_ONLY && !celt_accum) + { +#ifdef FIXED_POINT + for (i=0;ichannels;i++) + pcm[i] = SAT16(ADD32(pcm[i], pcm_silk[i])); +#else + for (i=0;ichannels;i++) + pcm[i] = pcm[i] + (opus_val16)((1.f/32768.f)*pcm_silk[i]); +#endif + } + + { + const CELTMode *celt_mode; + MUST_SUCCEED(celt_decoder_ctl(celt_dec, CELT_GET_MODE(&celt_mode))); + window = celt_mode->window; + } + + /* 5 ms redundant frame for SILK->CELT */ + if (redundancy && !celt_to_silk) + { + MUST_SUCCEED(celt_decoder_ctl(celt_dec, OPUS_RESET_STATE)); + MUST_SUCCEED(celt_decoder_ctl(celt_dec, CELT_SET_START_BAND(0))); + + celt_decode_with_ec(celt_dec, data+len, redundancy_bytes, redundant_audio, F5, NULL, 0); + MUST_SUCCEED(celt_decoder_ctl(celt_dec, OPUS_GET_FINAL_RANGE(&redundant_rng))); + smooth_fade(pcm+st->channels*(frame_size-F2_5), redundant_audio+st->channels*F2_5, + pcm+st->channels*(frame_size-F2_5), F2_5, st->channels, window, st->Fs); + } + /* 5ms redundant frame for CELT->SILK; ignore if the previous frame did not + use CELT (the first redundancy frame in a transition from SILK may have + been lost) */ + if (redundancy && celt_to_silk && (st->prev_mode != MODE_SILK_ONLY || st->prev_redundancy)) + { + for (c=0;cchannels;c++) + { + for (i=0;ichannels*i+c] = redundant_audio[st->channels*i+c]; + } + smooth_fade(redundant_audio+st->channels*F2_5, pcm+st->channels*F2_5, + pcm+st->channels*F2_5, F2_5, st->channels, window, st->Fs); + } + if (transition) + { + if (audiosize >= F5) + { + for (i=0;ichannels*F2_5;i++) + pcm[i] = pcm_transition[i]; + smooth_fade(pcm_transition+st->channels*F2_5, pcm+st->channels*F2_5, + pcm+st->channels*F2_5, F2_5, + st->channels, window, st->Fs); + } else { + /* Not enough time to do a clean transition, but we do it anyway + This will not preserve amplitude perfectly and may introduce + a bit of temporal aliasing, but it shouldn't be too bad and + that's pretty much the best we can do. In any case, generating this + transition it pretty silly in the first place */ + smooth_fade(pcm_transition, pcm, + pcm, F2_5, + st->channels, window, st->Fs); + } + } + + if(st->decode_gain) + { + opus_val32 gain; + gain = celt_exp2(MULT16_16_P15(QCONST16(6.48814081e-4f, 25), st->decode_gain)); + for (i=0;ichannels;i++) + { + opus_val32 x; + x = MULT16_32_P16(pcm[i],gain); + pcm[i] = SATURATE(x, 32767); + } + } + + if (len <= 1) + st->rangeFinal = 0; + else + st->rangeFinal = dec.rng ^ redundant_rng; + + st->prev_mode = mode; + st->prev_redundancy = redundancy && !celt_to_silk; + + if (celt_ret>=0) + { + if (OPUS_CHECK_ARRAY(pcm, audiosize*st->channels)) + OPUS_PRINT_INT(audiosize); + } + + RESTORE_STACK; + return celt_ret < 0 ? celt_ret : audiosize; + +} + +int opus_decode_native(OpusDecoder *st, const unsigned char *data, + opus_int32 len, opus_val16 *pcm, int frame_size, int decode_fec, + int self_delimited, opus_int32 *packet_offset, int soft_clip, const OpusDRED *dred, opus_int32 dred_offset) +{ + int i, nb_samples; + int count, offset; + unsigned char toc; + int packet_frame_size, packet_bandwidth, packet_mode, packet_stream_channels; + /* 48 x 2.5 ms = 120 ms */ + opus_int16 size[48]; + VALIDATE_OPUS_DECODER(st); + if (decode_fec<0 || decode_fec>1) + return OPUS_BAD_ARG; + /* For FEC/PLC, frame_size has to be to have a multiple of 2.5 ms */ + if ((decode_fec || len==0 || data==NULL) && frame_size%(st->Fs/400)!=0) + return OPUS_BAD_ARG; +#ifdef ENABLE_DRED + if (dred != NULL && dred->process_stage == 2) { + int F10; + int features_per_frame; + int needed_feature_frames; + int init_frames; + lpcnet_plc_fec_clear(&st->lpcnet); + F10 = st->Fs/100; + /* if blend==0, the last PLC call was "update" and we need to feed two extra 10-ms frames. */ + init_frames = (st->lpcnet.blend == 0) ? 2 : 0; + features_per_frame = IMAX(1, frame_size/F10); + needed_feature_frames = init_frames + features_per_frame; + lpcnet_plc_fec_clear(&st->lpcnet); + for (i=0;idred_offset*F10/4)/F10); + if (feature_offset <= 4*dred->nb_latents-1 && feature_offset >= 0) { + lpcnet_plc_fec_add(&st->lpcnet, dred->fec_features+feature_offset*DRED_NUM_FEATURES); + } else { + if (feature_offset >= 0) lpcnet_plc_fec_add(&st->lpcnet, NULL); + } + + } + } +#else + (void)dred; + (void)dred_offset; +#endif + if (len==0 || data==NULL) + { + int pcm_count=0; + do { + int ret; + ret = opus_decode_frame(st, NULL, 0, pcm+pcm_count*st->channels, frame_size-pcm_count, 0); + if (ret<0) + return ret; + pcm_count += ret; + } while (pcm_count < frame_size); + celt_assert(pcm_count == frame_size); + if (OPUS_CHECK_ARRAY(pcm, pcm_count*st->channels)) + OPUS_PRINT_INT(pcm_count); + st->last_packet_duration = pcm_count; + return pcm_count; + } else if (len<0) + return OPUS_BAD_ARG; + + packet_mode = opus_packet_get_mode(data); + packet_bandwidth = opus_packet_get_bandwidth(data); + packet_frame_size = opus_packet_get_samples_per_frame(data, st->Fs); + packet_stream_channels = opus_packet_get_nb_channels(data); + + count = opus_packet_parse_impl(data, len, self_delimited, &toc, NULL, + size, &offset, packet_offset, NULL, NULL); + if (count<0) + return count; + + data += offset; + + if (decode_fec) + { + int duration_copy; + int ret; + /* If no FEC can be present, run the PLC (recursive call) */ + if (frame_size < packet_frame_size || packet_mode == MODE_CELT_ONLY || st->mode == MODE_CELT_ONLY) + return opus_decode_native(st, NULL, 0, pcm, frame_size, 0, 0, NULL, soft_clip, NULL, 0); + /* Otherwise, run the PLC on everything except the size for which we might have FEC */ + duration_copy = st->last_packet_duration; + if (frame_size-packet_frame_size!=0) + { + ret = opus_decode_native(st, NULL, 0, pcm, frame_size-packet_frame_size, 0, 0, NULL, soft_clip, NULL, 0); + if (ret<0) + { + st->last_packet_duration = duration_copy; + return ret; + } + celt_assert(ret==frame_size-packet_frame_size); + } + /* Complete with FEC */ + st->mode = packet_mode; + st->bandwidth = packet_bandwidth; + st->frame_size = packet_frame_size; + st->stream_channels = packet_stream_channels; + ret = opus_decode_frame(st, data, size[0], pcm+st->channels*(frame_size-packet_frame_size), + packet_frame_size, 1); + if (ret<0) + return ret; + else { + if (OPUS_CHECK_ARRAY(pcm, frame_size*st->channels)) + OPUS_PRINT_INT(frame_size); + st->last_packet_duration = frame_size; + return frame_size; + } + } + + if (count*packet_frame_size > frame_size) + return OPUS_BUFFER_TOO_SMALL; + + /* Update the state as the last step to avoid updating it on an invalid packet */ + st->mode = packet_mode; + st->bandwidth = packet_bandwidth; + st->frame_size = packet_frame_size; + st->stream_channels = packet_stream_channels; + + nb_samples=0; + for (i=0;ichannels, frame_size-nb_samples, 0); + if (ret<0) + return ret; + celt_assert(ret==packet_frame_size); + data += size[i]; + nb_samples += ret; + } + st->last_packet_duration = nb_samples; + if (OPUS_CHECK_ARRAY(pcm, nb_samples*st->channels)) + OPUS_PRINT_INT(nb_samples); +#ifndef FIXED_POINT + if (soft_clip) + opus_pcm_soft_clip(pcm, nb_samples, st->channels, st->softclip_mem); + else + st->softclip_mem[0]=st->softclip_mem[1]=0; +#endif + return nb_samples; +} + +#ifdef FIXED_POINT + +int opus_decode(OpusDecoder *st, const unsigned char *data, + opus_int32 len, opus_val16 *pcm, int frame_size, int decode_fec) +{ + if(frame_size<=0) + return OPUS_BAD_ARG; + return opus_decode_native(st, data, len, pcm, frame_size, decode_fec, 0, NULL, 0, NULL, 0); +} + +#ifndef DISABLE_FLOAT_API +int opus_decode_float(OpusDecoder *st, const unsigned char *data, + opus_int32 len, float *pcm, int frame_size, int decode_fec) +{ + VARDECL(opus_int16, out); + int ret, i; + int nb_samples; + ALLOC_STACK; + + if(frame_size<=0) + { + RESTORE_STACK; + return OPUS_BAD_ARG; + } + if (data != NULL && len > 0 && !decode_fec) + { + nb_samples = opus_decoder_get_nb_samples(st, data, len); + if (nb_samples>0) + frame_size = IMIN(frame_size, nb_samples); + else + return OPUS_INVALID_PACKET; + } + celt_assert(st->channels == 1 || st->channels == 2); + ALLOC(out, frame_size*st->channels, opus_int16); + + ret = opus_decode_native(st, data, len, out, frame_size, decode_fec, 0, NULL, 0, NULL, 0); + if (ret > 0) + { + for (i=0;ichannels;i++) + pcm[i] = (1.f/32768.f)*(out[i]); + } + RESTORE_STACK; + return ret; +} +#endif + + +#else +int opus_decode(OpusDecoder *st, const unsigned char *data, + opus_int32 len, opus_int16 *pcm, int frame_size, int decode_fec) +{ + VARDECL(float, out); + int ret, i; + int nb_samples; + ALLOC_STACK; + + if(frame_size<=0) + { + RESTORE_STACK; + return OPUS_BAD_ARG; + } + + if (data != NULL && len > 0 && !decode_fec) + { + nb_samples = opus_decoder_get_nb_samples(st, data, len); + if (nb_samples>0) + frame_size = IMIN(frame_size, nb_samples); + else + return OPUS_INVALID_PACKET; + } + celt_assert(st->channels == 1 || st->channels == 2); + ALLOC(out, frame_size*st->channels, float); + + ret = opus_decode_native(st, data, len, out, frame_size, decode_fec, 0, NULL, 1, NULL, 0); + if (ret > 0) + { + for (i=0;ichannels;i++) + pcm[i] = FLOAT2INT16(out[i]); + } + RESTORE_STACK; + return ret; +} + +int opus_decode_float(OpusDecoder *st, const unsigned char *data, + opus_int32 len, opus_val16 *pcm, int frame_size, int decode_fec) +{ + if(frame_size<=0) + return OPUS_BAD_ARG; + return opus_decode_native(st, data, len, pcm, frame_size, decode_fec, 0, NULL, 0, NULL, 0); +} + +#endif + +int opus_decoder_ctl(OpusDecoder *st, int request, ...) +{ + int ret = OPUS_OK; + va_list ap; + void *silk_dec; + CELTDecoder *celt_dec; + + silk_dec = (char*)st+st->silk_dec_offset; + celt_dec = (CELTDecoder*)((char*)st+st->celt_dec_offset); + + + va_start(ap, request); + + switch (request) + { + case OPUS_GET_BANDWIDTH_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->bandwidth; + } + break; + case OPUS_SET_COMPLEXITY_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>10) + { + goto bad_arg; + } + st->complexity = value; + celt_decoder_ctl(celt_dec, OPUS_SET_COMPLEXITY(value)); + } + break; + case OPUS_GET_COMPLEXITY_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->complexity; + } + break; + case OPUS_GET_FINAL_RANGE_REQUEST: + { + opus_uint32 *value = va_arg(ap, opus_uint32*); + if (!value) + { + goto bad_arg; + } + *value = st->rangeFinal; + } + break; + case OPUS_RESET_STATE: + { + OPUS_CLEAR((char*)&st->OPUS_DECODER_RESET_START, + sizeof(OpusDecoder)- + ((char*)&st->OPUS_DECODER_RESET_START - (char*)st)); + + celt_decoder_ctl(celt_dec, OPUS_RESET_STATE); + silk_ResetDecoder( silk_dec ); + st->stream_channels = st->channels; + st->frame_size = st->Fs/400; +#ifdef ENABLE_DEEP_PLC + lpcnet_plc_reset( &st->lpcnet ); +#endif + } + break; + case OPUS_GET_SAMPLE_RATE_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->Fs; + } + break; + case OPUS_GET_PITCH_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + if (st->prev_mode == MODE_CELT_ONLY) + ret = celt_decoder_ctl(celt_dec, OPUS_GET_PITCH(value)); + else + *value = st->DecControl.prevPitchLag; + } + break; + case OPUS_GET_GAIN_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->decode_gain; + } + break; + case OPUS_SET_GAIN_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<-32768 || value>32767) + { + goto bad_arg; + } + st->decode_gain = value; + } + break; + case OPUS_GET_LAST_PACKET_DURATION_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->last_packet_duration; + } + break; + case OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>1) + { + goto bad_arg; + } + ret = celt_decoder_ctl(celt_dec, OPUS_SET_PHASE_INVERSION_DISABLED(value)); + } + break; + case OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + ret = celt_decoder_ctl(celt_dec, OPUS_GET_PHASE_INVERSION_DISABLED(value)); + } + break; +#ifdef USE_WEIGHTS_FILE + case OPUS_SET_DNN_BLOB_REQUEST: + { + const unsigned char *data = va_arg(ap, const unsigned char *); + opus_int32 len = va_arg(ap, opus_int32); + if(len<0 || data == NULL) + { + goto bad_arg; + } + ret = lpcnet_plc_load_model(&st->lpcnet, data, len); + ret = silk_LoadOSCEModels(silk_dec, data, len) || ret; + } + break; +#endif + default: + /*fprintf(stderr, "unknown opus_decoder_ctl() request: %d", request);*/ + ret = OPUS_UNIMPLEMENTED; + break; + } + + va_end(ap); + return ret; +bad_arg: + va_end(ap); + return OPUS_BAD_ARG; +} + +void opus_decoder_destroy(OpusDecoder *st) +{ + opus_free(st); +} + + +int opus_packet_get_bandwidth(const unsigned char *data) +{ + int bandwidth; + if (data[0]&0x80) + { + bandwidth = OPUS_BANDWIDTH_MEDIUMBAND + ((data[0]>>5)&0x3); + if (bandwidth == OPUS_BANDWIDTH_MEDIUMBAND) + bandwidth = OPUS_BANDWIDTH_NARROWBAND; + } else if ((data[0]&0x60) == 0x60) + { + bandwidth = (data[0]&0x10) ? OPUS_BANDWIDTH_FULLBAND : + OPUS_BANDWIDTH_SUPERWIDEBAND; + } else { + bandwidth = OPUS_BANDWIDTH_NARROWBAND + ((data[0]>>5)&0x3); + } + return bandwidth; +} + +int opus_packet_get_nb_channels(const unsigned char *data) +{ + return (data[0]&0x4) ? 2 : 1; +} + +int opus_packet_get_nb_frames(const unsigned char packet[], opus_int32 len) +{ + int count; + if (len<1) + return OPUS_BAD_ARG; + count = packet[0]&0x3; + if (count==0) + return 1; + else if (count!=3) + return 2; + else if (len<2) + return OPUS_INVALID_PACKET; + else + return packet[1]&0x3F; +} + +int opus_packet_get_nb_samples(const unsigned char packet[], opus_int32 len, + opus_int32 Fs) +{ + int samples; + int count = opus_packet_get_nb_frames(packet, len); + + if (count<0) + return count; + + samples = count*opus_packet_get_samples_per_frame(packet, Fs); + /* Can't have more than 120 ms */ + if (samples*25 > Fs*3) + return OPUS_INVALID_PACKET; + else + return samples; +} + +int opus_packet_has_lbrr(const unsigned char packet[], opus_int32 len) +{ + int ret; + const unsigned char *frames[48]; + opus_int16 size[48]; + int packet_mode, packet_frame_size, packet_stream_channels; + int nb_frames=1; + int lbrr; + + packet_mode = opus_packet_get_mode(packet); + if (packet_mode == MODE_CELT_ONLY) + return 0; + packet_frame_size = opus_packet_get_samples_per_frame(packet, 48000); + if (packet_frame_size > 960) + nb_frames = packet_frame_size/960; + packet_stream_channels = opus_packet_get_nb_channels(packet); + ret = opus_packet_parse(packet, len, NULL, frames, size, NULL); + if (ret <= 0) + return ret; + lbrr = (frames[0][0] >> (7-nb_frames)) & 0x1; + if (packet_stream_channels == 2) + lbrr = lbrr || ((frames[0][0] >> (6-2*nb_frames)) & 0x1); + return lbrr; +} + +int opus_decoder_get_nb_samples(const OpusDecoder *dec, + const unsigned char packet[], opus_int32 len) +{ + return opus_packet_get_nb_samples(packet, len, dec->Fs); +} + +struct OpusDREDDecoder { +#ifdef ENABLE_DRED + RDOVAEDec model; +#endif + int loaded; + int arch; + opus_uint32 magic; +}; + +#if defined(ENABLE_DRED) && (defined(ENABLE_HARDENING) || defined(ENABLE_ASSERTIONS)) +static void validate_dred_decoder(OpusDREDDecoder *st) +{ + celt_assert(st->magic == 0xD8EDDEC0); +#ifdef OPUS_ARCHMASK + celt_assert(st->arch >= 0); + celt_assert(st->arch <= OPUS_ARCHMASK); +#endif +} +#define VALIDATE_DRED_DECODER(st) validate_dred_decoder(st) +#else +#define VALIDATE_DRED_DECODER(st) +#endif + + +int opus_dred_decoder_get_size(void) +{ + return sizeof(OpusDREDDecoder); +} + +#ifdef ENABLE_DRED +int dred_decoder_load_model(OpusDREDDecoder *dec, const unsigned char *data, int len) +{ + WeightArray *list; + int ret; + parse_weights(&list, data, len); + ret = init_rdovaedec(&dec->model, list); + opus_free(list); + if (ret == 0) dec->loaded = 1; + return (ret == 0) ? OPUS_OK : OPUS_BAD_ARG; +} +#endif + +int opus_dred_decoder_init(OpusDREDDecoder *dec) +{ + int ret = 0; + dec->loaded = 0; +#if defined(ENABLE_DRED) && !defined(USE_WEIGHTS_FILE) + ret = init_rdovaedec(&dec->model, rdovaedec_arrays); + if (ret == 0) dec->loaded = 1; +#endif + dec->arch = opus_select_arch(); + /* To make sure nobody forgets to init, use a magic number. */ + dec->magic = 0xD8EDDEC0; + return (ret == 0) ? OPUS_OK : OPUS_UNIMPLEMENTED; +} + +OpusDREDDecoder *opus_dred_decoder_create(int *error) +{ + int ret; + OpusDREDDecoder *dec; + dec = (OpusDREDDecoder *)opus_alloc(opus_dred_decoder_get_size()); + if (dec == NULL) + { + if (error) + *error = OPUS_ALLOC_FAIL; + return NULL; + } + ret = opus_dred_decoder_init(dec); + if (error) + *error = ret; + if (ret != OPUS_OK) + { + opus_free(dec); + dec = NULL; + } + return dec; +} + +void opus_dred_decoder_destroy(OpusDREDDecoder *dec) +{ + if (dec) dec->magic = 0xDE57801D; + opus_free(dec); +} + +int opus_dred_decoder_ctl(OpusDREDDecoder *dred_dec, int request, ...) +{ +#ifdef ENABLE_DRED + int ret = OPUS_OK; + va_list ap; + + va_start(ap, request); + (void)dred_dec; + switch (request) + { +# ifdef USE_WEIGHTS_FILE + case OPUS_SET_DNN_BLOB_REQUEST: + { + const unsigned char *data = va_arg(ap, const unsigned char *); + opus_int32 len = va_arg(ap, opus_int32); + if(len<0 || data == NULL) + { + goto bad_arg; + } + return dred_decoder_load_model(dred_dec, data, len); + } + break; +# endif + default: + /*fprintf(stderr, "unknown opus_decoder_ctl() request: %d", request);*/ + ret = OPUS_UNIMPLEMENTED; + break; + } + va_end(ap); + return ret; +# ifdef USE_WEIGHTS_FILE +bad_arg: + va_end(ap); + return OPUS_BAD_ARG; +# endif +#else + (void)dred_dec; + (void)request; + return OPUS_UNIMPLEMENTED; +#endif +} + +#ifdef ENABLE_DRED +static int dred_find_payload(const unsigned char *data, opus_int32 len, const unsigned char **payload, int *dred_frame_offset) +{ + const unsigned char *data0; + int len0; + int frame = 0; + int ret; + const unsigned char *frames[48]; + opus_int16 size[48]; + int frame_size; + + *payload = NULL; + /* Get the padding section of the packet. */ + ret = opus_packet_parse_impl(data, len, 0, NULL, frames, size, NULL, NULL, &data0, &len0); + if (ret < 0) + return ret; + frame_size = opus_packet_get_samples_per_frame(data, 48000); + data = data0; + len = len0; + /* Scan extensions in order until we find the earliest frame with DRED data. */ + while (len > 0) + { + opus_int32 header_size; + int id, L; + len0 = len; + data0 = data; + id = *data0 >> 1; + L = *data0 & 0x1; + len = skip_extension(&data, len, &header_size); + if (len < 0) + break; + if (id == 1) + { + if (L==0) + { + frame++; + } else { + frame += data0[1]; + } + } else if (id == DRED_EXTENSION_ID) + { + const unsigned char *curr_payload; + opus_int32 curr_payload_len; + curr_payload = data0+header_size; + curr_payload_len = (data-data0)-header_size; + /* DRED position in the packet, in units of 2.5 ms like for the signaled DRED offset. */ + *dred_frame_offset = frame*frame_size/120; +#ifdef DRED_EXPERIMENTAL_VERSION + /* Check that temporary extension type and version match. + This check will be removed once extension is finalized. */ + if (curr_payload_len > DRED_EXPERIMENTAL_BYTES && curr_payload[0] == 'D' && curr_payload[1] == DRED_EXPERIMENTAL_VERSION) { + *payload = curr_payload+2; + return curr_payload_len-2; + } +#else + if (curr_payload_len > 0) { + *payload = curr_payload; + return curr_payload_len; + } +#endif + } + } + return 0; +} +#endif + +int opus_dred_get_size(void) +{ +#ifdef ENABLE_DRED + return sizeof(OpusDRED); +#else + return 0; +#endif +} + +OpusDRED *opus_dred_alloc(int *error) +{ +#ifdef ENABLE_DRED + OpusDRED *dec; + dec = (OpusDRED *)opus_alloc(opus_dred_get_size()); + if (dec == NULL) + { + if (error) + *error = OPUS_ALLOC_FAIL; + return NULL; + } + return dec; +#else + if (error) + *error = OPUS_UNIMPLEMENTED; + return NULL; +#endif +} + +void opus_dred_free(OpusDRED *dec) +{ +#ifdef ENABLE_DRED + opus_free(dec); +#else + (void)dec; +#endif +} + +int opus_dred_parse(OpusDREDDecoder *dred_dec, OpusDRED *dred, const unsigned char *data, opus_int32 len, opus_int32 max_dred_samples, opus_int32 sampling_rate, int *dred_end, int defer_processing) +{ +#ifdef ENABLE_DRED + const unsigned char *payload; + opus_int32 payload_len; + int dred_frame_offset=0; + VALIDATE_DRED_DECODER(dred_dec); + if (!dred_dec->loaded) return OPUS_UNIMPLEMENTED; + dred->process_stage = -1; + payload_len = dred_find_payload(data, len, &payload, &dred_frame_offset); + if (payload_len < 0) + return payload_len; + if (payload != NULL) + { + int offset; + int min_feature_frames; + offset = 100*max_dred_samples/sampling_rate; + min_feature_frames = IMIN(2 + offset, 2*DRED_NUM_REDUNDANCY_FRAMES); + dred_ec_decode(dred, payload, payload_len, min_feature_frames, dred_frame_offset); + if (!defer_processing) + opus_dred_process(dred_dec, dred, dred); + if (dred_end) *dred_end = IMAX(0, -dred->dred_offset*sampling_rate/400); + return IMAX(0, dred->nb_latents*sampling_rate/25 - dred->dred_offset* sampling_rate/400); + } + if (dred_end) *dred_end = 0; + return 0; +#else + (void)dred_dec; + (void)dred; + (void)data; + (void)len; + (void)max_dred_samples; + (void)sampling_rate; + (void)defer_processing; + (void)dred_end; + return OPUS_UNIMPLEMENTED; +#endif +} + +int opus_dred_process(OpusDREDDecoder *dred_dec, const OpusDRED *src, OpusDRED *dst) +{ +#ifdef ENABLE_DRED + if (dred_dec == NULL || src == NULL || dst == NULL || (src->process_stage != 1 && src->process_stage != 2)) + return OPUS_BAD_ARG; + VALIDATE_DRED_DECODER(dred_dec); + if (!dred_dec->loaded) return OPUS_UNIMPLEMENTED; + if (src != dst) + OPUS_COPY(dst, src, 1); + if (dst->process_stage == 2) + return OPUS_OK; + DRED_rdovae_decode_all(&dred_dec->model, dst->fec_features, dst->state, dst->latents, dst->nb_latents, dred_dec->arch); + dst->process_stage = 2; + return OPUS_OK; +#else + (void)dred_dec; + (void)src; + (void)dst; + return OPUS_UNIMPLEMENTED; +#endif +} + +int opus_decoder_dred_decode(OpusDecoder *st, const OpusDRED *dred, opus_int32 dred_offset, opus_int16 *pcm, opus_int32 frame_size) +{ +#ifdef ENABLE_DRED + VARDECL(float, out); + int ret, i; + ALLOC_STACK; + + if(frame_size<=0) + { + RESTORE_STACK; + return OPUS_BAD_ARG; + } + + celt_assert(st->channels == 1 || st->channels == 2); + ALLOC(out, frame_size*st->channels, float); + + ret = opus_decode_native(st, NULL, 0, out, frame_size, 0, 0, NULL, 1, dred, dred_offset); + if (ret > 0) + { + for (i=0;ichannels;i++) + pcm[i] = FLOAT2INT16(out[i]); + } + RESTORE_STACK; + return ret; +#else + (void)st; + (void)dred; + (void)dred_offset; + (void)pcm; + (void)frame_size; + return OPUS_UNIMPLEMENTED; +#endif +} + +int opus_decoder_dred_decode_float(OpusDecoder *st, const OpusDRED *dred, opus_int32 dred_offset, float *pcm, opus_int32 frame_size) +{ +#ifdef ENABLE_DRED + if(frame_size<=0) + return OPUS_BAD_ARG; + return opus_decode_native(st, NULL, 0, pcm, frame_size, 0, 0, NULL, 0, dred, dred_offset); +#else + (void)st; + (void)dred; + (void)dred_offset; + (void)pcm; + (void)frame_size; + return OPUS_UNIMPLEMENTED; +#endif +} diff --git a/port/third_party/opus/src/opus_encoder.c b/port/third_party/opus/src/opus_encoder.c new file mode 100644 index 00000000..d18d582f --- /dev/null +++ b/port/third_party/opus/src/opus_encoder.c @@ -0,0 +1,3089 @@ +/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited + Written by Jean-Marc Valin and Koen Vos */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "celt.h" +#include "entenc.h" +#include "modes.h" +#include "API.h" +#include "stack_alloc.h" +#include "float_cast.h" +#include "opus.h" +#include "arch.h" +#include "pitch.h" +#include "opus_private.h" +#include "os_support.h" +#include "cpu_support.h" +#include "analysis.h" +#include "mathops.h" +#include "tuning_parameters.h" + +#ifdef ENABLE_DRED +#include "dred_coding.h" +#endif + +#ifdef FIXED_POINT +#include "fixed/structs_FIX.h" +#else +#include "float/structs_FLP.h" +#endif +#ifdef ENABLE_OSCE_TRAINING_DATA +#include +#endif + +#define MAX_ENCODER_BUFFER 480 + +#ifndef DISABLE_FLOAT_API +#define PSEUDO_SNR_THRESHOLD 316.23f /* 10^(25/10) */ +#endif + +typedef struct { + opus_val32 XX, XY, YY; + opus_val16 smoothed_width; + opus_val16 max_follower; +} StereoWidthState; + +struct OpusEncoder { + int celt_enc_offset; + int silk_enc_offset; + silk_EncControlStruct silk_mode; +#ifdef ENABLE_DRED + DREDEnc dred_encoder; +#endif + int application; + int channels; + int delay_compensation; + int force_channels; + int signal_type; + int user_bandwidth; + int max_bandwidth; + int user_forced_mode; + int voice_ratio; + opus_int32 Fs; + int use_vbr; + int vbr_constraint; + int variable_duration; + opus_int32 bitrate_bps; + opus_int32 user_bitrate_bps; + int lsb_depth; + int encoder_buffer; + int lfe; + int arch; + int use_dtx; /* general DTX for both SILK and CELT */ + int fec_config; +#ifndef DISABLE_FLOAT_API + TonalityAnalysisState analysis; +#endif + +#define OPUS_ENCODER_RESET_START stream_channels + int stream_channels; + opus_int16 hybrid_stereo_width_Q14; + opus_int32 variable_HP_smth2_Q15; + opus_val16 prev_HB_gain; + opus_val32 hp_mem[4]; + int mode; + int prev_mode; + int prev_channels; + int prev_framesize; + int bandwidth; + /* Bandwidth determined automatically from the rate (before any other adjustment) */ + int auto_bandwidth; + int silk_bw_switch; + /* Sampling rate (at the API level) */ + int first; + opus_val16 * energy_masking; + StereoWidthState width_mem; + opus_val16 delay_buffer[MAX_ENCODER_BUFFER*2]; +#ifndef DISABLE_FLOAT_API + int detected_bandwidth; + int nb_no_activity_ms_Q1; + opus_val32 peak_signal_energy; +#endif +#ifdef ENABLE_DRED + int dred_duration; + int dred_q0; + int dred_dQ; + int dred_qmax; + int dred_target_chunks; + unsigned char activity_mem[DRED_MAX_FRAMES*4]; /* 2.5ms resolution*/ +#endif + int nonfinal_frame; /* current frame is not the final in a packet */ + opus_uint32 rangeFinal; +}; + +/* Transition tables for the voice and music. First column is the + middle (memoriless) threshold. The second column is the hysteresis + (difference with the middle) */ +static const opus_int32 mono_voice_bandwidth_thresholds[8] = { + 9000, 700, /* NB<->MB */ + 9000, 700, /* MB<->WB */ + 13500, 1000, /* WB<->SWB */ + 14000, 2000, /* SWB<->FB */ +}; +static const opus_int32 mono_music_bandwidth_thresholds[8] = { + 9000, 700, /* NB<->MB */ + 9000, 700, /* MB<->WB */ + 11000, 1000, /* WB<->SWB */ + 12000, 2000, /* SWB<->FB */ +}; +static const opus_int32 stereo_voice_bandwidth_thresholds[8] = { + 9000, 700, /* NB<->MB */ + 9000, 700, /* MB<->WB */ + 13500, 1000, /* WB<->SWB */ + 14000, 2000, /* SWB<->FB */ +}; +static const opus_int32 stereo_music_bandwidth_thresholds[8] = { + 9000, 700, /* NB<->MB */ + 9000, 700, /* MB<->WB */ + 11000, 1000, /* WB<->SWB */ + 12000, 2000, /* SWB<->FB */ +}; +/* Threshold bit-rates for switching between mono and stereo */ +static const opus_int32 stereo_voice_threshold = 19000; +static const opus_int32 stereo_music_threshold = 17000; + +/* Threshold bit-rate for switching between SILK/hybrid and CELT-only */ +static const opus_int32 mode_thresholds[2][2] = { + /* voice */ /* music */ + { 64000, 10000}, /* mono */ + { 44000, 10000}, /* stereo */ +}; + +static const opus_int32 fec_thresholds[] = { + 12000, 1000, /* NB */ + 14000, 1000, /* MB */ + 16000, 1000, /* WB */ + 20000, 1000, /* SWB */ + 22000, 1000, /* FB */ +}; + +int opus_encoder_get_size(int channels) +{ + int silkEncSizeBytes, celtEncSizeBytes; + int ret; + if (channels<1 || channels > 2) + return 0; + ret = silk_Get_Encoder_Size( &silkEncSizeBytes ); + if (ret) + return 0; + silkEncSizeBytes = align(silkEncSizeBytes); + celtEncSizeBytes = celt_encoder_get_size(channels); + return align(sizeof(OpusEncoder))+silkEncSizeBytes+celtEncSizeBytes; +} + +int opus_encoder_init(OpusEncoder* st, opus_int32 Fs, int channels, int application) +{ + void *silk_enc; + CELTEncoder *celt_enc; + int err; + int ret, silkEncSizeBytes; + + if((Fs!=48000&&Fs!=24000&&Fs!=16000&&Fs!=12000&&Fs!=8000)||(channels!=1&&channels!=2)|| + (application != OPUS_APPLICATION_VOIP && application != OPUS_APPLICATION_AUDIO + && application != OPUS_APPLICATION_RESTRICTED_LOWDELAY)) + return OPUS_BAD_ARG; + + OPUS_CLEAR((char*)st, opus_encoder_get_size(channels)); + /* Create SILK encoder */ + ret = silk_Get_Encoder_Size( &silkEncSizeBytes ); + if (ret) + return OPUS_BAD_ARG; + silkEncSizeBytes = align(silkEncSizeBytes); + st->silk_enc_offset = align(sizeof(OpusEncoder)); + st->celt_enc_offset = st->silk_enc_offset+silkEncSizeBytes; + silk_enc = (char*)st+st->silk_enc_offset; + celt_enc = (CELTEncoder*)((char*)st+st->celt_enc_offset); + + st->stream_channels = st->channels = channels; + + st->Fs = Fs; + + st->arch = opus_select_arch(); + + ret = silk_InitEncoder( silk_enc, st->arch, &st->silk_mode ); + if(ret)return OPUS_INTERNAL_ERROR; + + /* default SILK parameters */ + st->silk_mode.nChannelsAPI = channels; + st->silk_mode.nChannelsInternal = channels; + st->silk_mode.API_sampleRate = st->Fs; + st->silk_mode.maxInternalSampleRate = 16000; + st->silk_mode.minInternalSampleRate = 8000; + st->silk_mode.desiredInternalSampleRate = 16000; + st->silk_mode.payloadSize_ms = 20; + st->silk_mode.bitRate = 25000; + st->silk_mode.packetLossPercentage = 0; + st->silk_mode.complexity = 9; + st->silk_mode.useInBandFEC = 0; + st->silk_mode.useDRED = 0; + st->silk_mode.useDTX = 0; + st->silk_mode.useCBR = 0; + st->silk_mode.reducedDependency = 0; + + /* Create CELT encoder */ + /* Initialize CELT encoder */ + err = celt_encoder_init(celt_enc, Fs, channels, st->arch); + if(err!=OPUS_OK)return OPUS_INTERNAL_ERROR; + + celt_encoder_ctl(celt_enc, CELT_SET_SIGNALLING(0)); + celt_encoder_ctl(celt_enc, OPUS_SET_COMPLEXITY(st->silk_mode.complexity)); + +#ifdef ENABLE_DRED + /* Initialize DRED Encoder */ + dred_encoder_init( &st->dred_encoder, Fs, channels ); +#endif + + st->use_vbr = 1; + /* Makes constrained VBR the default (safer for real-time use) */ + st->vbr_constraint = 1; + st->user_bitrate_bps = OPUS_AUTO; + st->bitrate_bps = 3000+Fs*channels; + st->application = application; + st->signal_type = OPUS_AUTO; + st->user_bandwidth = OPUS_AUTO; + st->max_bandwidth = OPUS_BANDWIDTH_FULLBAND; + st->force_channels = OPUS_AUTO; + st->user_forced_mode = OPUS_AUTO; + st->voice_ratio = -1; + st->encoder_buffer = st->Fs/100; + st->lsb_depth = 24; + st->variable_duration = OPUS_FRAMESIZE_ARG; + + /* Delay compensation of 4 ms (2.5 ms for SILK's extra look-ahead + + 1.5 ms for SILK resamplers and stereo prediction) */ + st->delay_compensation = st->Fs/250; + + st->hybrid_stereo_width_Q14 = 1 << 14; + st->prev_HB_gain = Q15ONE; + st->variable_HP_smth2_Q15 = silk_LSHIFT( silk_lin2log( VARIABLE_HP_MIN_CUTOFF_HZ ), 8 ); + st->first = 1; + st->mode = MODE_HYBRID; + st->bandwidth = OPUS_BANDWIDTH_FULLBAND; + +#ifndef DISABLE_FLOAT_API + tonality_analysis_init(&st->analysis, st->Fs); + st->analysis.application = st->application; +#endif + + return OPUS_OK; +} + +static unsigned char gen_toc(int mode, int framerate, int bandwidth, int channels) +{ + int period; + unsigned char toc; + period = 0; + while (framerate < 400) + { + framerate <<= 1; + period++; + } + if (mode == MODE_SILK_ONLY) + { + toc = (bandwidth-OPUS_BANDWIDTH_NARROWBAND)<<5; + toc |= (period-2)<<3; + } else if (mode == MODE_CELT_ONLY) + { + int tmp = bandwidth-OPUS_BANDWIDTH_MEDIUMBAND; + if (tmp < 0) + tmp = 0; + toc = 0x80; + toc |= tmp << 5; + toc |= period<<3; + } else /* Hybrid */ + { + toc = 0x60; + toc |= (bandwidth-OPUS_BANDWIDTH_SUPERWIDEBAND)<<4; + toc |= (period-2)<<3; + } + toc |= (channels==2)<<2; + return toc; +} + +#ifndef FIXED_POINT +static void silk_biquad_float( + const opus_val16 *in, /* I: Input signal */ + const opus_int32 *B_Q28, /* I: MA coefficients [3] */ + const opus_int32 *A_Q28, /* I: AR coefficients [2] */ + opus_val32 *S, /* I/O: State vector [2] */ + opus_val16 *out, /* O: Output signal */ + const opus_int32 len, /* I: Signal length (must be even) */ + int stride +) +{ + /* DIRECT FORM II TRANSPOSED (uses 2 element state vector) */ + opus_int k; + opus_val32 vout; + opus_val32 inval; + opus_val32 A[2], B[3]; + + A[0] = (opus_val32)(A_Q28[0] * (1.f/((opus_int32)1<<28))); + A[1] = (opus_val32)(A_Q28[1] * (1.f/((opus_int32)1<<28))); + B[0] = (opus_val32)(B_Q28[0] * (1.f/((opus_int32)1<<28))); + B[1] = (opus_val32)(B_Q28[1] * (1.f/((opus_int32)1<<28))); + B[2] = (opus_val32)(B_Q28[2] * (1.f/((opus_int32)1<<28))); + + /* Negate A_Q28 values and split in two parts */ + + for( k = 0; k < len; k++ ) { + /* S[ 0 ], S[ 1 ]: Q12 */ + inval = in[ k*stride ]; + vout = S[ 0 ] + B[0]*inval; + + S[ 0 ] = S[1] - vout*A[0] + B[1]*inval; + + S[ 1 ] = - vout*A[1] + B[2]*inval + VERY_SMALL; + + /* Scale back to Q0 and saturate */ + out[ k*stride ] = vout; + } +} +#endif + +static void hp_cutoff(const opus_val16 *in, opus_int32 cutoff_Hz, opus_val16 *out, opus_val32 *hp_mem, int len, int channels, opus_int32 Fs, int arch) +{ + opus_int32 B_Q28[ 3 ], A_Q28[ 2 ]; + opus_int32 Fc_Q19, r_Q28, r_Q22; + (void)arch; + + silk_assert( cutoff_Hz <= silk_int32_MAX / SILK_FIX_CONST( 1.5 * 3.14159 / 1000, 19 ) ); + Fc_Q19 = silk_DIV32_16( silk_SMULBB( SILK_FIX_CONST( 1.5 * 3.14159 / 1000, 19 ), cutoff_Hz ), Fs/1000 ); + silk_assert( Fc_Q19 > 0 && Fc_Q19 < 32768 ); + + r_Q28 = SILK_FIX_CONST( 1.0, 28 ) - silk_MUL( SILK_FIX_CONST( 0.92, 9 ), Fc_Q19 ); + + /* b = r * [ 1; -2; 1 ]; */ + /* a = [ 1; -2 * r * ( 1 - 0.5 * Fc^2 ); r^2 ]; */ + B_Q28[ 0 ] = r_Q28; + B_Q28[ 1 ] = silk_LSHIFT( -r_Q28, 1 ); + B_Q28[ 2 ] = r_Q28; + + /* -r * ( 2 - Fc * Fc ); */ + r_Q22 = silk_RSHIFT( r_Q28, 6 ); + A_Q28[ 0 ] = silk_SMULWW( r_Q22, silk_SMULWW( Fc_Q19, Fc_Q19 ) - SILK_FIX_CONST( 2.0, 22 ) ); + A_Q28[ 1 ] = silk_SMULWW( r_Q22, r_Q22 ); + +#ifdef FIXED_POINT + if( channels == 1 ) { + silk_biquad_alt_stride1( in, B_Q28, A_Q28, hp_mem, out, len ); + } else { + silk_biquad_alt_stride2( in, B_Q28, A_Q28, hp_mem, out, len, arch ); + } +#else + silk_biquad_float( in, B_Q28, A_Q28, hp_mem, out, len, channels ); + if( channels == 2 ) { + silk_biquad_float( in+1, B_Q28, A_Q28, hp_mem+2, out+1, len, channels ); + } +#endif +} + +#ifdef FIXED_POINT +static void dc_reject(const opus_val16 *in, opus_int32 cutoff_Hz, opus_val16 *out, opus_val32 *hp_mem, int len, int channels, opus_int32 Fs) +{ + int c, i; + int shift; + + /* Approximates -round(log2(6.3*cutoff_Hz/Fs)) */ + shift=celt_ilog2(Fs/(cutoff_Hz*4)); + for (c=0;csilk_mode.useInBandFEC) { + dred_frac = MIN16(.7f, 3.f*st->silk_mode.packetLossPercentage/100.f); + bitrate_offset = 20000; + } else { + if (st->silk_mode.packetLossPercentage > 5) { + dred_frac = MIN16(.8f, .55f + st->silk_mode.packetLossPercentage/100.f); + } else { + dred_frac = 12*st->silk_mode.packetLossPercentage/100.f; + } + bitrate_offset = 12000; + } + /* Account for the fact that longer packets require less redundancy. */ + dred_frac = dred_frac/(dred_frac + (1-dred_frac)*(frame_size*50.f)/st->Fs); + /* Approximate fit based on a few experiments. Could probably be improved. */ + q0 = IMIN(15, IMAX(4, 51 - 3*EC_ILOG(IMAX(1, bitrate_bps-bitrate_offset)))); + dQ = bitrate_bps-bitrate_offset > 36000 ? 3 : 5; + qmax = 15; + target_dred_bitrate = IMAX(0, (int)(dred_frac*(bitrate_bps-bitrate_offset))); + if (st->dred_duration > 0) { + opus_int32 target_bits = target_dred_bitrate*frame_size/st->Fs; + max_dred_bits = estimate_dred_bitrate(q0, dQ, qmax, st->dred_duration, target_bits, &target_chunks); + } else { + max_dred_bits = 0; + target_chunks=0; + } + dred_bitrate = IMIN(target_dred_bitrate, max_dred_bits*st->Fs/frame_size); + /* If we can't afford enough bits, don't bother with DRED at all. */ + if (target_chunks < 2) + dred_bitrate = 0; + st->dred_q0 = q0; + st->dred_dQ = dQ; + st->dred_qmax = qmax; + st->dred_target_chunks = target_chunks; + return dred_bitrate; +} +#endif + +static opus_int32 user_bitrate_to_bitrate(OpusEncoder *st, int frame_size, int max_data_bytes) +{ + if(!frame_size)frame_size=st->Fs/400; + if (st->user_bitrate_bps==OPUS_AUTO) + return 60*st->Fs/frame_size + st->Fs*st->channels; + else if (st->user_bitrate_bps==OPUS_BITRATE_MAX) + return max_data_bytes*8*st->Fs/frame_size; + else + return st->user_bitrate_bps; +} + +#ifndef DISABLE_FLOAT_API +#ifdef FIXED_POINT +#define PCM2VAL(x) FLOAT2INT16(x) +#else +#define PCM2VAL(x) SCALEIN(x) +#endif + +void downmix_float(const void *_x, opus_val32 *y, int subframe, int offset, int c1, int c2, int C) +{ + const float *x; + int j; + + x = (const float *)_x; + for (j=0;j-1) + { + for (j=0;j-1) + { + for (j=0;j= OPUS_FRAMESIZE_2_5_MS && variable_duration <= OPUS_FRAMESIZE_120_MS) + { + if (variable_duration <= OPUS_FRAMESIZE_40_MS) + new_size = (Fs/400)<<(variable_duration-OPUS_FRAMESIZE_2_5_MS); + else + new_size = (variable_duration-OPUS_FRAMESIZE_2_5_MS-2)*Fs/50; + } + else + return -1; + if (new_size>frame_size) + return -1; + if (400*new_size!=Fs && 200*new_size!=Fs && 100*new_size!=Fs && + 50*new_size!=Fs && 25*new_size!=Fs && 50*new_size!=3*Fs && + 50*new_size!=4*Fs && 50*new_size!=5*Fs && 50*new_size!=6*Fs) + return -1; + return new_size; +} + +opus_val16 compute_stereo_width(const opus_val16 *pcm, int frame_size, opus_int32 Fs, StereoWidthState *mem) +{ + opus_val32 xx, xy, yy; + opus_val16 sqrt_xx, sqrt_yy; + opus_val16 qrrt_xx, qrrt_yy; + int frame_rate; + int i; + opus_val16 short_alpha; + + frame_rate = Fs/frame_size; + short_alpha = Q15ONE - MULT16_16(25, Q15ONE)/IMAX(50,frame_rate); + xx=xy=yy=0; + /* Unroll by 4. The frame size is always a multiple of 4 *except* for + 2.5 ms frames at 12 kHz. Since this setting is very rare (and very + stupid), we just discard the last two samples. */ + for (i=0;iXX += MULT16_32_Q15(short_alpha, xx-mem->XX); + mem->XY += MULT16_32_Q15(short_alpha, xy-mem->XY); + mem->YY += MULT16_32_Q15(short_alpha, yy-mem->YY); + mem->XX = MAX32(0, mem->XX); + mem->XY = MAX32(0, mem->XY); + mem->YY = MAX32(0, mem->YY); + if (MAX32(mem->XX, mem->YY)>QCONST16(8e-4f, 18)) + { + opus_val16 corr; + opus_val16 ldiff; + opus_val16 width; + sqrt_xx = celt_sqrt(mem->XX); + sqrt_yy = celt_sqrt(mem->YY); + qrrt_xx = celt_sqrt(sqrt_xx); + qrrt_yy = celt_sqrt(sqrt_yy); + /* Inter-channel correlation */ + mem->XY = MIN32(mem->XY, sqrt_xx*sqrt_yy); + corr = SHR32(frac_div32(mem->XY,EPSILON+MULT16_16(sqrt_xx,sqrt_yy)),16); + /* Approximate loudness difference */ + ldiff = MULT16_16(Q15ONE, ABS16(qrrt_xx-qrrt_yy))/(EPSILON+qrrt_xx+qrrt_yy); + width = MULT16_16_Q15(celt_sqrt(QCONST32(1.f,30)-MULT16_16(corr,corr)), ldiff); + /* Smoothing over one second */ + mem->smoothed_width += (width-mem->smoothed_width)/frame_rate; + /* Peak follower */ + mem->max_follower = MAX16(mem->max_follower-QCONST16(.02f,15)/frame_rate, mem->smoothed_width); + } + /*printf("%f %f %f %f %f ", corr/(float)Q15ONE, ldiff/(float)Q15ONE, width/(float)Q15ONE, mem->smoothed_width/(float)Q15ONE, mem->max_follower/(float)Q15ONE);*/ + return EXTRACT16(MIN32(Q15ONE, MULT16_16(20, mem->max_follower))); +} + +static int decide_fec(int useInBandFEC, int PacketLoss_perc, int last_fec, int mode, int *bandwidth, opus_int32 rate) +{ + int orig_bandwidth; + if (!useInBandFEC || PacketLoss_perc == 0 || mode == MODE_CELT_ONLY) + return 0; + orig_bandwidth = *bandwidth; + for (;;) + { + opus_int32 hysteresis; + opus_int32 LBRR_rate_thres_bps; + /* Compute threshold for using FEC at the current bandwidth setting */ + LBRR_rate_thres_bps = fec_thresholds[2*(*bandwidth - OPUS_BANDWIDTH_NARROWBAND)]; + hysteresis = fec_thresholds[2*(*bandwidth - OPUS_BANDWIDTH_NARROWBAND) + 1]; + if (last_fec == 1) LBRR_rate_thres_bps -= hysteresis; + if (last_fec == 0) LBRR_rate_thres_bps += hysteresis; + LBRR_rate_thres_bps = silk_SMULWB( silk_MUL( LBRR_rate_thres_bps, + 125 - silk_min( PacketLoss_perc, 25 ) ), SILK_FIX_CONST( 0.01, 16 ) ); + /* If loss <= 5%, we look at whether we have enough rate to enable FEC. + If loss > 5%, we decrease the bandwidth until we can enable FEC. */ + if (rate > LBRR_rate_thres_bps) + return 1; + else if (PacketLoss_perc <= 5) + return 0; + else if (*bandwidth > OPUS_BANDWIDTH_NARROWBAND) + (*bandwidth)--; + else + break; + } + /* Couldn't find any bandwidth to enable FEC, keep original bandwidth. */ + *bandwidth = orig_bandwidth; + return 0; +} + +static int compute_silk_rate_for_hybrid(int rate, int bandwidth, int frame20ms, int vbr, int fec, int channels) { + int entry; + int i; + int N; + int silk_rate; + static int rate_table[][5] = { + /* |total| |-------- SILK------------| + |-- No FEC -| |--- FEC ---| + 10ms 20ms 10ms 20ms */ + { 0, 0, 0, 0, 0}, + {12000, 10000, 10000, 11000, 11000}, + {16000, 13500, 13500, 15000, 15000}, + {20000, 16000, 16000, 18000, 18000}, + {24000, 18000, 18000, 21000, 21000}, + {32000, 22000, 22000, 28000, 28000}, + {64000, 38000, 38000, 50000, 50000} + }; + /* Do the allocation per-channel. */ + rate /= channels; + entry = 1 + frame20ms + 2*fec; + N = sizeof(rate_table)/sizeof(rate_table[0]); + for (i=1;i rate) break; + } + if (i == N) + { + silk_rate = rate_table[i-1][entry]; + /* For now, just give 50% of the extra bits to SILK. */ + silk_rate += (rate-rate_table[i-1][0])/2; + } else { + opus_int32 lo, hi, x0, x1; + lo = rate_table[i-1][entry]; + hi = rate_table[i][entry]; + x0 = rate_table[i-1][0]; + x1 = rate_table[i][0]; + silk_rate = (lo*(x1-rate) + hi*(rate-x0))/(x1-x0); + } + if (!vbr) + { + /* Tiny boost to SILK for CBR. We should probably tune this better. */ + silk_rate += 100; + } + if (bandwidth==OPUS_BANDWIDTH_SUPERWIDEBAND) + silk_rate += 300; + silk_rate *= channels; + /* Small adjustment for stereo (calibrated for 32 kb/s, haven't tried other bitrates). */ + if (channels == 2 && rate >= 12000) + silk_rate -= 1000; + return silk_rate; +} + +/* Returns the equivalent bitrate corresponding to 20 ms frames, + complexity 10 VBR operation. */ +static opus_int32 compute_equiv_rate(opus_int32 bitrate, int channels, + int frame_rate, int vbr, int mode, int complexity, int loss) +{ + opus_int32 equiv; + equiv = bitrate; + /* Take into account overhead from smaller frames. */ + if (frame_rate > 50) + equiv -= (40*channels+20)*(frame_rate - 50); + /* CBR is about a 8% penalty for both SILK and CELT. */ + if (!vbr) + equiv -= equiv/12; + /* Complexity makes about 10% difference (from 0 to 10) in general. */ + equiv = equiv * (90+complexity)/100; + if (mode == MODE_SILK_ONLY || mode == MODE_HYBRID) + { + /* SILK complexity 0-1 uses the non-delayed-decision NSQ, which + costs about 20%. */ + if (complexity<2) + equiv = equiv*4/5; + equiv -= equiv*loss/(6*loss + 10); + } else if (mode == MODE_CELT_ONLY) { + /* CELT complexity 0-4 doesn't have the pitch filter, which costs + about 10%. */ + if (complexity<5) + equiv = equiv*9/10; + } else { + /* Mode not known yet */ + /* Half the SILK loss*/ + equiv -= equiv*loss/(12*loss + 20); + } + return equiv; +} + +#ifndef DISABLE_FLOAT_API + +int is_digital_silence(const opus_val16* pcm, int frame_size, int channels, int lsb_depth) +{ + int silence = 0; + opus_val32 sample_max = 0; +#ifdef MLP_TRAINING + return 0; +#endif + sample_max = celt_maxabs16(pcm, frame_size*channels); + +#ifdef FIXED_POINT + silence = (sample_max == 0); + (void)lsb_depth; +#else + silence = (sample_max <= (opus_val16) 1 / (1 << lsb_depth)); +#endif + + return silence; +} + +#ifdef FIXED_POINT +static opus_val32 compute_frame_energy(const opus_val16 *pcm, int frame_size, int channels, int arch) +{ + int i; + opus_val32 sample_max; + int max_shift; + int shift; + opus_val32 energy = 0; + int len = frame_size*channels; + (void)arch; + /* Max amplitude in the signal */ + sample_max = celt_maxabs16(pcm, len); + + /* Compute the right shift required in the MAC to avoid an overflow */ + max_shift = celt_ilog2(len); + shift = IMAX(0, (celt_ilog2(1+sample_max) << 1) + max_shift - 28); + + /* Compute the energy */ + for (i=0; i NB_SPEECH_FRAMES_BEFORE_DTX*20*2) + { + if (*nb_no_activity_ms_Q1 <= (NB_SPEECH_FRAMES_BEFORE_DTX + MAX_CONSECUTIVE_DTX)*20*2) + /* Valid frame for DTX! */ + return 1; + else + (*nb_no_activity_ms_Q1) = NB_SPEECH_FRAMES_BEFORE_DTX*20*2; + } + } else + (*nb_no_activity_ms_Q1) = 0; + + return 0; +} + +#endif + +static int compute_redundancy_bytes(opus_int32 max_data_bytes, opus_int32 bitrate_bps, int frame_rate, int channels) +{ + int redundancy_bytes_cap; + int redundancy_bytes; + opus_int32 redundancy_rate; + int base_bits; + opus_int32 available_bits; + base_bits = (40*channels+20); + + /* Equivalent rate for 5 ms frames. */ + redundancy_rate = bitrate_bps + base_bits*(200 - frame_rate); + /* For VBR, further increase the bitrate if we can afford it. It's pretty short + and we'll avoid artefacts. */ + redundancy_rate = 3*redundancy_rate/2; + redundancy_bytes = redundancy_rate/1600; + + /* Compute the max rate we can use given CBR or VBR with cap. */ + available_bits = max_data_bytes*8 - 2*base_bits; + redundancy_bytes_cap = (available_bits*240/(240+48000/frame_rate) + base_bits)/8; + redundancy_bytes = IMIN(redundancy_bytes, redundancy_bytes_cap); + /* It we can't get enough bits for redundancy to be worth it, rely on the decoder PLC. */ + if (redundancy_bytes > 4 + 8*channels) + redundancy_bytes = IMIN(257, redundancy_bytes); + else + redundancy_bytes = 0; + return redundancy_bytes; +} + +static opus_int32 opus_encode_frame_native(OpusEncoder *st, const opus_val16 *pcm, int frame_size, + unsigned char *data, opus_int32 max_data_bytes, + int float_api, int first_frame, +#ifdef ENABLE_DRED + opus_int32 dred_bitrate_bps, +#endif +#ifndef DISABLE_FLOAT_API + AnalysisInfo *analysis_info, int is_silence, +#endif + int redundancy, int celt_to_silk, int prefill, + opus_int32 equiv_rate, int to_celt); + +opus_int32 opus_encode_native(OpusEncoder *st, const opus_val16 *pcm, int frame_size, + unsigned char *data, opus_int32 out_data_bytes, int lsb_depth, + const void *analysis_pcm, opus_int32 analysis_size, int c1, int c2, + int analysis_channels, downmix_func downmix, int float_api) +{ + void *silk_enc; + CELTEncoder *celt_enc; + int i; + int ret=0; + int prefill=0; + int redundancy = 0; + int celt_to_silk = 0; + int to_celt = 0; + int voice_est; /* Probability of voice in Q7 */ + opus_int32 equiv_rate; + int frame_rate; + opus_int32 max_rate; /* Max bitrate we're allowed to use */ + int curr_bandwidth; + opus_int32 max_data_bytes; /* Max number of bytes we're allowed to use */ + opus_int32 cbr_bytes=-1; + opus_val16 stereo_width; + const CELTMode *celt_mode; +#ifndef DISABLE_FLOAT_API + AnalysisInfo analysis_info; + int analysis_read_pos_bak=-1; + int analysis_read_subframe_bak=-1; + int is_silence = 0; +#endif +#ifdef ENABLE_DRED + opus_int32 dred_bitrate_bps; +#endif + ALLOC_STACK; + + max_data_bytes = IMIN(1276, out_data_bytes); + + st->rangeFinal = 0; + if (frame_size <= 0 || max_data_bytes <= 0) + { + RESTORE_STACK; + return OPUS_BAD_ARG; + } + + /* Cannot encode 100 ms in 1 byte */ + if (max_data_bytes==1 && st->Fs==(frame_size*10)) + { + RESTORE_STACK; + return OPUS_BUFFER_TOO_SMALL; + } + + silk_enc = (char*)st+st->silk_enc_offset; + celt_enc = (CELTEncoder*)((char*)st+st->celt_enc_offset); + + lsb_depth = IMIN(lsb_depth, st->lsb_depth); + + celt_encoder_ctl(celt_enc, CELT_GET_MODE(&celt_mode)); +#ifndef DISABLE_FLOAT_API + analysis_info.valid = 0; +#ifdef FIXED_POINT + if (st->silk_mode.complexity >= 10 && st->Fs>=16000) +#else + if (st->silk_mode.complexity >= 7 && st->Fs>=16000) +#endif + { + is_silence = is_digital_silence(pcm, frame_size, st->channels, lsb_depth); + analysis_read_pos_bak = st->analysis.read_pos; + analysis_read_subframe_bak = st->analysis.read_subframe; + run_analysis(&st->analysis, celt_mode, analysis_pcm, analysis_size, frame_size, + c1, c2, analysis_channels, st->Fs, + lsb_depth, downmix, &analysis_info); + + /* Track the peak signal energy */ + if (!is_silence && analysis_info.activity_probability > DTX_ACTIVITY_THRESHOLD) + st->peak_signal_energy = MAX32(MULT16_32_Q15(QCONST16(0.999f, 15), st->peak_signal_energy), + compute_frame_energy(pcm, frame_size, st->channels, st->arch)); + } else if (st->analysis.initialized) { + tonality_analysis_reset(&st->analysis); + } +#else + (void)analysis_pcm; + (void)analysis_size; + (void)c1; + (void)c2; + (void)analysis_channels; + (void)downmix; +#endif + +#ifndef DISABLE_FLOAT_API + /* Reset voice_ratio if this frame is not silent or if analysis is disabled. + * Otherwise, preserve voice_ratio from the last non-silent frame */ + if (!is_silence) + st->voice_ratio = -1; + + st->detected_bandwidth = 0; + if (analysis_info.valid) + { + int analysis_bandwidth; + if (st->signal_type == OPUS_AUTO) + { + float prob; + if (st->prev_mode == 0) + prob = analysis_info.music_prob; + else if (st->prev_mode == MODE_CELT_ONLY) + prob = analysis_info.music_prob_max; + else + prob = analysis_info.music_prob_min; + st->voice_ratio = (int)floor(.5+100*(1-prob)); + } + + analysis_bandwidth = analysis_info.bandwidth; + if (analysis_bandwidth<=12) + st->detected_bandwidth = OPUS_BANDWIDTH_NARROWBAND; + else if (analysis_bandwidth<=14) + st->detected_bandwidth = OPUS_BANDWIDTH_MEDIUMBAND; + else if (analysis_bandwidth<=16) + st->detected_bandwidth = OPUS_BANDWIDTH_WIDEBAND; + else if (analysis_bandwidth<=18) + st->detected_bandwidth = OPUS_BANDWIDTH_SUPERWIDEBAND; + else + st->detected_bandwidth = OPUS_BANDWIDTH_FULLBAND; + } +#else + st->voice_ratio = -1; +#endif + + if (st->channels==2 && st->force_channels!=1) + stereo_width = compute_stereo_width(pcm, frame_size, st->Fs, &st->width_mem); + else + stereo_width = 0; + st->bitrate_bps = user_bitrate_to_bitrate(st, frame_size, max_data_bytes); + + frame_rate = st->Fs/frame_size; + if (!st->use_vbr) + { + /* Multiply by 12 to make sure the division is exact. */ + int frame_rate12 = 12*st->Fs/frame_size; + /* We need to make sure that "int" values always fit in 16 bits. */ + cbr_bytes = IMIN( (12*st->bitrate_bps/8 + frame_rate12/2)/frame_rate12, max_data_bytes); + st->bitrate_bps = cbr_bytes*(opus_int32)frame_rate12*8/12; + /* Make sure we provide at least one byte to avoid failing. */ + max_data_bytes = IMAX(1, cbr_bytes); + } +#ifdef ENABLE_DRED + /* Allocate some of the bits to DRED if needed. */ + dred_bitrate_bps = compute_dred_bitrate(st, st->bitrate_bps, frame_size); + st->bitrate_bps -= dred_bitrate_bps; +#endif + if (max_data_bytes<3 || st->bitrate_bps < 3*frame_rate*8 + || (frame_rate<50 && (max_data_bytes*frame_rate<300 || st->bitrate_bps < 2400))) + { + /*If the space is too low to do something useful, emit 'PLC' frames.*/ + int tocmode = st->mode; + int bw = st->bandwidth == 0 ? OPUS_BANDWIDTH_NARROWBAND : st->bandwidth; + int packet_code = 0; + int num_multiframes = 0; + + if (tocmode==0) + tocmode = MODE_SILK_ONLY; + if (frame_rate>100) + tocmode = MODE_CELT_ONLY; + /* 40 ms -> 2 x 20 ms if in CELT_ONLY or HYBRID mode */ + if (frame_rate==25 && tocmode!=MODE_SILK_ONLY) + { + frame_rate = 50; + packet_code = 1; + } + + /* >= 60 ms frames */ + if (frame_rate<=16) + { + /* 1 x 60 ms, 2 x 40 ms, 2 x 60 ms */ + if (out_data_bytes==1 || (tocmode==MODE_SILK_ONLY && frame_rate!=10)) + { + tocmode = MODE_SILK_ONLY; + + packet_code = frame_rate <= 12; + frame_rate = frame_rate == 12 ? 25 : 16; + } + else + { + num_multiframes = 50/frame_rate; + frame_rate = 50; + packet_code = 3; + } + } + + if(tocmode==MODE_SILK_ONLY&&bw>OPUS_BANDWIDTH_WIDEBAND) + bw=OPUS_BANDWIDTH_WIDEBAND; + else if (tocmode==MODE_CELT_ONLY&&bw==OPUS_BANDWIDTH_MEDIUMBAND) + bw=OPUS_BANDWIDTH_NARROWBAND; + else if (tocmode==MODE_HYBRID&&bw<=OPUS_BANDWIDTH_SUPERWIDEBAND) + bw=OPUS_BANDWIDTH_SUPERWIDEBAND; + + data[0] = gen_toc(tocmode, frame_rate, bw, st->stream_channels); + data[0] |= packet_code; + + ret = packet_code <= 1 ? 1 : 2; + + max_data_bytes = IMAX(max_data_bytes, ret); + + if (packet_code==3) + data[1] = num_multiframes; + + if (!st->use_vbr) + { + ret = opus_packet_pad(data, ret, max_data_bytes); + if (ret == OPUS_OK) + ret = max_data_bytes; + else + ret = OPUS_INTERNAL_ERROR; + } + RESTORE_STACK; + return ret; + } + max_rate = frame_rate*max_data_bytes*8; + + /* Equivalent 20-ms rate for mode/channel/bandwidth decisions */ + equiv_rate = compute_equiv_rate(st->bitrate_bps, st->channels, st->Fs/frame_size, + st->use_vbr, 0, st->silk_mode.complexity, st->silk_mode.packetLossPercentage); + + if (st->signal_type == OPUS_SIGNAL_VOICE) + voice_est = 127; + else if (st->signal_type == OPUS_SIGNAL_MUSIC) + voice_est = 0; + else if (st->voice_ratio >= 0) + { + voice_est = st->voice_ratio*327>>8; + /* For AUDIO, never be more than 90% confident of having speech */ + if (st->application == OPUS_APPLICATION_AUDIO) + voice_est = IMIN(voice_est, 115); + } else if (st->application == OPUS_APPLICATION_VOIP) + voice_est = 115; + else + voice_est = 48; + + if (st->force_channels!=OPUS_AUTO && st->channels == 2) + { + st->stream_channels = st->force_channels; + } else { +#ifdef FUZZING + (void)stereo_music_threshold; + (void)stereo_voice_threshold; + /* Random mono/stereo decision */ + if (st->channels == 2 && (rand()&0x1F)==0) + st->stream_channels = 3-st->stream_channels; +#else + /* Rate-dependent mono-stereo decision */ + if (st->channels == 2) + { + opus_int32 stereo_threshold; + stereo_threshold = stereo_music_threshold + ((voice_est*voice_est*(stereo_voice_threshold-stereo_music_threshold))>>14); + if (st->stream_channels == 2) + stereo_threshold -= 1000; + else + stereo_threshold += 1000; + st->stream_channels = (equiv_rate > stereo_threshold) ? 2 : 1; + } else { + st->stream_channels = st->channels; + } +#endif + } + /* Update equivalent rate for channels decision. */ + equiv_rate = compute_equiv_rate(st->bitrate_bps, st->stream_channels, st->Fs/frame_size, + st->use_vbr, 0, st->silk_mode.complexity, st->silk_mode.packetLossPercentage); + + /* Allow SILK DTX if DTX is enabled but the generalized DTX cannot be used, + e.g. because of the complexity setting or sample rate. */ +#ifndef DISABLE_FLOAT_API + st->silk_mode.useDTX = st->use_dtx && !(analysis_info.valid || is_silence); +#else + st->silk_mode.useDTX = st->use_dtx; +#endif + + /* Mode selection depending on application and signal type */ + if (st->application == OPUS_APPLICATION_RESTRICTED_LOWDELAY) + { + st->mode = MODE_CELT_ONLY; + } else if (st->user_forced_mode == OPUS_AUTO) + { +#ifdef FUZZING + (void)stereo_width; + (void)mode_thresholds; + /* Random mode switching */ + if ((rand()&0xF)==0) + { + if ((rand()&0x1)==0) + st->mode = MODE_CELT_ONLY; + else + st->mode = MODE_SILK_ONLY; + } else { + if (st->prev_mode==MODE_CELT_ONLY) + st->mode = MODE_CELT_ONLY; + else + st->mode = MODE_SILK_ONLY; + } +#else + opus_int32 mode_voice, mode_music; + opus_int32 threshold; + + /* Interpolate based on stereo width */ + mode_voice = (opus_int32)(MULT16_32_Q15(Q15ONE-stereo_width,mode_thresholds[0][0]) + + MULT16_32_Q15(stereo_width,mode_thresholds[1][0])); + mode_music = (opus_int32)(MULT16_32_Q15(Q15ONE-stereo_width,mode_thresholds[1][1]) + + MULT16_32_Q15(stereo_width,mode_thresholds[1][1])); + /* Interpolate based on speech/music probability */ + threshold = mode_music + ((voice_est*voice_est*(mode_voice-mode_music))>>14); + /* Bias towards SILK for VoIP because of some useful features */ + if (st->application == OPUS_APPLICATION_VOIP) + threshold += 8000; + + /*printf("%f %d\n", stereo_width/(float)Q15ONE, threshold);*/ + /* Hysteresis */ + if (st->prev_mode == MODE_CELT_ONLY) + threshold -= 4000; + else if (st->prev_mode>0) + threshold += 4000; + + st->mode = (equiv_rate >= threshold) ? MODE_CELT_ONLY: MODE_SILK_ONLY; + + /* When FEC is enabled and there's enough packet loss, use SILK. + Unless the FEC is set to 2, in which case we don't switch to SILK if we're confident we have music. */ + if (st->silk_mode.useInBandFEC && st->silk_mode.packetLossPercentage > (128-voice_est)>>4 && (st->fec_config != 2 || voice_est > 25)) + st->mode = MODE_SILK_ONLY; + /* When encoding voice and DTX is enabled but the generalized DTX cannot be used, + use SILK in order to make use of its DTX. */ + if (st->silk_mode.useDTX && voice_est > 100) + st->mode = MODE_SILK_ONLY; +#endif + + /* If max_data_bytes represents less than 6 kb/s, switch to CELT-only mode */ + if (max_data_bytes < (frame_rate > 50 ? 9000 : 6000)*frame_size / (st->Fs * 8)) + st->mode = MODE_CELT_ONLY; + } else { + st->mode = st->user_forced_mode; + } + + /* Override the chosen mode to make sure we meet the requested frame size */ + if (st->mode != MODE_CELT_ONLY && frame_size < st->Fs/100) + st->mode = MODE_CELT_ONLY; + if (st->lfe) + st->mode = MODE_CELT_ONLY; + + if (st->prev_mode > 0 && + ((st->mode != MODE_CELT_ONLY && st->prev_mode == MODE_CELT_ONLY) || + (st->mode == MODE_CELT_ONLY && st->prev_mode != MODE_CELT_ONLY))) + { + redundancy = 1; + celt_to_silk = (st->mode != MODE_CELT_ONLY); + if (!celt_to_silk) + { + /* Switch to SILK/hybrid if frame size is 10 ms or more*/ + if (frame_size >= st->Fs/100) + { + st->mode = st->prev_mode; + to_celt = 1; + } else { + redundancy=0; + } + } + } + + /* When encoding multiframes, we can ask for a switch to CELT only in the last frame. This switch + * is processed above as the requested mode shouldn't interrupt stereo->mono transition. */ + if (st->stream_channels == 1 && st->prev_channels ==2 && st->silk_mode.toMono==0 + && st->mode != MODE_CELT_ONLY && st->prev_mode != MODE_CELT_ONLY) + { + /* Delay stereo->mono transition by two frames so that SILK can do a smooth downmix */ + st->silk_mode.toMono = 1; + st->stream_channels = 2; + } else { + st->silk_mode.toMono = 0; + } + + /* Update equivalent rate with mode decision. */ + equiv_rate = compute_equiv_rate(st->bitrate_bps, st->stream_channels, st->Fs/frame_size, + st->use_vbr, st->mode, st->silk_mode.complexity, st->silk_mode.packetLossPercentage); + + if (st->mode != MODE_CELT_ONLY && st->prev_mode == MODE_CELT_ONLY) + { + silk_EncControlStruct dummy; + silk_InitEncoder( silk_enc, st->arch, &dummy); + prefill=1; + } + + /* Automatic (rate-dependent) bandwidth selection */ + if (st->mode == MODE_CELT_ONLY || st->first || st->silk_mode.allowBandwidthSwitch) + { + const opus_int32 *voice_bandwidth_thresholds, *music_bandwidth_thresholds; + opus_int32 bandwidth_thresholds[8]; + int bandwidth = OPUS_BANDWIDTH_FULLBAND; + + if (st->channels==2 && st->force_channels!=1) + { + voice_bandwidth_thresholds = stereo_voice_bandwidth_thresholds; + music_bandwidth_thresholds = stereo_music_bandwidth_thresholds; + } else { + voice_bandwidth_thresholds = mono_voice_bandwidth_thresholds; + music_bandwidth_thresholds = mono_music_bandwidth_thresholds; + } + /* Interpolate bandwidth thresholds depending on voice estimation */ + for (i=0;i<8;i++) + { + bandwidth_thresholds[i] = music_bandwidth_thresholds[i] + + ((voice_est*voice_est*(voice_bandwidth_thresholds[i]-music_bandwidth_thresholds[i]))>>14); + } + do { + int threshold, hysteresis; + threshold = bandwidth_thresholds[2*(bandwidth-OPUS_BANDWIDTH_MEDIUMBAND)]; + hysteresis = bandwidth_thresholds[2*(bandwidth-OPUS_BANDWIDTH_MEDIUMBAND)+1]; + if (!st->first) + { + if (st->auto_bandwidth >= bandwidth) + threshold -= hysteresis; + else + threshold += hysteresis; + } + if (equiv_rate >= threshold) + break; + } while (--bandwidth>OPUS_BANDWIDTH_NARROWBAND); + /* We don't use mediumband anymore, except when explicitly requested or during + mode transitions. */ + if (bandwidth == OPUS_BANDWIDTH_MEDIUMBAND) + bandwidth = OPUS_BANDWIDTH_WIDEBAND; + st->bandwidth = st->auto_bandwidth = bandwidth; + /* Prevents any transition to SWB/FB until the SILK layer has fully + switched to WB mode and turned the variable LP filter off */ + if (!st->first && st->mode != MODE_CELT_ONLY && !st->silk_mode.inWBmodeWithoutVariableLP && st->bandwidth > OPUS_BANDWIDTH_WIDEBAND) + st->bandwidth = OPUS_BANDWIDTH_WIDEBAND; + } + + if (st->bandwidth>st->max_bandwidth) + st->bandwidth = st->max_bandwidth; + + if (st->user_bandwidth != OPUS_AUTO) + st->bandwidth = st->user_bandwidth; + + /* This prevents us from using hybrid at unsafe CBR/max rates */ + if (st->mode != MODE_CELT_ONLY && max_rate < 15000) + { + st->bandwidth = IMIN(st->bandwidth, OPUS_BANDWIDTH_WIDEBAND); + } + + /* Prevents Opus from wasting bits on frequencies that are above + the Nyquist rate of the input signal */ + if (st->Fs <= 24000 && st->bandwidth > OPUS_BANDWIDTH_SUPERWIDEBAND) + st->bandwidth = OPUS_BANDWIDTH_SUPERWIDEBAND; + if (st->Fs <= 16000 && st->bandwidth > OPUS_BANDWIDTH_WIDEBAND) + st->bandwidth = OPUS_BANDWIDTH_WIDEBAND; + if (st->Fs <= 12000 && st->bandwidth > OPUS_BANDWIDTH_MEDIUMBAND) + st->bandwidth = OPUS_BANDWIDTH_MEDIUMBAND; + if (st->Fs <= 8000 && st->bandwidth > OPUS_BANDWIDTH_NARROWBAND) + st->bandwidth = OPUS_BANDWIDTH_NARROWBAND; +#ifndef DISABLE_FLOAT_API + /* Use detected bandwidth to reduce the encoded bandwidth. */ + if (st->detected_bandwidth && st->user_bandwidth == OPUS_AUTO) + { + int min_detected_bandwidth; + /* Makes bandwidth detection more conservative just in case the detector + gets it wrong when we could have coded a high bandwidth transparently. + When operating in SILK/hybrid mode, we don't go below wideband to avoid + more complicated switches that require redundancy. */ + if (equiv_rate <= 18000*st->stream_channels && st->mode == MODE_CELT_ONLY) + min_detected_bandwidth = OPUS_BANDWIDTH_NARROWBAND; + else if (equiv_rate <= 24000*st->stream_channels && st->mode == MODE_CELT_ONLY) + min_detected_bandwidth = OPUS_BANDWIDTH_MEDIUMBAND; + else if (equiv_rate <= 30000*st->stream_channels) + min_detected_bandwidth = OPUS_BANDWIDTH_WIDEBAND; + else if (equiv_rate <= 44000*st->stream_channels) + min_detected_bandwidth = OPUS_BANDWIDTH_SUPERWIDEBAND; + else + min_detected_bandwidth = OPUS_BANDWIDTH_FULLBAND; + + st->detected_bandwidth = IMAX(st->detected_bandwidth, min_detected_bandwidth); + st->bandwidth = IMIN(st->bandwidth, st->detected_bandwidth); + } +#endif + st->silk_mode.LBRR_coded = decide_fec(st->silk_mode.useInBandFEC, st->silk_mode.packetLossPercentage, + st->silk_mode.LBRR_coded, st->mode, &st->bandwidth, equiv_rate); + celt_encoder_ctl(celt_enc, OPUS_SET_LSB_DEPTH(lsb_depth)); + + /* CELT mode doesn't support mediumband, use wideband instead */ + if (st->mode == MODE_CELT_ONLY && st->bandwidth == OPUS_BANDWIDTH_MEDIUMBAND) + st->bandwidth = OPUS_BANDWIDTH_WIDEBAND; + if (st->lfe) + st->bandwidth = OPUS_BANDWIDTH_NARROWBAND; + + curr_bandwidth = st->bandwidth; + + /* Chooses the appropriate mode for speech + *NEVER* switch to/from CELT-only mode here as this will invalidate some assumptions */ + if (st->mode == MODE_SILK_ONLY && curr_bandwidth > OPUS_BANDWIDTH_WIDEBAND) + st->mode = MODE_HYBRID; + if (st->mode == MODE_HYBRID && curr_bandwidth <= OPUS_BANDWIDTH_WIDEBAND) + st->mode = MODE_SILK_ONLY; + + /* Can't support higher than >60 ms frames, and >20 ms when in Hybrid or CELT-only modes */ + if ((frame_size > st->Fs/50 && (st->mode != MODE_SILK_ONLY)) || frame_size > 3*st->Fs/50) + { + int enc_frame_size; + int nb_frames; + VARDECL(unsigned char, tmp_data); + VARDECL(OpusRepacketizer, rp); + int max_header_bytes; + opus_int32 repacketize_len; + opus_int32 max_len_sum; + opus_int32 tot_size=0; + unsigned char *curr_data; + int tmp_len; + int dtx_count = 0; + + if (st->mode == MODE_SILK_ONLY) + { + if (frame_size == 2*st->Fs/25) /* 80 ms -> 2x 40 ms */ + enc_frame_size = st->Fs/25; + else if (frame_size == 3*st->Fs/25) /* 120 ms -> 2x 60 ms */ + enc_frame_size = 3*st->Fs/50; + else /* 100 ms -> 5x 20 ms */ + enc_frame_size = st->Fs/50; + } + else + enc_frame_size = st->Fs/50; + + nb_frames = frame_size/enc_frame_size; + +#ifndef DISABLE_FLOAT_API + if (analysis_read_pos_bak!= -1) + { + /* Reset analysis position to the beginning of the first frame so we + can use it one frame at a time. */ + st->analysis.read_pos = analysis_read_pos_bak; + st->analysis.read_subframe = analysis_read_subframe_bak; + } +#endif + + /* Worst cases: + * 2 frames: Code 2 with different compressed sizes + * >2 frames: Code 3 VBR */ + max_header_bytes = nb_frames == 2 ? 3 : (2+(nb_frames-1)*2); + + if (st->use_vbr || st->user_bitrate_bps==OPUS_BITRATE_MAX) + repacketize_len = out_data_bytes; + else { + celt_assert(cbr_bytes>=0); + repacketize_len = IMIN(cbr_bytes, out_data_bytes); + } + max_len_sum = nb_frames + repacketize_len - max_header_bytes; + + ALLOC(tmp_data, max_len_sum, unsigned char); + curr_data = tmp_data; + ALLOC(rp, 1, OpusRepacketizer); + opus_repacketizer_init(rp); + + + int bak_to_mono = st->silk_mode.toMono; + if (bak_to_mono) + st->force_channels = 1; + else + st->prev_channels = st->stream_channels; + + for (i=0;isilk_mode.toMono = 0; + st->nonfinal_frame = i<(nb_frames-1); + + /* When switching from SILK/Hybrid to CELT, only ask for a switch at the last frame */ + frame_to_celt = to_celt && i==nb_frames-1; + frame_redundancy = redundancy && (frame_to_celt || (!to_celt && i==0)); + + curr_max = IMIN(3*st->bitrate_bps/(3*8*st->Fs/enc_frame_size), max_len_sum/nb_frames); +#ifdef ENABLE_DRED + curr_max = IMIN(curr_max, (max_len_sum-3*dred_bitrate_bps/(3*8*st->Fs/frame_size))/nb_frames); + if (first_frame) curr_max += 3*dred_bitrate_bps/(3*8*st->Fs/frame_size); +#endif + curr_max = IMIN(max_len_sum-tot_size, curr_max); +#ifndef DISABLE_FLOAT_API + if (analysis_read_pos_bak != -1) { + is_silence = is_digital_silence(pcm, frame_size, st->channels, lsb_depth); + /* Get analysis for current frame. */ + tonality_get_info(&st->analysis, &analysis_info, enc_frame_size); + } +#endif + + tmp_len = opus_encode_frame_native(st, pcm+i*(st->channels*enc_frame_size), enc_frame_size, curr_data, curr_max, float_api, first_frame, +#ifdef ENABLE_DRED + dred_bitrate_bps, +#endif +#ifndef DISABLE_FLOAT_API + &analysis_info, + is_silence, +#endif + frame_redundancy, celt_to_silk, prefill, + equiv_rate, frame_to_celt + ); + if (tmp_len<0) + { + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } else if (tmp_len==1) { + dtx_count++; + } + ret = opus_repacketizer_cat(rp, curr_data, tmp_len); + + if (ret<0) + { + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } + tot_size += tmp_len; + curr_data += tmp_len; + } + ret = opus_repacketizer_out_range_impl(rp, 0, nb_frames, data, repacketize_len, 0, !st->use_vbr && (dtx_count != nb_frames), NULL, 0); + if (ret<0) + { + ret = OPUS_INTERNAL_ERROR; + } + st->silk_mode.toMono = bak_to_mono; + RESTORE_STACK; + return ret; + } else { + ret = opus_encode_frame_native(st, pcm, frame_size, data, max_data_bytes, float_api, 1, +#ifdef ENABLE_DRED + dred_bitrate_bps, +#endif +#ifndef DISABLE_FLOAT_API + &analysis_info, + is_silence, +#endif + redundancy, celt_to_silk, prefill, + equiv_rate, to_celt + ); + RESTORE_STACK; + return ret; + } +} + +static opus_int32 opus_encode_frame_native(OpusEncoder *st, const opus_val16 *pcm, int frame_size, + unsigned char *data, opus_int32 max_data_bytes, + int float_api, int first_frame, +#ifdef ENABLE_DRED + opus_int32 dred_bitrate_bps, +#endif +#ifndef DISABLE_FLOAT_API + AnalysisInfo *analysis_info, int is_silence, +#endif + int redundancy, int celt_to_silk, int prefill, + opus_int32 equiv_rate, int to_celt) +{ + void *silk_enc; + CELTEncoder *celt_enc; + const CELTMode *celt_mode; + int i; + int ret=0; + opus_int32 nBytes; + ec_enc enc; + int bytes_target; + int start_band = 0; + int redundancy_bytes = 0; /* Number of bytes to use for redundancy frame */ + int nb_compr_bytes; + opus_uint32 redundant_rng = 0; + int cutoff_Hz; + int hp_freq_smth1; + opus_val16 HB_gain; + int apply_padding; + int frame_rate; + int curr_bandwidth; + int delay_compensation; + int total_buffer; + opus_int activity = VAD_NO_DECISION; + VARDECL(opus_val16, pcm_buf); + VARDECL(opus_val16, tmp_prefill); + SAVE_STACK; + + st->rangeFinal = 0; + silk_enc = (char*)st+st->silk_enc_offset; + celt_enc = (CELTEncoder*)((char*)st+st->celt_enc_offset); + celt_encoder_ctl(celt_enc, CELT_GET_MODE(&celt_mode)); + curr_bandwidth = st->bandwidth; + if (st->application == OPUS_APPLICATION_RESTRICTED_LOWDELAY) + delay_compensation = 0; + else + delay_compensation = st->delay_compensation; + total_buffer = delay_compensation; + + frame_rate = st->Fs/frame_size; + +#ifndef DISABLE_FLOAT_API + if (is_silence) + { + activity = !is_silence; + } else if (analysis_info->valid) + { + activity = analysis_info->activity_probability >= DTX_ACTIVITY_THRESHOLD; + if (!activity) + { + /* Mark as active if this noise frame is sufficiently loud */ + opus_val32 noise_energy = compute_frame_energy(pcm, frame_size, st->channels, st->arch); + activity = st->peak_signal_energy < (PSEUDO_SNR_THRESHOLD * noise_energy); + } + } +#endif + + /* For the first frame at a new SILK bandwidth */ + if (st->silk_bw_switch) + { + redundancy = 1; + celt_to_silk = 1; + st->silk_bw_switch = 0; + /* Do a prefill without resetting the sampling rate control. */ + prefill=2; + } + + /* If we decided to go with CELT, make sure redundancy is off, no matter what + we decided earlier. */ + if (st->mode == MODE_CELT_ONLY) + redundancy = 0; + + if (redundancy) + { + redundancy_bytes = compute_redundancy_bytes(max_data_bytes, st->bitrate_bps, frame_rate, st->stream_channels); + if (redundancy_bytes == 0) + redundancy = 0; + } + + /* printf("%d %d %d %d\n", st->bitrate_bps, st->stream_channels, st->mode, curr_bandwidth); */ + bytes_target = IMIN(max_data_bytes-redundancy_bytes, st->bitrate_bps * frame_size / (st->Fs * 8)) - 1; + + data += 1; + + ec_enc_init(&enc, data, max_data_bytes-1); + + ALLOC(pcm_buf, (total_buffer+frame_size)*st->channels, opus_val16); + OPUS_COPY(pcm_buf, &st->delay_buffer[(st->encoder_buffer-total_buffer)*st->channels], total_buffer*st->channels); + + if (st->mode == MODE_CELT_ONLY) + hp_freq_smth1 = silk_LSHIFT( silk_lin2log( VARIABLE_HP_MIN_CUTOFF_HZ ), 8 ); + else + hp_freq_smth1 = ((silk_encoder*)silk_enc)->state_Fxx[0].sCmn.variable_HP_smth1_Q15; + + st->variable_HP_smth2_Q15 = silk_SMLAWB( st->variable_HP_smth2_Q15, + hp_freq_smth1 - st->variable_HP_smth2_Q15, SILK_FIX_CONST( VARIABLE_HP_SMTH_COEF2, 16 ) ); + + /* convert from log scale to Hertz */ + cutoff_Hz = silk_log2lin( silk_RSHIFT( st->variable_HP_smth2_Q15, 8 ) ); + + if (st->application == OPUS_APPLICATION_VOIP) + { + hp_cutoff(pcm, cutoff_Hz, &pcm_buf[total_buffer*st->channels], st->hp_mem, frame_size, st->channels, st->Fs, st->arch); + +#ifdef ENABLE_OSCE_TRAINING_DATA + /* write out high pass filtered clean signal*/ + static FILE *fout =NULL; + if (fout == NULL) + { + fout = fopen("clean_hp.s16", "wb"); + } + + { + int idx; + opus_int16 tmp; + for (idx = 0; idx < frame_size; idx++) + { + tmp = (opus_int16) (32768 * pcm_buf[total_buffer + idx] + 0.5f); + fwrite(&tmp, sizeof(tmp), 1, fout); + } + } +#endif + } else { + dc_reject(pcm, 3, &pcm_buf[total_buffer*st->channels], st->hp_mem, frame_size, st->channels, st->Fs); + } +#ifndef FIXED_POINT + if (float_api) + { + opus_val32 sum; + sum = celt_inner_prod(&pcm_buf[total_buffer*st->channels], &pcm_buf[total_buffer*st->channels], frame_size*st->channels, st->arch); + /* This should filter out both NaNs and ridiculous signals that could + cause NaNs further down. */ + if (!(sum < 1e9f) || celt_isnan(sum)) + { + OPUS_CLEAR(&pcm_buf[total_buffer*st->channels], frame_size*st->channels); + st->hp_mem[0] = st->hp_mem[1] = st->hp_mem[2] = st->hp_mem[3] = 0; + } + } +#else + (void)float_api; +#endif + +#ifdef ENABLE_DRED + if ( st->dred_duration > 0 && st->dred_encoder.loaded ) { + int frame_size_400Hz; + /* DRED Encoder */ + dred_compute_latents( &st->dred_encoder, &pcm_buf[total_buffer*st->channels], frame_size, total_buffer, st->arch ); + frame_size_400Hz = frame_size*400/st->Fs; + OPUS_MOVE(&st->activity_mem[frame_size_400Hz], st->activity_mem, 4*DRED_MAX_FRAMES-frame_size_400Hz); + for (i=0;iactivity_mem[i] = activity; + } else { + st->dred_encoder.latents_buffer_fill = 0; + OPUS_CLEAR(st->activity_mem, DRED_MAX_FRAMES); + } +#endif + + /* SILK processing */ + HB_gain = Q15ONE; + if (st->mode != MODE_CELT_ONLY) + { + opus_int32 total_bitRate, celt_rate; +#ifdef FIXED_POINT + const opus_int16 *pcm_silk; +#else + VARDECL(opus_int16, pcm_silk); + ALLOC(pcm_silk, st->channels*frame_size, opus_int16); +#endif + + /* Distribute bits between SILK and CELT */ + total_bitRate = 8 * bytes_target * frame_rate; + if( st->mode == MODE_HYBRID ) { + /* Base rate for SILK */ + st->silk_mode.bitRate = compute_silk_rate_for_hybrid(total_bitRate, + curr_bandwidth, st->Fs == 50 * frame_size, st->use_vbr, st->silk_mode.LBRR_coded, + st->stream_channels); + if (!st->energy_masking) + { + /* Increasingly attenuate high band when it gets allocated fewer bits */ + celt_rate = total_bitRate - st->silk_mode.bitRate; + HB_gain = Q15ONE - SHR32(celt_exp2(-celt_rate * QCONST16(1.f/1024, 10)), 1); + } + } else { + /* SILK gets all bits */ + st->silk_mode.bitRate = total_bitRate; + } + + /* Surround masking for SILK */ + if (st->energy_masking && st->use_vbr && !st->lfe) + { + opus_val32 mask_sum=0; + opus_val16 masking_depth; + opus_int32 rate_offset; + int c; + int end = 17; + opus_int16 srate = 16000; + if (st->bandwidth == OPUS_BANDWIDTH_NARROWBAND) + { + end = 13; + srate = 8000; + } else if (st->bandwidth == OPUS_BANDWIDTH_MEDIUMBAND) + { + end = 15; + srate = 12000; + } + for (c=0;cchannels;c++) + { + for(i=0;ienergy_masking[21*c+i], + QCONST16(.5f, DB_SHIFT)), -QCONST16(2.0f, DB_SHIFT)); + if (mask > 0) + mask = HALF16(mask); + mask_sum += mask; + } + } + /* Conservative rate reduction, we cut the masking in half */ + masking_depth = mask_sum / end*st->channels; + masking_depth += QCONST16(.2f, DB_SHIFT); + rate_offset = (opus_int32)PSHR32(MULT16_16(srate, masking_depth), DB_SHIFT); + rate_offset = MAX32(rate_offset, -2*st->silk_mode.bitRate/3); + /* Split the rate change between the SILK and CELT part for hybrid. */ + if (st->bandwidth==OPUS_BANDWIDTH_SUPERWIDEBAND || st->bandwidth==OPUS_BANDWIDTH_FULLBAND) + st->silk_mode.bitRate += 3*rate_offset/5; + else + st->silk_mode.bitRate += rate_offset; + } + + st->silk_mode.payloadSize_ms = 1000 * frame_size / st->Fs; + st->silk_mode.nChannelsAPI = st->channels; + st->silk_mode.nChannelsInternal = st->stream_channels; + if (curr_bandwidth == OPUS_BANDWIDTH_NARROWBAND) { + st->silk_mode.desiredInternalSampleRate = 8000; + } else if (curr_bandwidth == OPUS_BANDWIDTH_MEDIUMBAND) { + st->silk_mode.desiredInternalSampleRate = 12000; + } else { + celt_assert( st->mode == MODE_HYBRID || curr_bandwidth == OPUS_BANDWIDTH_WIDEBAND ); + st->silk_mode.desiredInternalSampleRate = 16000; + } + if( st->mode == MODE_HYBRID ) { + /* Don't allow bandwidth reduction at lowest bitrates in hybrid mode */ + st->silk_mode.minInternalSampleRate = 16000; + } else { + st->silk_mode.minInternalSampleRate = 8000; + } + + st->silk_mode.maxInternalSampleRate = 16000; + if (st->mode == MODE_SILK_ONLY) + { + opus_int32 effective_max_rate = frame_rate*max_data_bytes*8; + if (frame_rate > 50) + effective_max_rate = effective_max_rate*2/3; + if (effective_max_rate < 8000) + { + st->silk_mode.maxInternalSampleRate = 12000; + st->silk_mode.desiredInternalSampleRate = IMIN(12000, st->silk_mode.desiredInternalSampleRate); + } + if (effective_max_rate < 7000) + { + st->silk_mode.maxInternalSampleRate = 8000; + st->silk_mode.desiredInternalSampleRate = IMIN(8000, st->silk_mode.desiredInternalSampleRate); + } + } + + st->silk_mode.useCBR = !st->use_vbr; + + /* Call SILK encoder for the low band */ + + /* Max bits for SILK, counting ToC, redundancy bytes, and optionally redundancy. */ + st->silk_mode.maxBits = (max_data_bytes-1)*8; + if (redundancy && redundancy_bytes >= 2) + { + /* Counting 1 bit for redundancy position and 20 bits for flag+size (only for hybrid). */ + st->silk_mode.maxBits -= redundancy_bytes*8 + 1; + if (st->mode == MODE_HYBRID) + st->silk_mode.maxBits -= 20; + } + if (st->silk_mode.useCBR) + { + /* When we're in CBR mode, but we have non-SILK data to encode, switch SILK to VBR with cap to + save on complexity. Any variations will be absorbed by CELT and/or DRED and we can still + produce a constant bitrate without wasting bits. */ +#ifdef ENABLE_DRED + if (st->mode == MODE_HYBRID || dred_bitrate_bps > 0) +#else + if (st->mode == MODE_HYBRID) +#endif + { + /* Allow SILK to steal up to 25% of the remaining bits */ + opus_int16 other_bits = IMAX(0, st->silk_mode.maxBits - st->silk_mode.bitRate * frame_size / st->Fs); + st->silk_mode.maxBits = IMAX(0, st->silk_mode.maxBits - other_bits*3/4); + st->silk_mode.useCBR = 0; + } + } else { + /* Constrained VBR. */ + if (st->mode == MODE_HYBRID) + { + /* Compute SILK bitrate corresponding to the max total bits available */ + opus_int32 maxBitRate = compute_silk_rate_for_hybrid(st->silk_mode.maxBits*st->Fs / frame_size, + curr_bandwidth, st->Fs == 50 * frame_size, st->use_vbr, st->silk_mode.LBRR_coded, + st->stream_channels); + st->silk_mode.maxBits = maxBitRate * frame_size / st->Fs; + } + } + + if (prefill) + { + opus_int32 zero=0; + int prefill_offset; + /* Use a smooth onset for the SILK prefill to avoid the encoder trying to encode + a discontinuity. The exact location is what we need to avoid leaving any "gap" + in the audio when mixing with the redundant CELT frame. Here we can afford to + overwrite st->delay_buffer because the only thing that uses it before it gets + rewritten is tmp_prefill[] and even then only the part after the ramp really + gets used (rather than sent to the encoder and discarded) */ + prefill_offset = st->channels*(st->encoder_buffer-st->delay_compensation-st->Fs/400); + gain_fade(st->delay_buffer+prefill_offset, st->delay_buffer+prefill_offset, + 0, Q15ONE, celt_mode->overlap, st->Fs/400, st->channels, celt_mode->window, st->Fs); + OPUS_CLEAR(st->delay_buffer, prefill_offset); +#ifdef FIXED_POINT + pcm_silk = st->delay_buffer; +#else + for (i=0;iencoder_buffer*st->channels;i++) + pcm_silk[i] = FLOAT2INT16(st->delay_buffer[i]); +#endif + silk_Encode( silk_enc, &st->silk_mode, pcm_silk, st->encoder_buffer, NULL, &zero, prefill, activity ); + /* Prevent a second switch in the real encode call. */ + st->silk_mode.opusCanSwitch = 0; + } + +#ifdef FIXED_POINT + pcm_silk = pcm_buf+total_buffer*st->channels; +#else + for (i=0;ichannels;i++) + pcm_silk[i] = FLOAT2INT16(pcm_buf[total_buffer*st->channels + i]); +#endif + ret = silk_Encode( silk_enc, &st->silk_mode, pcm_silk, frame_size, &enc, &nBytes, 0, activity ); + if( ret ) { + /*fprintf (stderr, "SILK encode error: %d\n", ret);*/ + /* Handle error */ + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } + + /* Extract SILK internal bandwidth for signaling in first byte */ + if( st->mode == MODE_SILK_ONLY ) { + if( st->silk_mode.internalSampleRate == 8000 ) { + curr_bandwidth = OPUS_BANDWIDTH_NARROWBAND; + } else if( st->silk_mode.internalSampleRate == 12000 ) { + curr_bandwidth = OPUS_BANDWIDTH_MEDIUMBAND; + } else if( st->silk_mode.internalSampleRate == 16000 ) { + curr_bandwidth = OPUS_BANDWIDTH_WIDEBAND; + } + } else { + celt_assert( st->silk_mode.internalSampleRate == 16000 ); + } + + st->silk_mode.opusCanSwitch = st->silk_mode.switchReady && !st->nonfinal_frame; + + if (nBytes==0) + { + st->rangeFinal = 0; + data[-1] = gen_toc(st->mode, st->Fs/frame_size, curr_bandwidth, st->stream_channels); + RESTORE_STACK; + return 1; + } + + /* FIXME: How do we allocate the redundancy for CBR? */ + if (st->silk_mode.opusCanSwitch) + { + redundancy_bytes = compute_redundancy_bytes(max_data_bytes, st->bitrate_bps, frame_rate, st->stream_channels); + redundancy = (redundancy_bytes != 0); + celt_to_silk = 0; + st->silk_bw_switch = 1; + } + } + + /* CELT processing */ + { + int endband=21; + + switch(curr_bandwidth) + { + case OPUS_BANDWIDTH_NARROWBAND: + endband = 13; + break; + case OPUS_BANDWIDTH_MEDIUMBAND: + case OPUS_BANDWIDTH_WIDEBAND: + endband = 17; + break; + case OPUS_BANDWIDTH_SUPERWIDEBAND: + endband = 19; + break; + case OPUS_BANDWIDTH_FULLBAND: + endband = 21; + break; + } + celt_encoder_ctl(celt_enc, CELT_SET_END_BAND(endband)); + celt_encoder_ctl(celt_enc, CELT_SET_CHANNELS(st->stream_channels)); + } + celt_encoder_ctl(celt_enc, OPUS_SET_BITRATE(OPUS_BITRATE_MAX)); + if (st->mode != MODE_SILK_ONLY) + { + opus_val32 celt_pred=2; + /* We may still decide to disable prediction later */ + if (st->silk_mode.reducedDependency) + celt_pred = 0; + celt_encoder_ctl(celt_enc, CELT_SET_PREDICTION(celt_pred)); + } + + ALLOC(tmp_prefill, st->channels*st->Fs/400, opus_val16); + if (st->mode != MODE_SILK_ONLY && st->mode != st->prev_mode && st->prev_mode > 0) + { + OPUS_COPY(tmp_prefill, &st->delay_buffer[(st->encoder_buffer-total_buffer-st->Fs/400)*st->channels], st->channels*st->Fs/400); + } + + if (st->channels*(st->encoder_buffer-(frame_size+total_buffer)) > 0) + { + OPUS_MOVE(st->delay_buffer, &st->delay_buffer[st->channels*frame_size], st->channels*(st->encoder_buffer-frame_size-total_buffer)); + OPUS_COPY(&st->delay_buffer[st->channels*(st->encoder_buffer-frame_size-total_buffer)], + &pcm_buf[0], + (frame_size+total_buffer)*st->channels); + } else { + OPUS_COPY(st->delay_buffer, &pcm_buf[(frame_size+total_buffer-st->encoder_buffer)*st->channels], st->encoder_buffer*st->channels); + } + /* gain_fade() and stereo_fade() need to be after the buffer copying + because we don't want any of this to affect the SILK part */ + if( st->prev_HB_gain < Q15ONE || HB_gain < Q15ONE ) { + gain_fade(pcm_buf, pcm_buf, + st->prev_HB_gain, HB_gain, celt_mode->overlap, frame_size, st->channels, celt_mode->window, st->Fs); + } + st->prev_HB_gain = HB_gain; + if (st->mode != MODE_HYBRID || st->stream_channels==1) + { + if (equiv_rate > 32000) + st->silk_mode.stereoWidth_Q14 = 16384; + else if (equiv_rate < 16000) + st->silk_mode.stereoWidth_Q14 = 0; + else + st->silk_mode.stereoWidth_Q14 = 16384 - 2048*(opus_int32)(32000-equiv_rate)/(equiv_rate-14000); + } + if( !st->energy_masking && st->channels == 2 ) { + /* Apply stereo width reduction (at low bitrates) */ + if( st->hybrid_stereo_width_Q14 < (1 << 14) || st->silk_mode.stereoWidth_Q14 < (1 << 14) ) { + opus_val16 g1, g2; + g1 = st->hybrid_stereo_width_Q14; + g2 = (opus_val16)(st->silk_mode.stereoWidth_Q14); +#ifdef FIXED_POINT + g1 = g1==16384 ? Q15ONE : SHL16(g1,1); + g2 = g2==16384 ? Q15ONE : SHL16(g2,1); +#else + g1 *= (1.f/16384); + g2 *= (1.f/16384); +#endif + stereo_fade(pcm_buf, pcm_buf, g1, g2, celt_mode->overlap, + frame_size, st->channels, celt_mode->window, st->Fs); + st->hybrid_stereo_width_Q14 = st->silk_mode.stereoWidth_Q14; + } + } + + if ( st->mode != MODE_CELT_ONLY && ec_tell(&enc)+17+20*(st->mode == MODE_HYBRID) <= 8*(max_data_bytes-1)) + { + /* For SILK mode, the redundancy is inferred from the length */ + if (st->mode == MODE_HYBRID) + ec_enc_bit_logp(&enc, redundancy, 12); + if (redundancy) + { + int max_redundancy; + ec_enc_bit_logp(&enc, celt_to_silk, 1); + if (st->mode == MODE_HYBRID) + { + /* Reserve the 8 bits needed for the redundancy length, + and at least a few bits for CELT if possible */ + max_redundancy = (max_data_bytes-1)-((ec_tell(&enc)+8+3+7)>>3); + } + else + max_redundancy = (max_data_bytes-1)-((ec_tell(&enc)+7)>>3); + /* Target the same bit-rate for redundancy as for the rest, + up to a max of 257 bytes */ + redundancy_bytes = IMIN(max_redundancy, redundancy_bytes); + redundancy_bytes = IMIN(257, IMAX(2, redundancy_bytes)); + if (st->mode == MODE_HYBRID) + ec_enc_uint(&enc, redundancy_bytes-2, 256); + } + } else { + redundancy = 0; + } + + if (!redundancy) + { + st->silk_bw_switch = 0; + redundancy_bytes = 0; + } + if (st->mode != MODE_CELT_ONLY)start_band=17; + + if (st->mode == MODE_SILK_ONLY) + { + ret = (ec_tell(&enc)+7)>>3; + ec_enc_done(&enc); + nb_compr_bytes = ret; + } else { + nb_compr_bytes = (max_data_bytes-1)-redundancy_bytes; +#ifdef ENABLE_DRED + if (st->dred_duration > 0) + { + int max_celt_bytes; + opus_int32 dred_bytes = dred_bitrate_bps/(frame_rate*8); + /* Allow CELT to steal up to 25% of the remaining bits. */ + max_celt_bytes = nb_compr_bytes - dred_bytes*3/4; + /* But try to give CELT at least 5 bytes to prevent a mismatch with + the redundancy signaling. */ + max_celt_bytes = IMAX((ec_tell(&enc)+7)/8 + 5, max_celt_bytes); + /* Subject to the original max. */ + nb_compr_bytes = IMIN(nb_compr_bytes, max_celt_bytes); + } +#endif + ec_enc_shrink(&enc, nb_compr_bytes); + } + +#ifndef DISABLE_FLOAT_API + if (redundancy || st->mode != MODE_SILK_ONLY) + celt_encoder_ctl(celt_enc, CELT_SET_ANALYSIS(analysis_info)); +#endif + if (st->mode == MODE_HYBRID) { + SILKInfo info; + info.signalType = st->silk_mode.signalType; + info.offset = st->silk_mode.offset; + celt_encoder_ctl(celt_enc, CELT_SET_SILK_INFO(&info)); + } + + /* 5 ms redundant frame for CELT->SILK */ + if (redundancy && celt_to_silk) + { + int err; + celt_encoder_ctl(celt_enc, CELT_SET_START_BAND(0)); + celt_encoder_ctl(celt_enc, OPUS_SET_VBR(0)); + celt_encoder_ctl(celt_enc, OPUS_SET_BITRATE(OPUS_BITRATE_MAX)); + err = celt_encode_with_ec(celt_enc, pcm_buf, st->Fs/200, data+nb_compr_bytes, redundancy_bytes, NULL); + if (err < 0) + { + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } + celt_encoder_ctl(celt_enc, OPUS_GET_FINAL_RANGE(&redundant_rng)); + celt_encoder_ctl(celt_enc, OPUS_RESET_STATE); + } + + celt_encoder_ctl(celt_enc, CELT_SET_START_BAND(start_band)); + + if (st->mode != MODE_SILK_ONLY) + { + celt_encoder_ctl(celt_enc, OPUS_SET_VBR(st->use_vbr)); + if (st->mode == MODE_HYBRID) + { + if( st->use_vbr ) { + celt_encoder_ctl(celt_enc, OPUS_SET_BITRATE(st->bitrate_bps-st->silk_mode.bitRate)); + celt_encoder_ctl(celt_enc, OPUS_SET_VBR_CONSTRAINT(0)); + } + } else { + if (st->use_vbr) + { + celt_encoder_ctl(celt_enc, OPUS_SET_VBR(1)); + celt_encoder_ctl(celt_enc, OPUS_SET_VBR_CONSTRAINT(st->vbr_constraint)); + celt_encoder_ctl(celt_enc, OPUS_SET_BITRATE(st->bitrate_bps)); + } + } +#ifdef ENABLE_DRED + /* When Using DRED CBR, we can actually make the CELT part VBR and have DRED pick up the slack. */ + if (!st->use_vbr && st->dred_duration > 0) + { + opus_int32 celt_bitrate = st->bitrate_bps; + celt_encoder_ctl(celt_enc, OPUS_SET_VBR(1)); + celt_encoder_ctl(celt_enc, OPUS_SET_VBR_CONSTRAINT(0)); + if (st->mode == MODE_HYBRID) { + celt_bitrate -= st->silk_mode.bitRate; + } + celt_encoder_ctl(celt_enc, OPUS_SET_BITRATE(celt_bitrate)); + } +#endif + if (st->mode != st->prev_mode && st->prev_mode > 0) + { + unsigned char dummy[2]; + celt_encoder_ctl(celt_enc, OPUS_RESET_STATE); + + /* Prefilling */ + celt_encode_with_ec(celt_enc, tmp_prefill, st->Fs/400, dummy, 2, NULL); + celt_encoder_ctl(celt_enc, CELT_SET_PREDICTION(0)); + } + /* If false, we already busted the budget and we'll end up with a "PLC frame" */ + if (ec_tell(&enc) <= 8*nb_compr_bytes) + { + ret = celt_encode_with_ec(celt_enc, pcm_buf, frame_size, NULL, nb_compr_bytes, &enc); + if (ret < 0) + { + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } + /* Put CELT->SILK redundancy data in the right place. */ + if (redundancy && celt_to_silk && st->mode==MODE_HYBRID && nb_compr_bytes != ret) + { + OPUS_MOVE(data+ret, data+nb_compr_bytes, redundancy_bytes); + nb_compr_bytes = ret+redundancy_bytes; + } + } + } + + /* 5 ms redundant frame for SILK->CELT */ + if (redundancy && !celt_to_silk) + { + int err; + unsigned char dummy[2]; + int N2, N4; + N2 = st->Fs/200; + N4 = st->Fs/400; + + celt_encoder_ctl(celt_enc, OPUS_RESET_STATE); + celt_encoder_ctl(celt_enc, CELT_SET_START_BAND(0)); + celt_encoder_ctl(celt_enc, CELT_SET_PREDICTION(0)); + celt_encoder_ctl(celt_enc, OPUS_SET_VBR(0)); + celt_encoder_ctl(celt_enc, OPUS_SET_BITRATE(OPUS_BITRATE_MAX)); + + if (st->mode == MODE_HYBRID) + { + /* Shrink packet to what the encoder actually used. */ + nb_compr_bytes = ret; + ec_enc_shrink(&enc, nb_compr_bytes); + } + /* NOTE: We could speed this up slightly (at the expense of code size) by just adding a function that prefills the buffer */ + celt_encode_with_ec(celt_enc, pcm_buf+st->channels*(frame_size-N2-N4), N4, dummy, 2, NULL); + + err = celt_encode_with_ec(celt_enc, pcm_buf+st->channels*(frame_size-N2), N2, data+nb_compr_bytes, redundancy_bytes, NULL); + if (err < 0) + { + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } + celt_encoder_ctl(celt_enc, OPUS_GET_FINAL_RANGE(&redundant_rng)); + } + + + + /* Signalling the mode in the first byte */ + data--; + data[0] = gen_toc(st->mode, st->Fs/frame_size, curr_bandwidth, st->stream_channels); + + st->rangeFinal = enc.rng ^ redundant_rng; + + if (to_celt) + st->prev_mode = MODE_CELT_ONLY; + else + st->prev_mode = st->mode; + st->prev_channels = st->stream_channels; + st->prev_framesize = frame_size; + + st->first = 0; + + /* DTX decision */ +#ifndef DISABLE_FLOAT_API + if (st->use_dtx && (analysis_info->valid || is_silence)) + { + if (decide_dtx_mode(activity, &st->nb_no_activity_ms_Q1, 2*1000*frame_size/st->Fs)) + { + st->rangeFinal = 0; + data[0] = gen_toc(st->mode, st->Fs/frame_size, curr_bandwidth, st->stream_channels); + RESTORE_STACK; + return 1; + } + } else { + st->nb_no_activity_ms_Q1 = 0; + } +#endif + + /* In the unlikely case that the SILK encoder busted its target, tell + the decoder to call the PLC */ + if (ec_tell(&enc) > (max_data_bytes-1)*8) + { + if (max_data_bytes < 2) + { + RESTORE_STACK; + return OPUS_BUFFER_TOO_SMALL; + } + data[1] = 0; + ret = 1; + st->rangeFinal = 0; + } else if (st->mode==MODE_SILK_ONLY&&!redundancy) + { + /*When in LPC only mode it's perfectly + reasonable to strip off trailing zero bytes as + the required range decoder behavior is to + fill these in. This can't be done when the MDCT + modes are used because the decoder needs to know + the actual length for allocation purposes.*/ + while(ret>2&&data[ret]==0)ret--; + } + /* Count ToC and redundancy */ + ret += 1+redundancy_bytes; + apply_padding = !st->use_vbr; +#ifdef ENABLE_DRED + if (st->dred_duration > 0 && st->dred_encoder.loaded && first_frame) { + opus_extension_data extension; + unsigned char buf[DRED_MAX_DATA_SIZE]; + int dred_chunks; + int dred_bytes_left; + dred_chunks = IMIN((st->dred_duration+5)/4, DRED_NUM_REDUNDANCY_FRAMES/2); + if (st->use_vbr) dred_chunks = IMIN(dred_chunks, st->dred_target_chunks); + /* Remaining space for DRED, accounting for cost the 3 extra bytes for code 3, padding length, and extension number. */ + dred_bytes_left = IMIN(DRED_MAX_DATA_SIZE, max_data_bytes-ret-3); + /* Account for the extra bytes required to signal large padding length. */ + dred_bytes_left -= (dred_bytes_left+1+DRED_EXPERIMENTAL_BYTES)/255; + /* Check whether we actually have something to encode. */ + if (dred_chunks >= 1 && dred_bytes_left >= DRED_MIN_BYTES+DRED_EXPERIMENTAL_BYTES) { + int dred_bytes; +#ifdef DRED_EXPERIMENTAL_VERSION + /* Add temporary extension type and version. + These bytes will be removed once extension is finalized. */ + buf[0] = 'D'; + buf[1] = DRED_EXPERIMENTAL_VERSION; +#endif + dred_bytes = dred_encode_silk_frame(&st->dred_encoder, buf+DRED_EXPERIMENTAL_BYTES, dred_chunks, dred_bytes_left-DRED_EXPERIMENTAL_BYTES, + st->dred_q0, st->dred_dQ, st->dred_qmax, st->activity_mem, st->arch); + if (dred_bytes > 0) { + dred_bytes += DRED_EXPERIMENTAL_BYTES; + celt_assert(dred_bytes <= dred_bytes_left); + extension.id = DRED_EXTENSION_ID; + extension.frame = 0; + extension.data = buf; + extension.len = dred_bytes; + ret = opus_packet_pad_impl(data, ret, max_data_bytes, !st->use_vbr, &extension, 1); + if (ret < 0) + { + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } + apply_padding = 0; + } + } + } +#else + (void)first_frame; /* Avoids a warning about first_frame being unused. */ +#endif + if (apply_padding) + { + if (opus_packet_pad(data, ret, max_data_bytes) != OPUS_OK) + { + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } + ret = max_data_bytes; + } + RESTORE_STACK; + return ret; +} + +#ifdef FIXED_POINT + +#ifndef DISABLE_FLOAT_API +opus_int32 opus_encode_float(OpusEncoder *st, const float *pcm, int analysis_frame_size, + unsigned char *data, opus_int32 max_data_bytes) +{ + int i, ret; + int frame_size; + VARDECL(opus_int16, in); + ALLOC_STACK; + + frame_size = frame_size_select(analysis_frame_size, st->variable_duration, st->Fs); + if (frame_size <= 0) + { + RESTORE_STACK; + return OPUS_BAD_ARG; + } + ALLOC(in, frame_size*st->channels, opus_int16); + + for (i=0;ichannels;i++) + in[i] = FLOAT2INT16(pcm[i]); + ret = opus_encode_native(st, in, frame_size, data, max_data_bytes, 16, + pcm, analysis_frame_size, 0, -2, st->channels, downmix_float, 1); + RESTORE_STACK; + return ret; +} +#endif + +opus_int32 opus_encode(OpusEncoder *st, const opus_int16 *pcm, int analysis_frame_size, + unsigned char *data, opus_int32 out_data_bytes) +{ + int frame_size; + frame_size = frame_size_select(analysis_frame_size, st->variable_duration, st->Fs); + return opus_encode_native(st, pcm, frame_size, data, out_data_bytes, 16, + pcm, analysis_frame_size, 0, -2, st->channels, downmix_int, 0); +} + +#else +opus_int32 opus_encode(OpusEncoder *st, const opus_int16 *pcm, int analysis_frame_size, + unsigned char *data, opus_int32 max_data_bytes) +{ + int i, ret; + int frame_size; + VARDECL(float, in); + ALLOC_STACK; + + frame_size = frame_size_select(analysis_frame_size, st->variable_duration, st->Fs); + if (frame_size <= 0) + { + RESTORE_STACK; + return OPUS_BAD_ARG; + } + ALLOC(in, frame_size*st->channels, float); + + for (i=0;ichannels;i++) + in[i] = (1.0f/32768)*pcm[i]; + ret = opus_encode_native(st, in, frame_size, data, max_data_bytes, 16, + pcm, analysis_frame_size, 0, -2, st->channels, downmix_int, 0); + RESTORE_STACK; + return ret; +} +opus_int32 opus_encode_float(OpusEncoder *st, const float *pcm, int analysis_frame_size, + unsigned char *data, opus_int32 out_data_bytes) +{ + int frame_size; + frame_size = frame_size_select(analysis_frame_size, st->variable_duration, st->Fs); + return opus_encode_native(st, pcm, frame_size, data, out_data_bytes, 24, + pcm, analysis_frame_size, 0, -2, st->channels, downmix_float, 1); +} +#endif + + +int opus_encoder_ctl(OpusEncoder *st, int request, ...) +{ + int ret; + CELTEncoder *celt_enc; + va_list ap; + + ret = OPUS_OK; + va_start(ap, request); + + celt_enc = (CELTEncoder*)((char*)st+st->celt_enc_offset); + + switch (request) + { + case OPUS_SET_APPLICATION_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if ( (value != OPUS_APPLICATION_VOIP && value != OPUS_APPLICATION_AUDIO + && value != OPUS_APPLICATION_RESTRICTED_LOWDELAY) + || (!st->first && st->application != value)) + { + ret = OPUS_BAD_ARG; + break; + } + st->application = value; +#ifndef DISABLE_FLOAT_API + st->analysis.application = value; +#endif + } + break; + case OPUS_GET_APPLICATION_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->application; + } + break; + case OPUS_SET_BITRATE_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value != OPUS_AUTO && value != OPUS_BITRATE_MAX) + { + if (value <= 0) + goto bad_arg; + else if (value <= 500) + value = 500; + else if (value > (opus_int32)300000*st->channels) + value = (opus_int32)300000*st->channels; + } + st->user_bitrate_bps = value; + } + break; + case OPUS_GET_BITRATE_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = user_bitrate_to_bitrate(st, st->prev_framesize, 1276); + } + break; + case OPUS_SET_FORCE_CHANNELS_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if((value<1 || value>st->channels) && value != OPUS_AUTO) + { + goto bad_arg; + } + st->force_channels = value; + } + break; + case OPUS_GET_FORCE_CHANNELS_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->force_channels; + } + break; + case OPUS_SET_MAX_BANDWIDTH_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value < OPUS_BANDWIDTH_NARROWBAND || value > OPUS_BANDWIDTH_FULLBAND) + { + goto bad_arg; + } + st->max_bandwidth = value; + if (st->max_bandwidth == OPUS_BANDWIDTH_NARROWBAND) { + st->silk_mode.maxInternalSampleRate = 8000; + } else if (st->max_bandwidth == OPUS_BANDWIDTH_MEDIUMBAND) { + st->silk_mode.maxInternalSampleRate = 12000; + } else { + st->silk_mode.maxInternalSampleRate = 16000; + } + } + break; + case OPUS_GET_MAX_BANDWIDTH_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->max_bandwidth; + } + break; + case OPUS_SET_BANDWIDTH_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if ((value < OPUS_BANDWIDTH_NARROWBAND || value > OPUS_BANDWIDTH_FULLBAND) && value != OPUS_AUTO) + { + goto bad_arg; + } + st->user_bandwidth = value; + if (st->user_bandwidth == OPUS_BANDWIDTH_NARROWBAND) { + st->silk_mode.maxInternalSampleRate = 8000; + } else if (st->user_bandwidth == OPUS_BANDWIDTH_MEDIUMBAND) { + st->silk_mode.maxInternalSampleRate = 12000; + } else { + st->silk_mode.maxInternalSampleRate = 16000; + } + } + break; + case OPUS_GET_BANDWIDTH_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->bandwidth; + } + break; + case OPUS_SET_DTX_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>1) + { + goto bad_arg; + } + st->use_dtx = value; + } + break; + case OPUS_GET_DTX_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->use_dtx; + } + break; + case OPUS_SET_COMPLEXITY_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>10) + { + goto bad_arg; + } + st->silk_mode.complexity = value; + celt_encoder_ctl(celt_enc, OPUS_SET_COMPLEXITY(value)); + } + break; + case OPUS_GET_COMPLEXITY_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->silk_mode.complexity; + } + break; + case OPUS_SET_INBAND_FEC_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>2) + { + goto bad_arg; + } + st->fec_config = value; + st->silk_mode.useInBandFEC = (value != 0); + } + break; + case OPUS_GET_INBAND_FEC_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->fec_config; + } + break; + case OPUS_SET_PACKET_LOSS_PERC_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value < 0 || value > 100) + { + goto bad_arg; + } + st->silk_mode.packetLossPercentage = value; + celt_encoder_ctl(celt_enc, OPUS_SET_PACKET_LOSS_PERC(value)); + } + break; + case OPUS_GET_PACKET_LOSS_PERC_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->silk_mode.packetLossPercentage; + } + break; + case OPUS_SET_VBR_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>1) + { + goto bad_arg; + } + st->use_vbr = value; + st->silk_mode.useCBR = 1-value; + } + break; + case OPUS_GET_VBR_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->use_vbr; + } + break; + case OPUS_SET_VOICE_RATIO_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<-1 || value>100) + { + goto bad_arg; + } + st->voice_ratio = value; + } + break; + case OPUS_GET_VOICE_RATIO_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->voice_ratio; + } + break; + case OPUS_SET_VBR_CONSTRAINT_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>1) + { + goto bad_arg; + } + st->vbr_constraint = value; + } + break; + case OPUS_GET_VBR_CONSTRAINT_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->vbr_constraint; + } + break; + case OPUS_SET_SIGNAL_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value!=OPUS_AUTO && value!=OPUS_SIGNAL_VOICE && value!=OPUS_SIGNAL_MUSIC) + { + goto bad_arg; + } + st->signal_type = value; + } + break; + case OPUS_GET_SIGNAL_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->signal_type; + } + break; + case OPUS_GET_LOOKAHEAD_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->Fs/400; + if (st->application != OPUS_APPLICATION_RESTRICTED_LOWDELAY) + *value += st->delay_compensation; + } + break; + case OPUS_GET_SAMPLE_RATE_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->Fs; + } + break; + case OPUS_GET_FINAL_RANGE_REQUEST: + { + opus_uint32 *value = va_arg(ap, opus_uint32*); + if (!value) + { + goto bad_arg; + } + *value = st->rangeFinal; + } + break; + case OPUS_SET_LSB_DEPTH_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value<8 || value>24) + { + goto bad_arg; + } + st->lsb_depth=value; + } + break; + case OPUS_GET_LSB_DEPTH_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->lsb_depth; + } + break; + case OPUS_SET_EXPERT_FRAME_DURATION_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value != OPUS_FRAMESIZE_ARG && value != OPUS_FRAMESIZE_2_5_MS && + value != OPUS_FRAMESIZE_5_MS && value != OPUS_FRAMESIZE_10_MS && + value != OPUS_FRAMESIZE_20_MS && value != OPUS_FRAMESIZE_40_MS && + value != OPUS_FRAMESIZE_60_MS && value != OPUS_FRAMESIZE_80_MS && + value != OPUS_FRAMESIZE_100_MS && value != OPUS_FRAMESIZE_120_MS) + { + goto bad_arg; + } + st->variable_duration = value; + } + break; + case OPUS_GET_EXPERT_FRAME_DURATION_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->variable_duration; + } + break; + case OPUS_SET_PREDICTION_DISABLED_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if (value > 1 || value < 0) + goto bad_arg; + st->silk_mode.reducedDependency = value; + } + break; + case OPUS_GET_PREDICTION_DISABLED_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + goto bad_arg; + *value = st->silk_mode.reducedDependency; + } + break; + case OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>1) + { + goto bad_arg; + } + celt_encoder_ctl(celt_enc, OPUS_SET_PHASE_INVERSION_DISABLED(value)); + } + break; + case OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + celt_encoder_ctl(celt_enc, OPUS_GET_PHASE_INVERSION_DISABLED(value)); + } + break; +#ifdef ENABLE_DRED + case OPUS_SET_DRED_DURATION_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if(value<0 || value>DRED_MAX_FRAMES) + { + goto bad_arg; + } + st->dred_duration = value; + st->silk_mode.useDRED = !!value; + } + break; + case OPUS_GET_DRED_DURATION_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + *value = st->dred_duration; + } + break; +#endif + case OPUS_RESET_STATE: + { + void *silk_enc; + silk_EncControlStruct dummy; + char *start; + silk_enc = (char*)st+st->silk_enc_offset; +#ifndef DISABLE_FLOAT_API + tonality_analysis_reset(&st->analysis); +#endif + + start = (char*)&st->OPUS_ENCODER_RESET_START; + OPUS_CLEAR(start, sizeof(OpusEncoder) - (start - (char*)st)); + + celt_encoder_ctl(celt_enc, OPUS_RESET_STATE); + silk_InitEncoder( silk_enc, st->arch, &dummy ); +#ifdef ENABLE_DRED + /* Initialize DRED Encoder */ + dred_encoder_reset( &st->dred_encoder ); +#endif + st->stream_channels = st->channels; + st->hybrid_stereo_width_Q14 = 1 << 14; + st->prev_HB_gain = Q15ONE; + st->first = 1; + st->mode = MODE_HYBRID; + st->bandwidth = OPUS_BANDWIDTH_FULLBAND; + st->variable_HP_smth2_Q15 = silk_LSHIFT( silk_lin2log( VARIABLE_HP_MIN_CUTOFF_HZ ), 8 ); + } + break; + case OPUS_SET_FORCE_MODE_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + if ((value < MODE_SILK_ONLY || value > MODE_CELT_ONLY) && value != OPUS_AUTO) + { + goto bad_arg; + } + st->user_forced_mode = value; + } + break; + case OPUS_SET_LFE_REQUEST: + { + opus_int32 value = va_arg(ap, opus_int32); + st->lfe = value; + ret = celt_encoder_ctl(celt_enc, OPUS_SET_LFE(value)); + } + break; + case OPUS_SET_ENERGY_MASK_REQUEST: + { + opus_val16 *value = va_arg(ap, opus_val16*); + st->energy_masking = value; + ret = celt_encoder_ctl(celt_enc, OPUS_SET_ENERGY_MASK(value)); + } + break; + case OPUS_GET_IN_DTX_REQUEST: + { + opus_int32 *value = va_arg(ap, opus_int32*); + if (!value) + { + goto bad_arg; + } + if (st->silk_mode.useDTX && (st->prev_mode == MODE_SILK_ONLY || st->prev_mode == MODE_HYBRID)) { + /* DTX determined by Silk. */ + silk_encoder *silk_enc = (silk_encoder*)(void *)((char*)st+st->silk_enc_offset); + *value = silk_enc->state_Fxx[0].sCmn.noSpeechCounter >= NB_SPEECH_FRAMES_BEFORE_DTX; + /* Stereo: check second channel unless only the middle channel was encoded. */ + if(*value == 1 && st->silk_mode.nChannelsInternal == 2 && silk_enc->prev_decode_only_middle == 0) { + *value = silk_enc->state_Fxx[1].sCmn.noSpeechCounter >= NB_SPEECH_FRAMES_BEFORE_DTX; + } + } +#ifndef DISABLE_FLOAT_API + else if (st->use_dtx) { + /* DTX determined by Opus. */ + *value = st->nb_no_activity_ms_Q1 >= NB_SPEECH_FRAMES_BEFORE_DTX*20*2; + } +#endif + else { + *value = 0; + } + } + break; +#ifdef USE_WEIGHTS_FILE + case OPUS_SET_DNN_BLOB_REQUEST: + { + const unsigned char *data = va_arg(ap, const unsigned char *); + opus_int32 len = va_arg(ap, opus_int32); + if(len<0 || data == NULL) + { + goto bad_arg; + } +#ifdef ENABLE_DRED + ret = dred_encoder_load_model(&st->dred_encoder, data, len); +#endif + } + break; +#endif + case CELT_GET_MODE_REQUEST: + { + const CELTMode ** value = va_arg(ap, const CELTMode**); + if (!value) + { + goto bad_arg; + } + ret = celt_encoder_ctl(celt_enc, CELT_GET_MODE(value)); + } + break; + default: + /* fprintf(stderr, "unknown opus_encoder_ctl() request: %d", request);*/ + ret = OPUS_UNIMPLEMENTED; + break; + } + va_end(ap); + return ret; +bad_arg: + va_end(ap); + return OPUS_BAD_ARG; +} + +void opus_encoder_destroy(OpusEncoder *st) +{ + opus_free(st); +} diff --git a/port/third_party/opus/src/opus_private.h b/port/third_party/opus/src/opus_private.h new file mode 100644 index 00000000..279f5f95 --- /dev/null +++ b/port/third_party/opus/src/opus_private.h @@ -0,0 +1,223 @@ +/* Copyright (c) 2012 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#ifndef OPUS_PRIVATE_H +#define OPUS_PRIVATE_H + +#include "arch.h" +#include "opus.h" +#include "celt.h" + +#include /* va_list */ +#include /* offsetof */ + +struct OpusRepacketizer { + unsigned char toc; + int nb_frames; + const unsigned char *frames[48]; + opus_int16 len[48]; + int framesize; + const unsigned char *paddings[48]; + opus_int32 padding_len[48]; +}; + +typedef struct { + int id; + int frame; + const unsigned char *data; + opus_int32 len; +} opus_extension_data; + +typedef struct ChannelLayout { + int nb_channels; + int nb_streams; + int nb_coupled_streams; + unsigned char mapping[256]; +} ChannelLayout; + +typedef enum { + MAPPING_TYPE_NONE, + MAPPING_TYPE_SURROUND, + MAPPING_TYPE_AMBISONICS +} MappingType; + +struct OpusMSEncoder { + ChannelLayout layout; + int arch; + int lfe_stream; + int application; + int variable_duration; + MappingType mapping_type; + opus_int32 bitrate_bps; + /* Encoder states go here */ + /* then opus_val32 window_mem[channels*120]; */ + /* then opus_val32 preemph_mem[channels]; */ +}; + +struct OpusMSDecoder { + ChannelLayout layout; + /* Decoder states go here */ +}; + +int opus_multistream_encoder_ctl_va_list(struct OpusMSEncoder *st, int request, + va_list ap); +int opus_multistream_decoder_ctl_va_list(struct OpusMSDecoder *st, int request, + va_list ap); + +int validate_layout(const ChannelLayout *layout); +int get_left_channel(const ChannelLayout *layout, int stream_id, int prev); +int get_right_channel(const ChannelLayout *layout, int stream_id, int prev); +int get_mono_channel(const ChannelLayout *layout, int stream_id, int prev); + +typedef void (*opus_copy_channel_in_func)( + opus_val16 *dst, + int dst_stride, + const void *src, + int src_stride, + int src_channel, + int frame_size, + void *user_data +); + +typedef void (*opus_copy_channel_out_func)( + void *dst, + int dst_stride, + int dst_channel, + const opus_val16 *src, + int src_stride, + int frame_size, + void *user_data +); + +#define MODE_SILK_ONLY 1000 +#define MODE_HYBRID 1001 +#define MODE_CELT_ONLY 1002 + +#define OPUS_SET_VOICE_RATIO_REQUEST 11018 +#define OPUS_GET_VOICE_RATIO_REQUEST 11019 + +/** Configures the encoder's expected percentage of voice + * opposed to music or other signals. + * + * @note This interface is currently more aspiration than actuality. It's + * ultimately expected to bias an automatic signal classifier, but it currently + * just shifts the static bitrate to mode mapping around a little bit. + * + * @param[in] x int: Voice percentage in the range 0-100, inclusive. + * @hideinitializer */ +#define OPUS_SET_VOICE_RATIO(x) OPUS_SET_VOICE_RATIO_REQUEST, __opus_check_int(x) +/** Gets the encoder's configured voice ratio value, @see OPUS_SET_VOICE_RATIO + * + * @param[out] x int*: Voice percentage in the range 0-100, inclusive. + * @hideinitializer */ +#define OPUS_GET_VOICE_RATIO(x) OPUS_GET_VOICE_RATIO_REQUEST, __opus_check_int_ptr(x) + + +#define OPUS_SET_FORCE_MODE_REQUEST 11002 +#define OPUS_SET_FORCE_MODE(x) OPUS_SET_FORCE_MODE_REQUEST, __opus_check_int(x) + +typedef void (*downmix_func)(const void *, opus_val32 *, int, int, int, int, int); +void downmix_float(const void *_x, opus_val32 *sub, int subframe, int offset, int c1, int c2, int C); +void downmix_int(const void *_x, opus_val32 *sub, int subframe, int offset, int c1, int c2, int C); +int is_digital_silence(const opus_val16* pcm, int frame_size, int channels, int lsb_depth); + +int encode_size(int size, unsigned char *data); + +opus_int32 frame_size_select(opus_int32 frame_size, int variable_duration, opus_int32 Fs); + +opus_int32 opus_encode_native(OpusEncoder *st, const opus_val16 *pcm, int frame_size, + unsigned char *data, opus_int32 out_data_bytes, int lsb_depth, + const void *analysis_pcm, opus_int32 analysis_size, int c1, int c2, + int analysis_channels, downmix_func downmix, int float_api); + +int opus_decode_native(OpusDecoder *st, const unsigned char *data, opus_int32 len, + opus_val16 *pcm, int frame_size, int decode_fec, int self_delimited, + opus_int32 *packet_offset, int soft_clip, const OpusDRED *dred, opus_int32 dred_offset); + +/* Make sure everything is properly aligned. */ +static OPUS_INLINE int align(int i) +{ + struct foo {char c; union { void* p; opus_int32 i; opus_val32 v; } u;}; + + unsigned int alignment = offsetof(struct foo, u); + + /* Optimizing compilers should optimize div and multiply into and + for all sensible alignment values. */ + return ((i + alignment - 1) / alignment) * alignment; +} + +/* More than that is ridiculous for now (3 * max frames per packet)*/ +opus_int32 skip_extension(const unsigned char **data, opus_int32 len, opus_int32 *header_size); + +int opus_packet_parse_impl(const unsigned char *data, opus_int32 len, + int self_delimited, unsigned char *out_toc, + const unsigned char *frames[48], opus_int16 size[48], + int *payload_offset, opus_int32 *packet_offset, + const unsigned char **padding, opus_int32 *padding_len); + +opus_int32 opus_repacketizer_out_range_impl(OpusRepacketizer *rp, int begin, int end, + unsigned char *data, opus_int32 maxlen, int self_delimited, int pad, + const opus_extension_data *extensions, int nb_extensions); + +int pad_frame(unsigned char *data, opus_int32 len, opus_int32 new_len); + +int opus_multistream_encode_native +( + struct OpusMSEncoder *st, + opus_copy_channel_in_func copy_channel_in, + const void *pcm, + int analysis_frame_size, + unsigned char *data, + opus_int32 max_data_bytes, + int lsb_depth, + downmix_func downmix, + int float_api, + void *user_data +); + +int opus_multistream_decode_native( + struct OpusMSDecoder *st, + const unsigned char *data, + opus_int32 len, + void *pcm, + opus_copy_channel_out_func copy_channel_out, + int frame_size, + int decode_fec, + int soft_clip, + void *user_data +); + +opus_int32 opus_packet_extensions_parse(const unsigned char *data, opus_int32 len, opus_extension_data *extensions, opus_int32 *nb_extensions); + +opus_int32 opus_packet_extensions_generate(unsigned char *data, opus_int32 len, const opus_extension_data *extensions, opus_int32 nb_extensions, int pad); + +opus_int32 opus_packet_extensions_count(const unsigned char *data, opus_int32 len); + +opus_int32 opus_packet_pad_impl(unsigned char *data, opus_int32 len, opus_int32 new_len, int pad, const opus_extension_data *extensions, int nb_extensions); + +#endif /* OPUS_PRIVATE_H */ diff --git a/port/third_party/opus/src/repacketizer.c b/port/third_party/opus/src/repacketizer.c new file mode 100644 index 00000000..79798b02 --- /dev/null +++ b/port/third_party/opus/src/repacketizer.c @@ -0,0 +1,464 @@ +/* Copyright (c) 2011 Xiph.Org Foundation + Written by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "opus.h" +#include "opus_private.h" +#include "os_support.h" +#include "stack_alloc.h" + + +int opus_repacketizer_get_size(void) +{ + return sizeof(OpusRepacketizer); +} + +OpusRepacketizer *opus_repacketizer_init(OpusRepacketizer *rp) +{ + rp->nb_frames = 0; + return rp; +} + +OpusRepacketizer *opus_repacketizer_create(void) +{ + OpusRepacketizer *rp; + rp=(OpusRepacketizer *)opus_alloc(opus_repacketizer_get_size()); + if(rp==NULL)return NULL; + return opus_repacketizer_init(rp); +} + +void opus_repacketizer_destroy(OpusRepacketizer *rp) +{ + opus_free(rp); +} + +static int opus_repacketizer_cat_impl(OpusRepacketizer *rp, const unsigned char *data, opus_int32 len, int self_delimited) +{ + unsigned char tmp_toc; + int curr_nb_frames,ret; + /* Set of check ToC */ + if (len<1) return OPUS_INVALID_PACKET; + if (rp->nb_frames == 0) + { + rp->toc = data[0]; + rp->framesize = opus_packet_get_samples_per_frame(data, 8000); + } else if ((rp->toc&0xFC) != (data[0]&0xFC)) + { + /*fprintf(stderr, "toc mismatch: 0x%x vs 0x%x\n", rp->toc, data[0]);*/ + return OPUS_INVALID_PACKET; + } + curr_nb_frames = opus_packet_get_nb_frames(data, len); + if(curr_nb_frames<1) return OPUS_INVALID_PACKET; + + /* Check the 120 ms maximum packet size */ + if ((curr_nb_frames+rp->nb_frames)*rp->framesize > 960) + { + return OPUS_INVALID_PACKET; + } + + ret=opus_packet_parse_impl(data, len, self_delimited, &tmp_toc, &rp->frames[rp->nb_frames], &rp->len[rp->nb_frames], + NULL, NULL, &rp->paddings[rp->nb_frames], &rp->padding_len[rp->nb_frames]); + if(ret<1)return ret; + + /* set padding length to zero for all but the first frame */ + while (curr_nb_frames > 1) + { + rp->nb_frames++; + rp->padding_len[rp->nb_frames] = 0; + rp->paddings[rp->nb_frames] = NULL; + curr_nb_frames--; + } + rp->nb_frames++; + return OPUS_OK; +} + +int opus_repacketizer_cat(OpusRepacketizer *rp, const unsigned char *data, opus_int32 len) +{ + return opus_repacketizer_cat_impl(rp, data, len, 0); +} + +int opus_repacketizer_get_nb_frames(OpusRepacketizer *rp) +{ + return rp->nb_frames; +} + +opus_int32 opus_repacketizer_out_range_impl(OpusRepacketizer *rp, int begin, int end, + unsigned char *data, opus_int32 maxlen, int self_delimited, int pad, const opus_extension_data *extensions, int nb_extensions) +{ + int i, count; + opus_int32 tot_size; + opus_int16 *len; + const unsigned char **frames; + unsigned char * ptr; + int ones_begin=0, ones_end=0; + int ext_begin=0, ext_len=0; + int ext_count, total_ext_count; + VARDECL(opus_extension_data, all_extensions); + SAVE_STACK; + + if (begin<0 || begin>=end || end>rp->nb_frames) + { + /*fprintf(stderr, "%d %d %d\n", begin, end, rp->nb_frames);*/ + RESTORE_STACK; + return OPUS_BAD_ARG; + } + count = end-begin; + + len = rp->len+begin; + frames = rp->frames+begin; + if (self_delimited) + tot_size = 1 + (len[count-1]>=252); + else + tot_size = 0; + + /* figure out total number of extensions */ + total_ext_count = nb_extensions; + for (i=begin;ipaddings[i], rp->padding_len[i]); + if (n > 0) total_ext_count += n; + } + ALLOC(all_extensions, total_ext_count ? total_ext_count : ALLOC_NONE, opus_extension_data); + /* copy over any extensions that were passed in */ + for (ext_count=0;ext_countpaddings[i], rp->padding_len[i], + &all_extensions[ext_count], &frame_ext_count); + if (ret<0) + { + RESTORE_STACK; + return OPUS_INTERNAL_ERROR; + } + /* renumber the extension frame numbers */ + for (j=0;j maxlen) + { + RESTORE_STACK; + return OPUS_BUFFER_TOO_SMALL; + } + *ptr++ = rp->toc&0xFC; + } else if (count==2) + { + if (len[1] == len[0]) + { + /* Code 1 */ + tot_size += 2*len[0]+1; + if (tot_size > maxlen) + { + RESTORE_STACK; + return OPUS_BUFFER_TOO_SMALL; + } + *ptr++ = (rp->toc&0xFC) | 0x1; + } else { + /* Code 2 */ + tot_size += len[0]+len[1]+2+(len[0]>=252); + if (tot_size > maxlen) + { + RESTORE_STACK; + return OPUS_BUFFER_TOO_SMALL; + } + *ptr++ = (rp->toc&0xFC) | 0x2; + ptr += encode_size(len[0], ptr); + } + } + if (count > 2 || (pad && tot_size < maxlen) || ext_count > 0) + { + /* Code 3 */ + int vbr; + int pad_amount=0; + + /* Restart the process for the padding case */ + ptr = data; + if (self_delimited) + tot_size = 1 + (len[count-1]>=252); + else + tot_size = 0; + vbr = 0; + for (i=1;i=252) + len[i]; + tot_size += len[count-1]; + + if (tot_size > maxlen) + { + RESTORE_STACK; + return OPUS_BUFFER_TOO_SMALL; + } + *ptr++ = (rp->toc&0xFC) | 0x3; + *ptr++ = count | 0x80; + } else { + tot_size += count*len[0]+2; + if (tot_size > maxlen) + { + RESTORE_STACK; + return OPUS_BUFFER_TOO_SMALL; + } + *ptr++ = (rp->toc&0xFC) | 0x3; + *ptr++ = count; + } + pad_amount = pad ? (maxlen-tot_size) : 0; + if (ext_count>0) + { + /* figure out how much space we need for the extensions */ + ext_len = opus_packet_extensions_generate(NULL, maxlen-tot_size, all_extensions, ext_count, 0); + if (ext_len < 0) return ext_len; + if (!pad) + pad_amount = ext_len + ext_len/254 + 1; + } + if (pad_amount != 0) + { + int nb_255s; + data[1] |= 0x40; + nb_255s = (pad_amount-1)/255; + if (tot_size + ext_len + nb_255s + 1 > maxlen) + { + RESTORE_STACK; + return OPUS_BUFFER_TOO_SMALL; + } + ext_begin = tot_size+pad_amount-ext_len; + /* Prepend 0x01 padding */ + ones_begin = tot_size+nb_255s+1; + ones_end = tot_size+pad_amount-ext_len; + for (i=0;i 0) { + int ret = opus_packet_extensions_generate(&data[ext_begin], ext_len, all_extensions, ext_count, 0); + celt_assert(ret == ext_len); + } + for (i=ones_begin;inb_frames, data, maxlen, 0, 0, NULL, 0); +} + +opus_int32 opus_packet_pad_impl(unsigned char *data, opus_int32 len, opus_int32 new_len, int pad, const opus_extension_data *extensions, int nb_extensions) +{ + OpusRepacketizer rp; + opus_int32 ret; + VARDECL(unsigned char, copy); + SAVE_STACK; + if (len < 1) + return OPUS_BAD_ARG; + if (len==new_len) + return OPUS_OK; + else if (len > new_len) + return OPUS_BAD_ARG; + ALLOC(copy, len, unsigned char); + opus_repacketizer_init(&rp); + /* Moving payload to the end of the packet so we can do in-place padding */ + OPUS_COPY(copy, data, len); + ret = opus_repacketizer_cat(&rp, copy, len); + if (ret != OPUS_OK) + return ret; + ret = opus_repacketizer_out_range_impl(&rp, 0, rp.nb_frames, data, new_len, 0, pad, extensions, nb_extensions); + RESTORE_STACK; + return ret; +} + +int opus_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len) +{ + opus_int32 ret; + ALLOC_STACK; + ret = opus_packet_pad_impl(data, len, new_len, 1, NULL, 0); + RESTORE_STACK; + if (ret > 0) + return OPUS_OK; + else + return ret; +} + +opus_int32 opus_packet_unpad(unsigned char *data, opus_int32 len) +{ + OpusRepacketizer rp; + opus_int32 ret; + int i; + if (len < 1) + return OPUS_BAD_ARG; + opus_repacketizer_init(&rp); + ret = opus_repacketizer_cat(&rp, data, len); + if (ret < 0) + return ret; + /* Discard all padding and extensions. */ + for (i=0;i 0 && ret <= len); + return ret; +} + +int opus_multistream_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len, int nb_streams) +{ + int s; + int count; + unsigned char toc; + opus_int16 size[48]; + opus_int32 packet_offset; + opus_int32 amount; + + if (len < 1) + return OPUS_BAD_ARG; + if (len==new_len) + return OPUS_OK; + else if (len > new_len) + return OPUS_BAD_ARG; + amount = new_len - len; + /* Seek to last stream */ + for (s=0;snetwork_player_data.machine_index) ? L"Unmute voice" : + L"Mute voice"); + } + } if (network_votekick_host()) { offered[_scoreboard_item_kick] = TRUE; @@ -1989,6 +2004,18 @@ static void scoreboard_menu_pick( case _scoreboard_item_vote: network_votekick_request(player_index); break; + case _scoreboard_item_mute: + { + struct player_datum *player = player_try_and_get(scoreboard_menu.player_index); + + if (player) + { + long machine_index = player->network_player_data.machine_index; + + network_voice_mute_machine(machine_index, !network_voice_machine_muted(machine_index)); + } + break; + } case _scoreboard_item_kick: network_votekick_host_kick(player_index, FALSE); break; @@ -2318,6 +2345,21 @@ static void game_engine_rasterize_scoreboard( status_string, ping_string); row_color = has_teams ? &team_colors[PIN(player->team_index, 0, 1)] : &color; + /* port: a player talking (or muted) in voice chat, its speaker at the + right of the place column */ + if (network && (network_voice_machine_speaking(player->network_player_data.machine_index) || + network_voice_machine_muted(player->network_player_data.machine_index))) + { + rectangle2d icon; + short row_left = (short)(left + column * (SCOREBOARD_COLUMN_WIDTH + SCOREBOARD_COLUMN_GAP)); + + scoreboard_rectangle(&icon, bounds.x0, top, line_height, (short)(row_left + SCOREBOARD_PLACE_WIDTH - 24), + 20, 2 + row, 1); + icon.y0 = (short)(icon.y0 + (icon.y1 - icon.y0) / 6); + icon.y1 = (short)(icon.y1 - (icon.y1 - icon.y0) / 6); + network_voice_draw_icon(&icon, network_voice_machine_muted(player->network_player_data.machine_index), + alpha); + } /* port: another machine's player, picked with the pointer: lit under it (not under the menu), and its menu opened when clicked */ if (pointer_state > 0 && player->local_player_index == NONE) @@ -3818,6 +3860,68 @@ void game_engine_rasterize_message( return; } +/* port: who is talking in voice chat (network_voice.c), down the view's +left from below its middle: each machine's first player's name after its +speaker (this machine's own too, as it talks); while the scores are hidden */ +static void game_engine_rasterize_voice_speakers( + void) +{ + enum + { + MAXIMUM_SPEAKER_ROWS = 8, + }; + rectangle2d bounds = render.camera.window_bounds; + long font_index = hud_get_font_index(); + long machines_listed[MULTIPLAYER_MAXIMUM_PLAYERS]; + short listed_count = 0; + struct data_iterator iterator; + struct player_datum *player; + struct font_header *font; + short line_height; + short row = 0; + + if (font_index == NONE || !network_voice_available()) + return; + offset_rectangle2d(&bounds, -render.camera.viewport_bounds.x0, -render.camera.viewport_bounds.y0); + font = font_definition_get(font_index); + line_height = (short)(font->leading_height + font->descending_height + font->ascending_height); + if (line_height <= 0) + return; + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL && row < MAXIMUM_SPEAKER_ROWS) + { + long machine_index = player->network_player_data.machine_index; + real_argb_color color; + rectangle2d icon; + rectangle2d text; + short index; + + if (player->quit_out_of_game) + continue; + for (index = 0; index < listed_count && machines_listed[index] != machine_index; index++) + ; + if (index < listed_count || listed_count >= (short)NUMBEROF(machines_listed)) + continue; + machines_listed[listed_count++] = machine_index; + if (!network_voice_machine_speaking(machine_index)) + continue; + icon.x0 = (short)(bounds.x0 + 16); + icon.y0 = (short)(bounds.y0 + (bounds.y1 - bounds.y0) * 55 / 100 + row * line_height); + icon.x1 = (short)(icon.x0 + line_height); + icon.y1 = (short)(icon.y0 + line_height - 2); + network_voice_draw_icon(&icon, FALSE, 1.0f); + text = icon; + text.x0 = (short)(icon.x1 + 4); + text.x1 = bounds.x1; + text.y1 = (short)(icon.y0 + line_height); + color.alpha = 1.0f; + color.red = color.green = color.blue = 0.9f; + draw_string_set_draw_mode(font_index, NONE, 0, 0, &color); + rasterizer_draw_unicode_string(&text, NULL, NULL, 0, player->name); + row++; + } +} + static void game_engine_post_rasterize_in_game( void) { @@ -3870,6 +3974,10 @@ static void game_engine_post_rasterize_in_game( /* (port: the full-screen scoreboard's scroll forgotten) */ game_engine_scoreboard_closed(); } + /* (port: who is talking, in the first view, while the scores are not + shown) */ + if (fade <= 0.0f && local_player_index == local_player_get_next(NONE)) + game_engine_rasterize_voice_speakers(); game_engine_globals.hud_message_timers[local_player_index] = fade; diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index ef959fa3..d56041f0 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5594,6 +5594,10 @@ static void widget_instance_render_spinner_list( return; } +/* port/linux/game/menu_functions.c's: a text box drawn, at the bounds its +text was drawn in (the lobby's speaker icons) */ +void menu_functions_text_box_drawn(struct widget_instance *widget, rectangle2d const *bounds); + /* ---------- the mouse (desktop builds) The menus were made for a controller: the d-pad moves the focus through a @@ -6322,6 +6326,18 @@ static void widget_instance_render_recursive( clip_rect, offset, widget_instance_text_box_is_focused(widget)); + /* port: what the menus draw beside a text (the lobby's speaker + icons: port/linux/game/menu_functions.c), its bounds as it was + drawn in them (widget_instance_render_text_box), its font and + justification still set */ + { + rectangle2d text_bounds = definition->bounds; + + offset_rectangle2d(&text_bounds, offset.x, offset.y); + text_bounds.x0 += definition->horizontal_offset; + text_bounds.y0 += definition->vertical_offset; + menu_functions_text_box_drawn(widget, &text_bounds); + } break; case _ui_widget_type_spinner_list: diff --git a/source/main/main.c b/source/main/main.c index 31a23036..b4f4862b 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -380,6 +380,7 @@ symbols in this file: #include "networking/network_game_manager.h" #include "networking/network_server_manager.h" /* port: a co-op game's level won */ #include "network_coop.h" /* port: port/linux/game/network_coop.c */ +#include "network_voice.h" /* port: port/linux/game/network_voice.c */ #include "camera/director.h" #include "camera/observer.h" #include "cutscene/cinematics.h" @@ -3437,6 +3438,8 @@ void main_loop( /* automated system link tests (port/linux/game/network_test.c) */ network_test_update(main_globals.main_menu_scenario_loaded, main_globals.seconds_elapsed); + /* port: voice chat, in the lobby and in game (port/linux/game/network_voice.c) */ + network_voice_update(); connection = main_globals.connection; if (connection==_game_connection_network_client) { diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index 3ec3511a..640e186a 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -807,6 +807,8 @@ void network_distributed_kick(char const *names); /* port/linux/game/network_votekick.c's (the players' votes to kick) */ void network_votekick_machine_joined(long machine_index); boolean network_votekick_kept_out(unsigned long address, char const *hardware_id); +/* port/linux/game/network_voice.c's (voice chat: each machine's key) */ +void network_voice_machine_joined(long machine_index); /* port/linux/src/p2p.c's */ enum { @@ -2132,6 +2134,8 @@ boolean network_game_server_accept_client_machine_into_game( network_game_server_client_machine_join_times[machine_index] = system_milliseconds(); /* (its time played for votes starts over) */ network_votekick_machine_joined(machine_index); + /* (and its voice chat key) */ + network_voice_machine_joined(machine_index); } else { diff --git a/source/networking/network_server_message_handler.c b/source/networking/network_server_message_handler.c index e01e93ba..41d1c874 100644 --- a/source/networking/network_server_message_handler.c +++ b/source/networking/network_server_message_handler.c @@ -267,6 +267,9 @@ symbols in this file: /* port/linux/game/network_distributed.c's */ void network_distributed_handle_message(long machine_index, word const *message, word size); +/* port/linux/game/network_voice.c's: voice chat, in the lobby too */ +boolean network_voice_handles_message(word const *message, word size); +void network_voice_handle_message(long machine_index, word const *message, word size); void network_distributed_handle_stream_message(long machine_index, word const *message, word size); /* ---------- constants */ @@ -1364,6 +1367,19 @@ boolean network_game_server_handle_client_message( case _message_type_data: /* the distributed netcode's messages (port/linux/NETCODE.md) */ + /* (port: voice chat's from any machine that joined, in the + lobby too: network_voice.c) */ + if (network_voice_handles_message(message, message_buffer_size)) + { + long machine_index; + + if (network_game_server_client_machine_is_joined_to_game(server, machine) && + network_game_server_get_client_machine(server, machine, &machine_index)) + { + network_voice_handle_message(machine_index, message, message_buffer_size); + } + break; + } /* (in game, from a machine that has loaded it, as a datagram is: else dropped) */ if (network_game_server_client_machine_is_joined_to_game(server, machine) && @@ -1561,6 +1577,18 @@ boolean network_game_server_handle_datagram( case _message_type_data: /* the distributed netcode's messages (port/linux/NETCODE.md) */ + /* (port: voice chat's from any machine that joined, in the + lobby too, its key checked: network_voice.c) */ + if (network_voice_handles_message(message, datagram_size)) + { + struct network_game_server_client_machine *client_machine = + network_game_server_get_client_machine_at_address(server, source_address->address.long_words[0]); + long machine_index; + + if (client_machine && network_game_server_get_client_machine(server, client_machine, &machine_index)) + network_voice_handle_message(machine_index, message, datagram_size); + break; + } /* (from a machine in the game, which the lookup finds only among those that joined, and only in game: else dropped) */ if (network_game_server_get_state(server, NULL) == _network_game_server_state_ingame) diff --git a/tools/android_build.py b/tools/android_build.py index fcce7c58..53381667 100644 --- a/tools/android_build.py +++ b/tools/android_build.py @@ -27,7 +27,7 @@ from typing import Any, Dict, List, Optional from .linux_build import (LINUX_PROFILE, MINIUPNPC_DEFINES, MINIUPNPC_DIR, MUSL_MATH_DIR, XDK_INCLUDE, compile_launcher, game_defines_and_includes, game_sources, miniupnpc_sources, - musl_math_sources, pgo_mode, pgo_profile, + musl_math_sources, opus_cflags, opus_sources, pgo_mode, pgo_profile, profile_use_flags, updater_defines, xdk_headers) from .embed_assets import hud_assets_build, hud_configure_inputs from .ninja_syntax import Writer @@ -469,6 +469,10 @@ def generate_android_build(n: Writer, sln: Any) -> None: objects.append(guest_object(EXPAT_DIR / name, platform_cflags)) # internet play's reliable streams (port/third_party/kcp; p2p.c) objects.append(guest_object(KCP_DIR / "ikcp.c", platform_cflags)) + # voice chat's codec (port/third_party/opus), with the guest's ABI and C + # library + for source in opus_sources(): + objects.append(guest_object(source, " ".join([opus_cflags(guest_abi), *libc_includes]))) # internet play's signatures, for public games' listings # (port/third_party/monocypher; p2p_crypto.c) for name in ("monocypher.c", "monocypher-ed25519.c"): diff --git a/tools/ci_build.py b/tools/ci_build.py index fc02a92f..5bf9f441 100644 --- a/tools/ci_build.py +++ b/tools/ci_build.py @@ -111,6 +111,9 @@ def main() -> int: # the menus' XML parser (port/third_party/expat), in every build, whose # MIT license asks copies to carry its notice shutil.copy2(ROOT / "port/third_party/expat/COPYING", dist / "expat-COPYING.txt") + # voice chat's codec (port/third_party/opus), in every build, whose BSD + # license asks binaries to carry its notice + shutil.copy2(ROOT / "port/third_party/opus/COPYING", dist / "opus-COPYING.txt") # internet play's MQTT brokers, a file beside the game (network.brokers_file; # Android's APK has its own copy) if args.platform != "android": diff --git a/tools/harness/tests/test_voice_route.py b/tools/harness/tests/test_voice_route.py new file mode 100644 index 00000000..303760a4 --- /dev/null +++ b/tools/harness/tests/test_voice_route.py @@ -0,0 +1,59 @@ +"""The real rules of network_voice.c the host relays each voice by: who hears whom in the lobby and in each mode +(network.voice_mode), and the largest packet a quality (network.voice_*_kbps) allows.""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, constant, enum_with, function, mutated, read, run # noqa: E402 + +CASES = ["lobby", "off", "team-proximity", "team-enemy-proximity", "team-global", "team-global-enemy-proximity", + "packet-limit"] + +# a fault in network_voice.c, the function it is in, and the case that must catch it +NEGATIVE_CONTROLS = { + "lobby-ignores-setting": (("return lobby_voice ? _voice_route_global : _voice_route_none;", + "return _voice_route_global;"), "voice_route", "lobby"), + "proximity-ignores-death": (("boolean near = both_alive && distance <= range;", + "boolean near = distance <= range;"), "voice_route", "team-proximity"), + "proximity-ignores-range": (("boolean near = both_alive && distance <= range;", "boolean near = both_alive;"), + "voice_route", "team-enemy-proximity"), + "team-hears-enemies": (("return teammates ? _voice_route_global : _voice_route_none;", + "return _voice_route_global;"), "voice_route", "team-global"), + "enemies-heard-anywhere": (("\t\treturn near ? _voice_route_proximity : _voice_route_none;\n\tdefault:", + "\t\treturn _voice_route_proximity;\n\tdefault:"), + "voice_route", "team-global-enemy-proximity"), + "no-packet-cap": (("return MIN(limit, VOICE_MAXIMUM_PACKET);", "return limit * 4;"), "voice_packet_limit", + "packet-limit"), +} + + +def generated(fault=None, faulty_function=None): + source = read("port/linux/game/network_voice.c") + header = read("port/linux/game/network_voice.h") + audio = read("port/linux/src/voice_audio.h") + config = enum_with(header, "_voice_mode_off") + "\n" + enum_with(header, "_voice_route_none") + "\n" + config += f"#define VOICE_FRAMES_PER_SECOND {constant(source, 'VOICE_FRAMES_PER_SECOND')}\n" + config += f"#define VOICE_MAXIMUM_PACKET {constant(audio, 'VOICE_MAXIMUM_PACKET')}\n" + text = "" + for name in ("voice_route", "voice_packet_limit"): + code = function(source, name) + if fault and name == faulty_function: + code = mutated(code, *fault) + text += code + "\n" + return (("config.inc", config), ("under_test.inc", text)) + + +@pytest.mark.parametrize("case", CASES) +def test_case(case): + status, output = run(build("voice_route", generated()), case) + assert status == 0, output + + +@pytest.mark.parametrize("control", NEGATIVE_CONTROLS) +def test_negative_control(control): + fault, faulty_function, case = NEGATIVE_CONTROLS[control] + status, output = run(build("voice_route", generated(fault, faulty_function)), case) + assert status == CHECK_FAILED, f"network_voice.c with a fault ({control}) passed '{case}': the test cannot see it" diff --git a/tools/harness/tests/voice_route.c b/tools/harness/tests/voice_route.c new file mode 100644 index 00000000..b89d1d1e --- /dev/null +++ b/tools/harness/tests/voice_route.c @@ -0,0 +1,104 @@ +/* +VOICE_ROUTE.C (test) + +The real rules of port/linux/game/network_voice.c that the host sends each +voice by: who hears whom in the lobby and in each of network.voice_mode's +modes, and the largest packet a quality allows. test_voice_route.py takes +the modes and routes (config.inc) and the code (under_test.inc). +*/ + +#include "harness.h" + +#define PIN(value, low, high) ((value) < (low) ? (low) : (value) > (high) ? (high) : (value)) +#define MIN(a, b) ((a) < (b) ? (a) : (b)) + +#include "config.inc" +#include "under_test.inc" + +/* a listener's route to a speaker in a game: teammates or not, near (both +alive, within the range) or far, or one of them dead */ +static short in_game(short mode, boolean teammates, real distance, boolean both_alive) +{ + return voice_route(mode, FALSE, TRUE, teammates, both_alive, distance, 15.0f); +} + +int main(int argc, char **argv) +{ + char const *case_name = argc > 1 ? argv[1] : ""; + short mode; + + /* the lobby: everyone, or no one, whatever the game's mode */ + CASE("lobby") + { + for (mode = 0; mode < NUMBER_OF_VOICE_MODES; mode++) + { + CHECK(voice_route(mode, TRUE, TRUE, FALSE, FALSE, 1000.0f, 15.0f) == _voice_route_global, + "mode %d: the lobby's voice not heard by everyone", mode); + CHECK(voice_route(mode, TRUE, FALSE, TRUE, TRUE, 0.0f, 15.0f) == _voice_route_none, + "mode %d: the lobby heard with its voice off", mode); + } + return 0; + } + CASE("off") + { + CHECK(in_game(_voice_mode_off, TRUE, 0.0f, TRUE) == _voice_route_none, "off: a teammate heard"); + CHECK(in_game(_voice_mode_off, FALSE, 0.0f, TRUE) == _voice_route_none, "off: an enemy heard"); + return 0; + } + CASE("team-proximity") + { + CHECK(in_game(_voice_mode_team_proximity, TRUE, 10.0f, TRUE) == _voice_route_proximity, "a near teammate"); + CHECK(in_game(_voice_mode_team_proximity, TRUE, 20.0f, TRUE) == _voice_route_none, "a far teammate heard"); + CHECK(in_game(_voice_mode_team_proximity, FALSE, 1.0f, TRUE) == _voice_route_none, "a near enemy heard"); + CHECK(in_game(_voice_mode_team_proximity, TRUE, 1.0f, FALSE) == _voice_route_none, "the dead heard near"); + return 0; + } + CASE("team-enemy-proximity") + { + CHECK(in_game(_voice_mode_team_enemy_proximity, TRUE, 10.0f, TRUE) == _voice_route_proximity, + "a near teammate"); + CHECK(in_game(_voice_mode_team_enemy_proximity, FALSE, 10.0f, TRUE) == _voice_route_proximity, + "a near enemy"); + CHECK(in_game(_voice_mode_team_enemy_proximity, TRUE, 20.0f, TRUE) == _voice_route_none, + "a far teammate heard"); + CHECK(in_game(_voice_mode_team_enemy_proximity, FALSE, 20.0f, TRUE) == _voice_route_none, + "a far enemy heard"); + return 0; + } + CASE("team-global") + { + CHECK(in_game(_voice_mode_team_global, TRUE, 1000.0f, FALSE) == _voice_route_global, + "a teammate anywhere, alive or not"); + CHECK(in_game(_voice_mode_team_global, FALSE, 1.0f, TRUE) == _voice_route_none, "a near enemy heard"); + return 0; + } + CASE("team-global-enemy-proximity") + { + CHECK(in_game(_voice_mode_team_global_enemy_proximity, TRUE, 1000.0f, FALSE) == _voice_route_global, + "a teammate anywhere"); + CHECK(in_game(_voice_mode_team_global_enemy_proximity, FALSE, 10.0f, TRUE) == _voice_route_proximity, + "a near enemy"); + CHECK(in_game(_voice_mode_team_global_enemy_proximity, FALSE, 20.0f, TRUE) == _voice_route_none, + "a far enemy heard"); + CHECK(in_game(_voice_mode_team_global_enemy_proximity, FALSE, 1.0f, FALSE) == _voice_route_none, + "a dead enemy heard"); + return 0; + } + /* a quality's packets fit, and no more than twice their share */ + CASE("packet-limit") + { + short kbps; + + for (kbps = 8; kbps <= 64; kbps++) + { + long share = (long)kbps * 1000 / 8 / 50; + + CHECK(voice_packet_limit(kbps) >= share, "%d kbps: its own packets refused", kbps); + CHECK(voice_packet_limit(kbps) <= 2 * share + 16, "%d kbps: %ld bytes taken", kbps, voice_packet_limit(kbps)); + CHECK(voice_packet_limit(kbps) <= VOICE_MAXIMUM_PACKET, "%d kbps: more than a packet", kbps); + } + return 0; + } + fprintf(stderr, "unknown case: %s\n", case_name); + return 2; +} diff --git a/tools/linux_build.py b/tools/linux_build.py index 5569daec..4064a222 100644 --- a/tools/linux_build.py +++ b/tools/linux_build.py @@ -192,6 +192,23 @@ PROFILE_USE_FLAGS = [ ] +# voice chat's codec (port/third_party/opus; network_voice.c): the +# floating-point library in plain C, its files as sources.txt lists them +OPUS_DIR = Path("port/third_party/opus") +OPUS_DEFINES = ("-DOPUS_BUILD", "-DVAR_ARRAYS", "-DHAVE_LRINTF", "-DHAVE_LRINT") + + +def opus_sources() -> List[Path]: + """Opus's library sources (port/third_party/opus/sources.txt)""" + return [OPUS_DIR / line.strip() for line in (OPUS_DIR / "sources.txt").read_text().splitlines() if line.strip()] + + +def opus_cflags(abi: str) -> str: + """the flags Opus's sources build with, on the platform layer's ABI""" + return " ".join([abi, "-std=gnu11", "-O2", *OPUS_DEFINES, *(f"-I{OPUS_DIR / name}" for name in + ("include", "celt", "silk", "silk/float")), "-w"]) + + def musl_math_sources() -> List[Path]: """musl's maths functions the game uses (port/third_party/musl-math)""" return sorted((MUSL_MATH_DIR / "src").glob("*.c")) @@ -492,6 +509,9 @@ def generate_linux_build(n: Writer, sln: Any) -> None: add_object(EXPAT_DIR / name, " ".join([abi, "-std=gnu11", f"-I{EXPAT_DIR}", "-w"])) # internet play's reliable streams (port/third_party/kcp; p2p.c) add_object(KCP_DIR / "ikcp.c", " ".join([abi, "-std=gnu11", "-w"])) + # voice chat's codec (port/third_party/opus) + for source in opus_sources(): + add_object(source, opus_cflags(abi)) # internet play's signatures, for public games' listings # (port/third_party/monocypher; p2p_crypto.c) for name in ("monocypher.c", "monocypher-ed25519.c"): diff --git a/tools/port_settings.py b/tools/port_settings.py index f453b3af..083dd94b 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -103,7 +103,14 @@ SCREENS = { "screen": "audio_settings_screen", "header": ("header_profile_audio_settings", f"{PE}/audio_settings/header_profile_audio_settings"), "spacing": 30, + # (the devices, desktop only, first: Android's rows from the top) + "platform_places": True, "rows": [ + # (port/linux/game/menu_tags.c adds the devices SDL finds) + ("OUTPUT DEVICE:", "audio.output_device", [("SYSTEM DEFAULT", "default")], + "Where the game's sound and the voices play.", "desktop"), + ("INPUT DEVICE:", "audio.input_device", [("SYSTEM DEFAULT", "default")], + "The microphone voice chat listens to.", "desktop"), ("MASTER VOLUME:", "audio.volume", VOLUMES, "The volume of everything.", None), ("MUSIC VOLUME:", "audio.music_volume", VOLUMES, "The music's volume.", None), ("EFFECTS VOLUME:", "audio.effects_volume", VOLUMES, @@ -112,6 +119,11 @@ SCREENS = { "Echo sounds as the place you are in does, and\nmuffle those behind walls, as the Xbox did.", None), ("SOUND:", "audio.enabled", ON_OFF, "Play sound at all; from the next time the game\nstarts.", None), + # voice chat: this machine's own (port/linux/game/network_voice.c) + ("VOICE CHAT:", "audio.voice_chat", + [("PUSH TO TALK", "push_to_talk"), ("OPEN MIC", "open_mic"), ("OFF", "off")], + "Talk in network games: while PUSH TO TALK is held\n(Controls), whenever you speak, or never.", None), + ("VOICE VOLUME:", "audio.voice_volume", VOLUMES, "The other players' voices.", None), ], }, "network_setup": { @@ -169,6 +181,32 @@ SCREENS = { CONTROL_GROUPS = ["MOVEMENT", "WEAPONS", "ACTIONS"] CONTROL_ROWS = 7 +# the settings whose rows have the wider spinner (a device's name) +WIDE_SETTINGS = {"audio.output_device", "audio.input_device"} + +# the host's voice chat, on Teamplay Options (_teamplay_options_extras): +# each row's key, its label, its choices' words (menu_functions.c's +# gametype_options give their settings' values) and their helps +TEAMPLAY_EDIT = "main_menu/settings_select/multiplayer_setup/teamplay_options_edit" +VOICE_QUALITIES = (8, 12, 16, 24, 32, 48, 64) +VOICE_ROWS = [ + ("voice_mode", "VOICE CHAT:", ["OFF", "TEAM NEAR", "ANYONE NEAR", "TEAM", "TEAM, ENEMIES NEAR"], [ + "No voice chat during the game.", + "Players hear their teammates who are near them.", + "Players hear everyone who is near them.", + "Players hear all their teammates, wherever they\\nare.", + "Players hear all their teammates, and the enemies\\nwho are near them.", + ]), + ("voice_lobby", "LOBBY VOICE CHAT:", ["ON", "OFF"], [ + "Everyone hears everyone in the lobby, before and\\nafter the game.", + "No voice chat in the lobby.", + ]), + ("voice_kbps", "VOICE QUALITY:", [f"{kbps} KBPS" for kbps in VOICE_QUALITIES], + [f"Voices at {kbps} kilobits a second, in the lobby\\nand in the game." for kbps in VOICE_QUALITIES]), + ("voice_proximity", "VOICE NEAR DISTANCE:", ["15 M", "30 M", "45 M", "60 M", "90 M", "150 M"], + [f"Players {metres} metres apart or less are near\\nfor voice chat." for metres in (15, 30, 45, 60, 90, 150)]), +] + # the profile menu's words for what its items now open STRING_OVERRIDES = { f"{PE}/profile_edit_descriptions": [ @@ -247,28 +285,44 @@ def _setting_screen(folder: str, spec: dict) -> list: base = f"{PE}/{folder}" rows, extra = [], [] place = -1 + # (platform_places: each platform's rows in places of their own, with + # no gap where the other's are; a row for both, a child for each) + places = {"desktop": -1, "android": -1} for index, (label, setting, choices, _, platform, *named) in enumerate(spec["rows"]): key = named[0] if named else setting.split(".", 1)[1] row = f"{base}/op_{key}" - if setting not in spec.get("same_place", ()) and key not in spec.get("same_place", ()): - place += 1 - rows.append((row, platform, place)) + if spec.get("platform_places"): + for name in places: + if platform in (None, name): + places[name] += 1 + if platform or places["desktop"] == places["android"]: + rows.append((row, platform, places[platform or "desktop"])) + else: + rows += [(row, name, places[name]) for name in places] + else: + if setting not in spec.get("same_place", ()) and key not in spec.get("same_place", ()): + place += 1 + rows.append((row, platform, place)) + # (a device's name wants the wider spinner Server Setup's co-op rows + # have: WIDE_SETTINGS) + wide = setting in WIDE_SETTINGS extra += _widget(row, [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF"), ("platform", platform)], [f'', - f'']) + f'']) extra += _widget(f"{base}/{key}_label", [("type", "text"), ("controller", 1), ("width", 300), ("height", 22), ("string_list", f"{base}/labels"), ("string_index", index), ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) extra += _widget(f"{base}/{key}_spinner", - [("type", "spinner"), ("left", 3), ("top", 2), ("width", 147), ("height", 20), - ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + [("type", "spinner"), ("left", None if wide else 3), ("top", 2), ("width", 206 if wide else 147), + ("height", 20), ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), ("strings", "|".join(shown for shown, _ in choices)), ("setting", setting), ("values", "|".join(value for _, value in choices)), ("font", "ui\\large_ui"), - ("color", "#FF2896FF"), ("align", "center"), ("text_y", 1), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 4 if wide else 1), ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), - ("header_bounds", "7 -6 19 0"), ("footer_bounds", "7 150 19 156")], + ("header_bounds", "7 -13 19 -7" if wide else "7 -6 19 0"), + ("footer_bounds", "7 208 19 214" if wide else "7 150 19 156")], ['']) extra += _button(f"{base}/button_defaults", 3, ['', '']) @@ -361,6 +415,9 @@ MT = "main_menu/multiplayer_type_select" # in turn: menu_functions.c's gametype_option_help) SLAYER_EDIT = "main_menu/settings_select/multiplayer_setup/playlist_edit/slayer_edit" STRING_INSERTS = { + # (Teamplay Options' voice chat rows, after its own: VOICE_ROWS) + f"{TEAMPLAY_EDIT}/teamplay_options_labels": [(3, [label for _, label, *_ in VOICE_ROWS])], + f"{TEAMPLAY_EDIT}/cap_teamplay_options": [(10, [text for *_, helps in VOICE_ROWS for text in helps])], f"{SLAYER_EDIT}/var_kills_to_win": [(5, ["75", "100", "150", "200", "250", "500"])], f"{SLAYER_EDIT}/cap_slayer": [(11, [ "Seventy-five kills to win. Settle in for a long\\nfight.", @@ -510,6 +567,12 @@ WIDGET_PATCHES = { f'', f'', ]}}, + # (Teamplay Options' voice chat rows, over its buttons: Server Setup's + # only, _teamplay_options_extras) + f"{TEAMPLAY_EDIT}/teamplay_options_menu": {"insert_before": { + f"{TEAMPLAY_EDIT}/teamplay_button_bar": [ + f'' + for index, (key, *_) in enumerate(VOICE_ROWS)]}}, # (the PC's Vehicles row's Start opened Item Options) "main_menu/settings_select/multiplayer_setup/playlist_edit/playlist_edit_vehicles_list_item": {"handlers": [ '', @@ -1139,6 +1202,35 @@ def _item_options_extras() -> list: return lines +def _teamplay_options_extras() -> list: + """Teamplay Options' rows of the port's: the host's voice chat + (port/linux/game/network_voice.c), in Server Setup's copy only + (menu_functions.c's gametype_options_init), kept in config.toml""" + lines = [] + for index, (key, label, strings, _) in enumerate(VOICE_ROWS): + lines += _widget(f"{TEAMPLAY_EDIT}/op_{key}", [("width", 512), ("height", 28), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', + f'']) + lines += _widget(f"{TEAMPLAY_EDIT}/{key}_label", [("type", "text"), ("controller", 1), ("width", 300), + ("height", 22), + ("string_list", f"{TEAMPLAY_EDIT}/teamplay_options_labels"), + ("string_index", 3 + index), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) + lines += _widget(f"{TEAMPLAY_EDIT}/{key}_spinner", + [("type", "spinner"), ("top", 2), ("width", 206), ("height", 20), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("string_list", f"{TEAMPLAY_EDIT}/var_{key}"), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 4), + ("list_flags", "items_from_strings"), + ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), + ("header_bounds", "7 -13 19 -7"), ("footer_bounds", "7 208 19 214")], + ['']) + lines += _strings(f"{TEAMPLAY_EDIT}/var_{key}", strings) + return lines + + def _map_kind() -> list: """the map lists' first row (New Game's and the Map screen's), as the gametype list's chooser: a spinner of SINGLEPLAYER or MULTIPLAYER maps, @@ -1175,6 +1267,7 @@ def multiplayer_files() -> dict: f"{MT}/coop".replace("/", ".") + ".xml": head + _coop() + ["", ""], "main_menu/new_select".replace("/", ".") + ".port.xml": head + _map_kind() + ["", ""], "main_menu/settings_select/multiplayer_setup/item_options_edit".replace("/", ".") + ".port.xml": head + _item_options_extras() + ["", ""], + TEAMPLAY_EDIT.replace("/", ".") + ".port.xml": head + _teamplay_options_extras() + ["", ""], } diff --git a/tools/windows_build.py b/tools/windows_build.py index f410e935..111e0169 100644 --- a/tools/windows_build.py +++ b/tools/windows_build.py @@ -20,8 +20,8 @@ from typing import Any, Dict, List, Optional from .linux_build import (LINUX_PROFILE, MINIUPNPC_DIR, OPTIMISATION, WINDOWS_PROFILE, XDK_INCLUDE, lto_mode, march_flag, miniupnpc_sources, pgo_mode, compile_launcher, game_defines_and_includes, - game_sources, musl_math_cflags, musl_math_sources, pgo_profile, profile_use_flags, - xdk_headers) + game_sources, musl_math_cflags, musl_math_sources, opus_cflags, opus_sources, pgo_profile, + profile_use_flags, xdk_headers) from .embed_assets import hud_assets_build, hud_configure_inputs from .ninja_syntax import Writer @@ -435,6 +435,9 @@ def generate_windows_build(n: Writer, sln: Any) -> None: add_object(EXPAT_DIR / name, " ".join([abi, "-std=gnu11", f"-I{EXPAT_DIR}", "-w"])) # internet play's reliable streams (port/third_party/kcp; p2p.c) add_object(KCP_DIR / "ikcp.c", " ".join([abi, "-std=gnu11", "-w"])) + # voice chat's codec (port/third_party/opus) + for source in opus_sources(): + add_object(source, opus_cflags(abi)) # internet play's signatures, for public games' listings # (port/third_party/monocypher; p2p_crypto.c) for name in ("monocypher.c", "monocypher-ed25519.c"): From 84cf2f8f1928927ec925c7aabdb7dac8cce52988 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:36:14 +1300 Subject: [PATCH 072/187] Include stdio.h for snprintf in the voice code --- port/linux/src/dsound_sdl.c | 1 + port/linux/src/voice_audio.c | 1 + 2 files changed, 2 insertions(+) diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 4c3f9967..7f165488 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -41,6 +41,7 @@ turns the reverb off; audio.enabled = false skips opening a device #include #include +#include #include #include #include diff --git a/port/linux/src/voice_audio.c b/port/linux/src/voice_audio.c index d9ec2d86..297242fa 100644 --- a/port/linux/src/voice_audio.c +++ b/port/linux/src/voice_audio.c @@ -23,6 +23,7 @@ one's redundancy (Opus's in-band FEC), or by concealment. #include #include +#include #include #include #include From 887e2c11c4038ac9d72b0e2acaa01909be73f09f Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:01:36 +1300 Subject: [PATCH 073/187] Vote kick in Server Setup Teamplay Options has a VOTE KICK row (on by default) under the voice chat rows, which sets network.votekick for the host's games, as they set theirs. --- port/assets/menus/UNWIRED.md | 2 +- ...layer_setup.teamplay_options_edit.port.xml | 13 +++++++ ...ultiplayer_setup.teamplay_options_edit.xml | 1 + port/assets/menus/ce/strings.xml | 3 ++ port/linux/README.md | 19 ++++++----- port/linux/game/menu_functions.c | 4 ++- tools/port_settings.py | 34 ++++++++++++------- 7 files changed, 52 insertions(+), 24 deletions(-) diff --git a/port/assets/menus/UNWIRED.md b/port/assets/menus/UNWIRED.md index a57a6a1e..4f3dd52c 100644 --- a/port/assets/menus/UNWIRED.md +++ b/port/assets/menus/UNWIRED.md @@ -21,7 +21,7 @@ do (save a setting, join a game) does not happen; the screens open and close as | `gamespy update filter settings` | 2 | PC function | | `gt edit list update` | 1 | PC function | | `gt select list update` | 1 | PC function | -| `mouse spinner 1wide click` | 76 | PC function | +| `mouse spinner 1wide click` | 77 | PC function | | `mp map list update` | 1 | PC function | | `mp prof init teamplay options` | 1 | PC function | | `mp prof init vehicle options` | 1 | PC function | diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml index 389e9277..f80aee33 100644 --- a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml @@ -65,4 +65,17 @@
    + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml index 08713f89..12bebaa8 100644 --- a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml +++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml @@ -52,6 +52,7 @@ +
    diff --git a/port/assets/menus/ce/strings.xml b/port/assets/menus/ce/strings.xml index 23c77ae9..99d8a5e9 100644 --- a/port/assets/menus/ce/strings.xml +++ b/port/assets/menus/ce/strings.xml @@ -1043,6 +1043,8 @@ + + @@ -1052,6 +1054,7 @@ + diff --git a/port/linux/README.md b/port/linux/README.md index a25afca9..6e456ed7 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -279,7 +279,7 @@ the setting for one start of the game. It has priority over the file. | `network.voice_mode` | `"team_global_enemy_proximity"` | `HALO_NET_VOICE_MODE` | When you host: who hears whom during a game. `"off"`; `"team_proximity"` (teammates who are near); `"team_enemy_proximity"` (all players who are near); `"team_global"` (all teammates); `"team_global_enemy_proximity"` (all teammates, and enemies who are near). Refer to "Voice chat". | | `network.voice_kbps` | `24` | `HALO_NET_VOICE_KBPS` | When you host: the voice quality in the lobby and in a game, in kilobits per second, `8` to `64`. | | `network.voice_proximity` | `15.0` | `HALO_NET_VOICE_PROXIMITY` | When you host: the distance in world units (1 unit is approximately 3 metres) at which players are near, for voice chat. `5` to `100`. | -| `network.votekick` | `true` | `HALO_NET_VOTEKICK` | When you host: `true`, the players can vote to kick a player. Refer to "Security". `false`: no votes. | +| `network.votekick` | `true` | `HALO_NET_VOTEKICK` | When you host: `true`, the players can vote to kick a player. Refer to "Security". `false`: no votes. VOTE KICK in Server Setup > Teamplay Options writes its choice here. | | `network.votekick_minutes` | `5` | `HALO_NET_VOTEKICK_MINUTES` | When you host: the minutes that a player must play on the server before the player can start a vote to kick (`0` to `60`). To vote, a player must play for 2 minutes, or for this time if it is less. | | `network.votekick_ban_minutes` | `30` | `HALO_NET_VOTEKICK_BAN_MINUTES` | When you host: the minutes that a player who is kicked by a vote cannot join again (`1` to `1440`). | | `network.coop_public` | `false` | `HALO_NET_COOP_PUBLIC` | `true`: an online co-op game (Create Game > Internet, a SINGLEPLAYER map) starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in co-op's Server Setup writes its choice here. Refer to "Server browser". | @@ -636,14 +636,15 @@ Only machines with the invite can find the game: Refer to `NETCODE.md`. `kick ` drops the player the same way, but keeps nothing: no line in `bans.txt`, and the player can join again at once. In co-op, `bringto` brings every player to the host. -- Players can vote to kick a player (`network.votekick`). Hold the - scoreboard key, right-click to show the pointer, and click the name of - the player. Then click **Start a vote to kick**. Other players vote in - the same way, and see the vote on the scoreboard. `votekick ` in the developer console does the same. The host also gets - **Kick** and **Ban** in this menu: these do the same as the `kick` and - `ban` commands (click **Ban** two times). The host counts the votes, and - these rules prevent abuse: +- Players can vote to kick a player. The host turns this on or off with + VOTE KICK in Server Setup > Teamplay Options (`network.votekick`; in + co-op, only in `config.toml`). Hold the scoreboard key, right-click to + show the pointer, and click the name of the player. Then click **Start + a vote to kick**. Other players vote in the same way, and see the vote on + the scoreboard. `votekick ` in the developer console does + the same. The host also gets **Kick** and **Ban** in this menu: these do + the same as the `kick` and `ban` commands (click **Ban** two times). The + host counts the votes, and these rules prevent abuse: - The vote passes when more than half of the players vote for it, and at least two players. The player of the vote is counted, but cannot vote. Thus, in a game of two equal teams, one team cannot kick a player of diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index b9c4b75e..f78cc53e 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -4655,7 +4655,7 @@ static struct gametype_option const gametype_options[] = { "friendly_fire_penalty_spinner", _option_short, OPTIONS_FIELD(friendly_fire_penalty), 0, 4, { 0, 5, 10, 15 } }, { "autobalance_spinner", _option_option_byte, OPTIONS_FIELD(auto_team_balance), 0, 2, { 0, 1 } }, /* (the host's voice chat, below them: port/linux/game/network_voice.c; - tools/port_settings.py's VOICE_ROWS) */ + tools/port_settings.py's TEAMPLAY_ROWS) */ { "voice_mode_spinner", _option_setting, 0, 0, 5, { 0 }, "network.voice_mode", { "off", "team_proximity", "team_enemy_proximity", "team_global", "team_global_enemy_proximity" } }, { "voice_lobby_spinner", _option_setting, 0, 0, 2, { 0 }, "network.voice_lobby", { "true", "false" } }, @@ -4663,6 +4663,8 @@ static struct gametype_option const gametype_options[] = { "8", "12", "16", "24", "32", "48", "64" } }, { "voice_proximity_spinner", _option_setting, 0, 0, 6, { 0 }, "network.voice_proximity", { "5", "10", "15", "20", "30", "50" } }, + /* (and the players' votes to kick: port/linux/game/network_votekick.c) */ + { "votekick_spinner", _option_setting, 0, 0, 2, { 0 }, "network.votekick", { "true", "false" } }, /* vehicle options (the side's set and counts: vehicles_update) */ { "vehicles_respawn_spinner", _option_short, OPTIONS_FIELD(vehicle_respawn_time), 0, 7, { 0, 30, 60, 90, 120, 180, 300 } }, diff --git a/tools/port_settings.py b/tools/port_settings.py index 083dd94b..60af7991 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -184,12 +184,13 @@ CONTROL_ROWS = 7 # the settings whose rows have the wider spinner (a device's name) WIDE_SETTINGS = {"audio.output_device", "audio.input_device"} -# the host's voice chat, on Teamplay Options (_teamplay_options_extras): -# each row's key, its label, its choices' words (menu_functions.c's -# gametype_options give their settings' values) and their helps +# the host's voice chat and vote kicks, on Teamplay Options +# (_teamplay_options_extras): each row's key, its label, its choices' words +# (menu_functions.c's gametype_options give their settings' values) and +# their helps TEAMPLAY_EDIT = "main_menu/settings_select/multiplayer_setup/teamplay_options_edit" VOICE_QUALITIES = (8, 12, 16, 24, 32, 48, 64) -VOICE_ROWS = [ +TEAMPLAY_ROWS = [ ("voice_mode", "VOICE CHAT:", ["OFF", "TEAM NEAR", "ANYONE NEAR", "TEAM", "TEAM, ENEMIES NEAR"], [ "No voice chat during the game.", "Players hear their teammates who are near them.", @@ -205,6 +206,11 @@ VOICE_ROWS = [ [f"Voices at {kbps} kilobits a second, in the lobby\\nand in the game." for kbps in VOICE_QUALITIES]), ("voice_proximity", "VOICE NEAR DISTANCE:", ["15 M", "30 M", "45 M", "60 M", "90 M", "150 M"], [f"Players {metres} metres apart or less are near\\nfor voice chat." for metres in (15, 30, 45, 60, 90, 150)]), + # (network_votekick.c) + ("votekick", "VOTE KICK:", ["ON", "OFF"], [ + "Players can vote to kick a player, from the\\nscoreboard.", + "Players cannot vote to kick anyone.", + ]), ] # the profile menu's words for what its items now open @@ -415,9 +421,10 @@ MT = "main_menu/multiplayer_type_select" # in turn: menu_functions.c's gametype_option_help) SLAYER_EDIT = "main_menu/settings_select/multiplayer_setup/playlist_edit/slayer_edit" STRING_INSERTS = { - # (Teamplay Options' voice chat rows, after its own: VOICE_ROWS) - f"{TEAMPLAY_EDIT}/teamplay_options_labels": [(3, [label for _, label, *_ in VOICE_ROWS])], - f"{TEAMPLAY_EDIT}/cap_teamplay_options": [(10, [text for *_, helps in VOICE_ROWS for text in helps])], + # (Teamplay Options' voice chat and vote kick rows, after its own: + # TEAMPLAY_ROWS) + f"{TEAMPLAY_EDIT}/teamplay_options_labels": [(3, [label for _, label, *_ in TEAMPLAY_ROWS])], + f"{TEAMPLAY_EDIT}/cap_teamplay_options": [(10, [text for *_, helps in TEAMPLAY_ROWS for text in helps])], f"{SLAYER_EDIT}/var_kills_to_win": [(5, ["75", "100", "150", "200", "250", "500"])], f"{SLAYER_EDIT}/cap_slayer": [(11, [ "Seventy-five kills to win. Settle in for a long\\nfight.", @@ -567,12 +574,12 @@ WIDGET_PATCHES = { f'', f'', ]}}, - # (Teamplay Options' voice chat rows, over its buttons: Server Setup's - # only, _teamplay_options_extras) + # (Teamplay Options' voice chat and vote kick rows, over its buttons: + # Server Setup's only, _teamplay_options_extras) f"{TEAMPLAY_EDIT}/teamplay_options_menu": {"insert_before": { f"{TEAMPLAY_EDIT}/teamplay_button_bar": [ f'' - for index, (key, *_) in enumerate(VOICE_ROWS)]}}, + for index, (key, *_) in enumerate(TEAMPLAY_ROWS)]}}, # (the PC's Vehicles row's Start opened Item Options) "main_menu/settings_select/multiplayer_setup/playlist_edit/playlist_edit_vehicles_list_item": {"handlers": [ '', @@ -1204,10 +1211,11 @@ def _item_options_extras() -> list: def _teamplay_options_extras() -> list: """Teamplay Options' rows of the port's: the host's voice chat - (port/linux/game/network_voice.c), in Server Setup's copy only - (menu_functions.c's gametype_options_init), kept in config.toml""" + (port/linux/game/network_voice.c) and vote kicks (network_votekick.c), + in Server Setup's copy only (menu_functions.c's gametype_options_init), + kept in config.toml""" lines = [] - for index, (key, label, strings, _) in enumerate(VOICE_ROWS): + for index, (key, label, strings, _) in enumerate(TEAMPLAY_ROWS): lines += _widget(f"{TEAMPLAY_EDIT}/op_{key}", [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], From 75811cc2a3eb9e1b20840d2e2024605aa832406e Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:44:49 +1300 Subject: [PATCH 074/187] Co-op setup options in categories Co-op's Server Setup has CO-OP OPTIONS (friendly fire, the extra enemies, player collisions) and VOICE AND VOTING (voice chat and vote kick, as multiplayer's Teamplay Options has them) in screens of their own, as the gametype's options are, each choice with its help; their spinners set config.toml's settings on OK, and the co-op game starts from them. --- port/assets/menus/NON_HANDDRAWN.md | 2 +- port/assets/menus/UNWIRED.md | 4 +- port/assets/menus/ce/bitmaps.xml | 6 + ...ultiplayer_type_select.server_settings.xml | 338 ++++++++++++++---- .../coop_options/header_coop_options.png | Bin 0 -> 62470 bytes .../voice_options/header_voice_options.png | Bin 0 -> 72482 bytes port/assets/menus/menus.json | 2 + .../coop_options/header_coop_options.svg | 13 + .../voice_options/header_voice_options.svg | 13 + port/linux/README.md | 18 +- port/linux/game/menu_functions.c | 159 ++++---- port/linux/game/menu_tags.c | 1 + port/linux/src/port_config.c | 22 +- tools/port_settings.py | 207 ++++++----- 14 files changed, 550 insertions(+), 235 deletions(-) create mode 100644 port/assets/menus/ce/port/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.png create mode 100644 port/assets/menus/ce/port/main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options.png create mode 100644 port/assets/menus/port_svg/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.svg create mode 100644 port/assets/menus/port_svg/main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options.svg diff --git a/port/assets/menus/NON_HANDDRAWN.md b/port/assets/menus/NON_HANDDRAWN.md index 3e973564..167d8e6f 100644 --- a/port/assets/menus/NON_HANDDRAWN.md +++ b/port/assets/menus/NON_HANDDRAWN.md @@ -62,7 +62,7 @@ until it is redrawn. | `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | | `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | -13 of 157 frames are placeholders. +13 of 159 frames are placeholders. The PC version's `ui\gamespy` and `ui\ticker` fonts are drawn with `ui\small_ui`, which the Xbox's map has. diff --git a/port/assets/menus/UNWIRED.md b/port/assets/menus/UNWIRED.md index 4f3dd52c..27d20537 100644 --- a/port/assets/menus/UNWIRED.md +++ b/port/assets/menus/UNWIRED.md @@ -10,7 +10,7 @@ do (save a setting, join a game) does not happen; the screens open and close as | Function | Uses | | | --- | --- | --- | -| `common button bar update` | 25 | PC function | +| `common button bar update` | 27 | PC function | | `direct ip connect init` | 1 | PC function | | `direct ip connect update` | 1 | PC function | | `direct ip edit field` | 4 | PC function | @@ -21,7 +21,7 @@ do (save a setting, join a game) does not happen; the screens open and close as | `gamespy update filter settings` | 2 | PC function | | `gt edit list update` | 1 | PC function | | `gt select list update` | 1 | PC function | -| `mouse spinner 1wide click` | 77 | PC function | +| `mouse spinner 1wide click` | 82 | PC function | | `mp map list update` | 1 | PC function | | `mp prof init teamplay options` | 1 | PC function | | `mp prof init vehicle options` | 1 | PC function | diff --git a/port/assets/menus/ce/bitmaps.xml b/port/assets/menus/ce/bitmaps.xml index 2c12b6c6..cef95f52 100644 --- a/port/assets/menus/ce/bitmaps.xml +++ b/port/assets/menus/ce/bitmaps.xml @@ -332,6 +332,12 @@ + + + + + + diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml index 31f42d3d..732cb636 100644 --- a/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml +++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.server_settings.xml @@ -19,11 +19,8 @@ - - - - - + + @@ -89,64 +86,25 @@ - + - - - + + + + + - + - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -233,11 +191,8 @@ - - - - - + + @@ -253,6 +208,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -261,9 +330,152 @@ + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/port/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.png b/port/assets/menus/ce/port/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.png new file mode 100644 index 0000000000000000000000000000000000000000..e24a4f609582d5071589e3cc46a3f10b79032876 GIT binary patch literal 62470 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 t%wWL;E(jMa$6w9i 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%0Do?=b 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¥v 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 D4~$dh literal 0 HcmV?d00001 diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json index 1886ef25..cb46b1d6 100644 --- a/port/assets/menus/menus.json +++ b/port/assets/menus/menus.json @@ -59,6 +59,8 @@ "ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png", "ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png", "ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png", + "ce/port/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.png", + "ce/port/main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options.png", "ce/port/pause/pausebox_center__0.png", "ce/port/pause/pausebox_center__1.png", "ce/port/pause/pausebox_left__0.png", diff --git a/port/assets/menus/port_svg/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.svg b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.svg new file mode 100644 index 00000000..3e25b31b --- /dev/null +++ b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/assets/menus/port_svg/main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options.svg b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options.svg new file mode 100644 index 00000000..e85d5312 --- /dev/null +++ b/port/assets/menus/port_svg/main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/port/linux/README.md b/port/linux/README.md index 6e456ed7..4c030097 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -270,16 +270,16 @@ the setting for one start of the game. It has priority over the file. | `network.allow_upnp` | `true` | `HALO_NET_ALLOW_UPNP` | `true`: internet play can ask the router to forward its port (UPnP). `false`: the game does not ask. Refer to "Internet play". | | `network.public_lobby` | `true` | `HALO_NET_PUBLIC_LOBBY` | `true`: the server browser. Public games are listed, and Join Game > Server Browser shows them. `false`: no games are listed or shown. Refer to "Server browser". | | `network.host_public` | `true` | `HALO_NET_HOST_PUBLIC` | `true`: a new game of Create Game > Internet starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in Server Setup changes it for each game. Refer to "Server browser". | -| `network.coop_friendly_fire` | `"on"` | `HALO_NET_COOP_FRIENDLY_FIRE` | Whether the players of an online co-op game hurt each other: `"off"`, `"on"`, `"shields_only"` or `"explosives_only"`. FRIENDLY FIRE in co-op's Server Setup writes its choice here. Their AI allies they always can, as in the campaign. | -| `network.coop_player_collisions` | `true` | `HALO_NET_COOP_PLAYER_COLLISIONS` | Whether the players of an online co-op game bump into each other. `false`: they walk through each other, so that one cannot block a doorway or stand on another; they still bump into the AI's characters. PLAYER COLLISIONS in co-op's Server Setup writes its choice here. | -| `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup writes its choice here. | -| `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad), up to 8 times the squad however many players there are. PER PLAYER in co-op's Server Setup writes its choice here. | -| `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup writes its choice here. | +| `network.coop_friendly_fire` | `"on"` | `HALO_NET_COOP_FRIENDLY_FIRE` | Whether the players of an online co-op game hurt each other: `"off"`, `"on"`, `"shields_only"` or `"explosives_only"`. FRIENDLY FIRE in co-op's Server Setup > Co-op Options writes its choice here. Their AI allies they always can, as in the campaign. | +| `network.coop_player_collisions` | `true` | `HALO_NET_COOP_PLAYER_COLLISIONS` | Whether the players of an online co-op game bump into each other. `false`: they walk through each other, so that one cannot block a doorway or stand on another; they still bump into the AI's characters. PLAYER COLLISIONS in co-op's Server Setup > Co-op Options writes its choice here. | +| `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup > Co-op Options writes its choice here. | +| `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad), up to 8 times the squad however many players there are. PER PLAYER in co-op's Server Setup > Co-op Options writes its choice here. | +| `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup > Co-op Options writes its choice here. | | `network.voice_lobby` | `true` | `HALO_NET_VOICE_LOBBY` | When you host: `true`, all players hear all players in the lobby, before and after a game. | | `network.voice_mode` | `"team_global_enemy_proximity"` | `HALO_NET_VOICE_MODE` | When you host: who hears whom during a game. `"off"`; `"team_proximity"` (teammates who are near); `"team_enemy_proximity"` (all players who are near); `"team_global"` (all teammates); `"team_global_enemy_proximity"` (all teammates, and enemies who are near). Refer to "Voice chat". | | `network.voice_kbps` | `24` | `HALO_NET_VOICE_KBPS` | When you host: the voice quality in the lobby and in a game, in kilobits per second, `8` to `64`. | | `network.voice_proximity` | `15.0` | `HALO_NET_VOICE_PROXIMITY` | When you host: the distance in world units (1 unit is approximately 3 metres) at which players are near, for voice chat. `5` to `100`. | -| `network.votekick` | `true` | `HALO_NET_VOTEKICK` | When you host: `true`, the players can vote to kick a player. Refer to "Security". `false`: no votes. VOTE KICK in Server Setup > Teamplay Options writes its choice here. | +| `network.votekick` | `true` | `HALO_NET_VOTEKICK` | When you host: `true`, the players can vote to kick a player. Refer to "Security". `false`: no votes. VOTE KICK in Server Setup > Teamplay Options (in co-op, Voice and Voting) writes its choice here. | | `network.votekick_minutes` | `5` | `HALO_NET_VOTEKICK_MINUTES` | When you host: the minutes that a player must play on the server before the player can start a vote to kick (`0` to `60`). To vote, a player must play for 2 minutes, or for this time if it is less. | | `network.votekick_ban_minutes` | `30` | `HALO_NET_VOTEKICK_BAN_MINUTES` | When you host: the minutes that a player who is kicked by a vote cannot join again (`1` to `1440`). | | `network.coop_public` | `false` | `HALO_NET_COOP_PUBLIC` | `true`: an online co-op game (Create Game > Internet, a SINGLEPLAYER map) starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in co-op's Server Setup writes its choice here. Refer to "Server browser". | @@ -493,8 +493,8 @@ The host sets voice chat for all players in Server Setup > Teamplay Options - The quality (VOICE QUALITY), 8 to 64 kilobits per second (24 is the default), in the lobby and in the game. -In co-op, Server Setup has no Teamplay Options: the host sets voice chat in -`config.toml`. +In co-op, Server Setup has no Teamplay Options: the same settings are in +Server Setup > Voice and Voting. A speaker shows next to the name of a player who talks: in the lobby, on the scoreboard, and in a list at the left of the screen during a game. To @@ -638,7 +638,7 @@ Only machines with the invite can find the game: again at once. In co-op, `bringto` brings every player to the host. - Players can vote to kick a player. The host turns this on or off with VOTE KICK in Server Setup > Teamplay Options (`network.votekick`; in - co-op, only in `config.toml`). Hold the scoreboard key, right-click to + co-op, Server Setup > Voice and Voting). Hold the scoreboard key, right-click to show the pointer, and click the name of the player. Then click **Start a vote to kick**. Other players vote in the same way, and see the vote on the scoreboard. `votekick ` in the developer console does diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index f78cc53e..7b98f071 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -2226,9 +2226,10 @@ static struct starts with COOPERATIVE_DEFAULT_PLAYERS players at most) */ short cooperative_maximum_players_index; boolean cooperative_maximum_players_set; - /* co-op's FRIENDLY FIRE shown (network.coop_friendly_fire's) */ + /* the co-op game's FRIENDLY FIRE, and its PLAYER COLLISIONS OFF, as it + starts (CO-OP OPTIONS': network.coop_friendly_fire's and + network.coop_player_collisions': server_start) */ short cooperative_friendly_fire; - /* co-op's PLAYER COLLISIONS shown, OFF (network.coop_player_collisions's) */ boolean cooperative_no_player_collisions; /* the browser's games */ struct advertised_game *games[MAXIMUM_ADVERTISED_GAMES]; @@ -2764,18 +2765,15 @@ static char const *const server_settings_gametype_rows[] = /* the most players a co-op game hosted starts with (maximum_players') */ #define COOPERATIVE_DEFAULT_PLAYERS 16 -/* Server Setup's help for co-op's FRIENDLY FIRE and EXTRA ENEMIES, by -their choices, and EXTRA ENEMIES' PER PLAYER and MULTIPLIER (its -help_strings, tools/port_settings.py) */ -#define COOPERATIVE_FRIENDLY_FIRE_HELP 12 -#define COOPERATIVE_EXTRA_ENEMIES_HELP 16 -#define COOPERATIVE_ENEMIES_PER_PLAYER_HELP 19 -#define COOPERATIVE_ENEMIES_MULTIPLIER_HELP 20 -#define COOPERATIVE_PLAYER_COLLISIONS_HELP 21 -#define SERVER_PASSWORD_HELP 23 +/* Server Setup's help for co-op's options' rows (CO-OP OPTIONS, VOICE AND +VOTING), and PASSWORD's (its help_strings, tools/port_settings.py) */ +#define COOPERATIVE_OPTIONS_HELP 12 +#define COOPERATIVE_VOICE_OPTIONS_HELP 13 +#define SERVER_PASSWORD_HELP 14 -/* co-op's EXTRA ENEMIES' choices (port_settings.COOP_ENEMIES_MODES, in this -order): its amount's row is the choice's */ +/* co-op's EXTRA ENEMIES' choices (network.coop_enemies_mode's values, as +gametype_options' coop_extra_enemies_spinner has them, in this order): its +amount's row is the choice's */ enum { _cooperative_enemies_none, @@ -2784,8 +2782,9 @@ enum NUMBER_OF_COOPERATIVE_ENEMIES_MODES }; -/* co-op's FRIENDLY FIRE's choices (network.coop_friendly_fire's values, -port_settings.COOP_FRIENDLY_FIRE_VALUES, in this order) */ +/* co-op's FRIENDLY FIRE's choices (network.coop_friendly_fire's values, as +gametype_options' coop_friendly_fire_spinner and server_start have them, in +this order) */ static short const cooperative_friendly_fire_modes[] = { _friendly_fire_off, _friendly_fire_on, _friendly_fire_shields_only, _friendly_fire_explosives_only @@ -2899,54 +2898,17 @@ static void server_settings_update(struct widget_instance *list) for (row = 0; row < NUMBEROF(server_settings_gametype_rows); row++) visible_set(named(list, server_settings_gametype_rows[row], 0), !cooperative); - /* co-op's FRIENDLY FIRE, in their place: its help is its choice's */ - visible_set(named(list, "op_friendly_fire", 0), cooperative); - if (cooperative && (spinner = named(list, "friendly_fire_spinner", 0)) != NULL) - { - short choice = (short)PIN(spinner->parameters.list.selected_index, 0, - NUMBEROF(cooperative_friendly_fire_modes) - 1); - struct widget_instance *help = list->parameters.list.extended_description; - - multiplayer.cooperative_friendly_fire = cooperative_friendly_fire_modes[choice]; - if (help && list->focused_child == named(list, "op_friendly_fire", 0)) - help->parameters.text_box.string_list_index = (short)(COOPERATIVE_FRIENDLY_FIRE_HELP + choice); - } - /* ... and its EXTRA ENEMIES, with the row of the amount of the one - chosen below it (network.coop_enemies_mode and its amounts, which the - host reads as the game begins: coop_enemies.c) */ + /* co-op's options in their place, rows opening their screens + (tools/port_settings.py's COOP_SETUP_SCREENS), each its help */ { struct widget_instance *help = list->parameters.list.extended_description; - short mode = _cooperative_enemies_none; - if ((spinner = named(list, "extra_enemies_spinner", 0)) != NULL) - { - mode = (short)PIN(spinner->parameters.list.selected_index, 0, - NUMBER_OF_COOPERATIVE_ENEMIES_MODES - 1); - } - visible_set(named(list, "op_extra_enemies", 0), cooperative); - visible_set(named(list, "op_enemies_per_player", 0), cooperative && mode == _cooperative_enemies_per_player); - visible_set(named(list, "op_enemies_multiplier", 0), cooperative && mode == _cooperative_enemies_multiplier); - if (cooperative && help) - { - if (list->focused_child == named(list, "op_extra_enemies", 0)) - help->parameters.text_box.string_list_index = (short)(COOPERATIVE_EXTRA_ENEMIES_HELP + mode); - else if (list->focused_child == named(list, "op_enemies_per_player", 0)) - help->parameters.text_box.string_list_index = COOPERATIVE_ENEMIES_PER_PLAYER_HELP; - else if (list->focused_child == named(list, "op_enemies_multiplier", 0)) - help->parameters.text_box.string_list_index = COOPERATIVE_ENEMIES_MULTIPLIER_HELP; - } - } - /* ... and its PLAYER COLLISIONS, below them: ON or OFF, its help its - choice's */ - visible_set(named(list, "op_player_collisions", 0), cooperative); - if (cooperative && (spinner = named(list, "player_collisions_spinner", 0)) != NULL) - { - short choice = (short)PIN(spinner->parameters.list.selected_index, 0, 1); - struct widget_instance *help = list->parameters.list.extended_description; - - multiplayer.cooperative_no_player_collisions = choice == 1; - if (help && list->focused_child == named(list, "op_player_collisions", 0)) - help->parameters.text_box.string_list_index = (short)(COOPERATIVE_PLAYER_COLLISIONS_HELP + choice); + visible_set(named(list, "op_coop_options", 0), cooperative); + visible_set(named(list, "op_voice_options", 0), cooperative); + if (help && list->focused_child == named(list, "op_coop_options", 0)) + help->parameters.text_box.string_list_index = COOPERATIVE_OPTIONS_HELP; + else if (help && list->focused_child == named(list, "op_voice_options", 0)) + help->parameters.text_box.string_list_index = COOPERATIVE_VOICE_OPTIONS_HELP; } } /* LISTING (an internet game's): PUBLIC, listed in everyone's server @@ -3041,7 +3003,21 @@ static boolean server_start(void) and its FRIENDLY FIRE) */ if (hosting_cooperative()) { + /* (as CO-OP OPTIONS left them: network.coop_friendly_fire, in + cooperative_friendly_fire_modes' order, and + network.coop_player_collisions) */ + static char const *const friendly_fire_names[] = { "off", "on", "shields_only", "explosives_only" }; + char const *friendly_fire = config_string("network.coop_friendly_fire"); + short index; + gametype_setup_end(); + multiplayer.cooperative_friendly_fire = _friendly_fire_on; + for (index = 0; friendly_fire && index < (short)NUMBEROF(friendly_fire_names); index++) + { + if (!strcmp(friendly_fire, friendly_fire_names[index])) + multiplayer.cooperative_friendly_fire = cooperative_friendly_fire_modes[index]; + } + multiplayer.cooperative_no_player_collisions = !config_boolean("network.coop_player_collisions"); network_game_server_port_set_cooperative_friendly_fire(multiplayer.cooperative_friendly_fire); network_game_server_port_set_cooperative_player_collisions(!multiplayer.cooperative_no_player_collisions); } @@ -4665,6 +4641,18 @@ static struct gametype_option const gametype_options[] = { "5", "10", "15", "20", "30", "50" } }, /* (and the players' votes to kick: port/linux/game/network_votekick.c) */ { "votekick_spinner", _option_setting, 0, 0, 2, { 0 }, "network.votekick", { "true", "false" } }, + /* co-op's options (Server Setup's CO-OP OPTIONS: tools/port_settings.py's + COOP_SETUP_SCREENS; coop_enemies.c) */ + { "coop_friendly_fire_spinner", _option_setting, 0, 0, 4, { 0 }, "network.coop_friendly_fire", + { "off", "on", "shields_only", "explosives_only" } }, + { "coop_extra_enemies_spinner", _option_setting, 0, 0, 3, { 0 }, "network.coop_enemies_mode", + { "none", "per_player", "multiplier" } }, + { "coop_enemies_per_player_spinner", _option_setting, 0, 0, 5, { 0 }, "network.coop_enemies", + { "25", "50", "100", "150", "200" } }, + { "coop_enemies_multiplier_spinner", _option_setting, 0, 0, 5, { 0 }, "network.coop_enemies_multiplier", + { "2", "4", "8", "16", "32" } }, + { "coop_player_collisions_spinner", _option_setting, 0, 0, 2, { 0 }, "network.coop_player_collisions", + { "true", "false" } }, /* vehicle options (the side's set and counts: vehicles_update) */ { "vehicles_respawn_spinner", _option_short, OPTIONS_FIELD(vehicle_respawn_time), 0, 7, { 0, 30, 60, 90, 120, 180, 300 } }, @@ -4825,6 +4813,36 @@ static void gametype_options_each(struct widget_instance *list, boolean save) } } +/* each of a screen's rows of the host's own settings (_option_setting: +co-op's options' screens, tools/port_settings.py's _setup_option_screen), +shown, or (save) set */ +static void setting_options_each(struct widget_instance *list, boolean save) +{ + struct widget_instance *row; + + for (row = list->child; row; row = row->next) + { + struct widget_instance *spinner; + + for (spinner = row->child; spinner; spinner = spinner->next) + { + struct gametype_option const *option = spinner->type == 2 ? gametype_option_named(spinner->name) : NULL; + short index; + + if (!option || option->kind != _option_setting) + continue; + index = (short)PIN(spinner->parameters.list.selected_index, 0, option->count - 1); + if (!save) + spinner->parameters.list.selected_index = gametype_option_setting_index(option); + else if (index != gametype_option_setting_index(option) && + !setting_write(option->setting, option->setting_values[index])) + { + platform_log("menus: could not set %s", option->setting); + } + } + } +} + /* the options' list of a button of its bar (OK's): the one above it whose rows are its options */ static struct widget_instance *gametype_options_list(struct widget_instance *widget) @@ -5157,6 +5175,15 @@ static void gametype_option_help(struct widget_instance *list) visible_set(named(list, "op_primary_weapon", 0), custom); visible_set(named(list, "op_secondary_weapon", 0), custom); } + /* (CO-OP OPTIONS: the row of the amount of the extra enemies chosen, + PER PLAYER's or MULTIPLIER's, in the one place) */ + if (named(list, "coop_extra_enemies_spinner", 0)) + { + short mode = named(list, "coop_extra_enemies_spinner", 0)->parameters.list.selected_index; + + visible_set(named(list, "op_coop_enemies_per_player", 0), mode == _cooperative_enemies_per_player); + visible_set(named(list, "op_coop_enemies_multiplier", 0), mode == _cooperative_enemies_multiplier); + } /* (the server browser's filters, hidden: their helps are fewer than their values) */ if (!description || !list->focused_child || !strncmp(list->name, "filters", 7)) @@ -5325,6 +5352,18 @@ boolean pc_menu_event_function_invoke( { return gametype_setup_edit(); } + /* (co-op's options' screens: their settings shown, and OK's) */ + else if (!strcmp(name, "port setup options init")) + { + setting_options_each(widget, FALSE); + } + else if (!strcmp(name, "port setup options save")) + { + struct widget_instance *list = gametype_options_list(widget); + + if (list) + setting_options_each(list, TRUE); + } else if (!strcmp(name, "mp profile save changes")) { return ui_widget_port_gametype_save(widget, widget_deleted); diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index 0565835e..8b1de17c 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -354,6 +354,7 @@ static char const *const port_function_names[] = "mp level select", "mp profiles list initialize", "mp profiles list dispose", "mp profile set for game", "port lobby preview join", "port setup edit", + "port setup options init", "port setup options save", "port map list back", "port pause end game", "port coop begin", "port coop player 2 list initialize", "port coop player 2", diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index cc890700..c54915b4 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -348,32 +348,32 @@ static const struct config_setting config_settings[] = { "network.coop_friendly_fire", _config_string, "\"on\"", "HALO_NET_COOP_FRIENDLY_FIRE", _environment_value, _platform_all, "Whether the players of an online co-op game hurt each other: \"off\",\n" - "\"on\", \"shields_only\" or \"explosives_only\" (Server Setup's FRIENDLY\n" - "FIRE in co-op, which writes its choice here). Their AI allies they\n" - "always can, as in the campaign." }, + "\"on\", \"shields_only\" or \"explosives_only\" (FRIENDLY FIRE in\n" + "co-op's Server Setup > Co-op Options writes its choice here). Their AI\n" + "allies they always can, as in the campaign." }, { "network.coop_player_collisions", _config_boolean, "true", "HALO_NET_COOP_PLAYER_COLLISIONS", _environment_value, _platform_all, "Whether the players of an online co-op game bump into each other;\n" "false, they walk through each other (the AI's characters they still\n" - "bump into). Server Setup's PLAYER COLLISIONS in co-op writes its\n" - "choice here." }, + "bump into). PLAYER COLLISIONS in co-op's Server Setup > Co-op Options\n" + "writes its choice here." }, { "network.coop_enemies_mode", _config_string, "\"per_player\"", "HALO_NET_COOP_ENEMIES_MODE", _environment_value, _platform_all, "Online co-op's extra enemies: \"none\", \"per_player\" (each squad of\n" "enemies grows by coop_enemies for each player past the first) or\n" "\"multiplier\" (each is coop_enemies_multiplier times as large, for any\n" - "number of players). Server Setup's EXTRA ENEMIES in co-op writes its\n" - "choice here." }, + "number of players). EXTRA ENEMIES in co-op's Server Setup > Co-op\n" + "Options writes its choice here." }, { "network.coop_enemies", _config_integer, "50", "HALO_NET_COOP_ENEMIES", _environment_value, _platform_all, "Online co-op's extra enemies per player, a percentage: for each player\n" "past the first, each squad of enemies a level places gets this much of\n" - "itself more (100: as many again; 25 to 200). Server Setup's PER PLAYER\n" - "in co-op writes its choice here." }, + "itself more (100: as many again; 25 to 200). PER PLAYER in co-op's\n" + "Server Setup > Co-op Options writes its choice here." }, { "network.coop_enemies_multiplier", _config_integer, "2", "HALO_NET_COOP_ENEMIES_MULTIPLIER", _environment_value, _platform_all, "Online co-op's static multiplier of its enemies: each squad of enemies\n" - "a level places is this many times as large (2 to 32). Server Setup's\n" - "MULTIPLIER in co-op writes its choice here." }, + "a level places is this many times as large (2 to 32). MULTIPLIER in\n" + "co-op's Server Setup > Co-op Options writes its choice here." }, { "network.brokers_file", _config_string, "\"brokers.txt\"", "HALO_NET_BROKERS_FILE", _environment_value, _platform_all, "The file of the public MQTT brokers through which the machines of an\n" diff --git a/tools/port_settings.py b/tools/port_settings.py index 60af7991..f9e9b179 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -804,73 +804,25 @@ def _server_settings() -> list: rows.append((f"{base}/op_listing", None)) # PASSWORD (a public internet game's: menu_functions.c's server_settings_update), # which players from the server browser must type - extra += _value_row(base, "password", 15, "ss edit server password") + extra += _value_row(base, "password", 10 + len(COOP_SETUP_SCREENS), "ss edit server password") rows.append((f"{base}/op_password", None)) - # co-op's FRIENDLY FIRE (between its players), in the place of the - # gametype's rows, which co-op hides as multiplayer hides it, laid out - # as Teamplay Options' (menu_functions.c's server_settings_update); its - # choice kept in network.coop_friendly_fire - extra += _widget(f"{base}/op_friendly_fire", [("width", 512), ("height", 28), - ("flags", "pass_unhandled_to_focused_child"), - ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], - [f'', - f'']) - extra += _widget(f"{base}/friendly_fire_label", [("type", "text"), ("controller", 1), ("width", 300), - ("height", 22), ("string_list", f"{base}/labels"), - ("string_index", 10), ("font", "ui\\large_ui"), - ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) - extra += _widget(f"{base}/friendly_fire_spinner", - [("type", "spinner"), ("top", 2), ("width", 206), ("height", 20), - ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), - ("strings", "OFF|ON|SHIELD ONLY|EXPLOSIVES ONLY"), ("setting", "network.coop_friendly_fire"), - ("values", "|".join(COOP_FRIENDLY_FIRE_VALUES)), ("font", "ui\\large_ui"), - ("color", "#FF2896FF"), ("align", "center"), ("text_y", 4), - ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), - ("header_bounds", "7 -13 19 -7"), ("footer_bounds", "7 208 19 214")], - ['', '', - '']) - rows.append((f"{base}/op_friendly_fire", None, 5)) - # ... and its EXTRA ENEMIES (port/linux/game/coop_enemies.c), below it: - # NONE, PER PLAYER or STATIC MULTIPLIER, and below that the amount of the - # one chosen (its own row, the other hidden: menu_functions.c's - # server_settings_update); each kept in its network.coop_enemies setting - def spinner_row(key, label_index, strings, setting, values): - nonlocal extra + # co-op's options, in the place of the gametype's rows, which co-op hides + # as multiplayer hides co-op's (menu_functions.c's server_settings_update): + # rows opening their screens, as the gametype's options' do + # (_setup_option_screen; COOP_SETUP_SCREENS) + for index, (key, title, _, _) in enumerate(COOP_SETUP_SCREENS): + target = f"{base}/{key}/{key}_screen" extra += _widget(f"{base}/op_{key}", [("width", 512), ("height", 28), - ("flags", "pass_unhandled_to_focused_child"), - ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], - [f'', - f'']) - extra += _widget(f"{base}/{key}_label", [("type", "text"), ("controller", 1), ("width", 300), - ("height", 22), ("string_list", f"{base}/labels"), - ("string_index", label_index), ("font", "ui\\large_ui"), - ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) - extra += _widget(f"{base}/{key}_spinner", - [("type", "spinner"), ("top", 2), ("width", 206), ("height", 20), - ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), - ("strings", "|".join(strings)), ("setting", setting), - ("values", "|".join(str(value) for value in values)), ("font", "ui\\large_ui"), - ("color", "#FF2896FF"), ("align", "center"), ("text_y", 4), - ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), - ("header_bounds", "7 -13 19 -7"), ("footer_bounds", "7 208 19 214")], - ['', - '', - '']) - - spinner_row("extra_enemies", 11, ["NONE", "PER PLAYER", "STATIC MULTIPLIER"], "network.coop_enemies_mode", - COOP_ENEMIES_MODES) - rows.append((f"{base}/op_extra_enemies", None, 6)) - spinner_row("enemies_per_player", 12, [f"{value}%" for value in COOP_ENEMIES_PERCENTAGES], - "network.coop_enemies", COOP_ENEMIES_PERCENTAGES) - rows.append((f"{base}/op_enemies_per_player", None, 7)) - spinner_row("enemies_multiplier", 13, [f"{value}X" for value in COOP_ENEMIES_MULTIPLIERS], - "network.coop_enemies_multiplier", COOP_ENEMIES_MULTIPLIERS) - rows.append((f"{base}/op_enemies_multiplier", None, 7)) - # ... and its PLAYER COLLISIONS, below them (network.coop_player_collisions, - # which the host sends its players as the game begins: menu_functions.c's - # server_start) - spinner_row("player_collisions", 14, ["ON", "OFF"], "network.coop_player_collisions", ["true", "false"]) - rows.append((f"{base}/op_player_collisions", None, 8)) + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', f'', + '', + f'']) + extra += _widget(f"{base}/{key}_label", [("type", "text"), ("controller", 1), ("width", 300), ("height", 22), + ("string_list", f"{base}/labels"), ("string_index", 10 + index), + ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("text_x", 13), + ("text_y", 4)], []) + rows.append((f"{base}/op_{key}", None, 5 + index)) # the gametype's options for this game (the gametype editor's screens, # editing a copy of the gametype chosen: "port setup edit") for index, (key, screen) in enumerate(SETUP_OPTION_SCREENS): @@ -900,8 +852,8 @@ def _server_settings() -> list: '']) extra += _strings(f"{base}/labels", ["GAME NAME:", "MAXIMUM PLAYERS:", "INVITE LINK:", "GAME TYPE:", "PLAYER OPTIONS:", "ITEM OPTIONS:", "VEHICLE OPTIONS:", "INDICATOR OPTIONS:", - "TEAMPLAY OPTIONS:", "LISTING:", "FRIENDLY FIRE:", "EXTRA ENEMIES:", - "PER PLAYER:", "MULTIPLIER:", "PLAYER COLLISIONS:", "PASSWORD:"]) + "TEAMPLAY OPTIONS:", "LISTING:", + *(f"{title}:" for _, title, _, _ in COOP_SETUP_SCREENS), "PASSWORD:"]) extra += _strings(f"{base}/help_strings", [ "", "The name the game shows in the lists of games.\\nEnter changes it.", @@ -916,26 +868,16 @@ def _server_settings() -> list: # (LISTING's, by its choice) "Anyone can see and join your game: it is listed\\nin everyone's Server Browser.", "Only players with your invite link can join.", - # (FRIENDLY FIRE's, by its choice, as Teamplay Options' words them) - "Players can not be hurt by weapons and explosives\\nfired by the other players.", - "Players can be hurt by weapons or explosives\\nfired by the other players.", - "Damage from the other players will only reduce\\nshields. Health will be unaffected.", - "Players can be hurt by damage from explosives\\nfired by the other players.", - # (EXTRA ENEMIES', by its choice: COOP_ENEMIES_MODES' order) - "Enemy squads are as the campaign has them.", - "Enemy squads grow with the players: by the amount\\nbelow for each player past the first.", - "Enemy squads are the amount below times as large,\\nhowever many players there are.", - # (PER PLAYER's and MULTIPLIER's) - "For each player past the first, enemy squads get\\nthis much more of themselves (100%: as many again).", - "Each enemy squad is this many times as large.", - # (PLAYER COLLISIONS', by its choice) - "Players bump into each other, as in the campaign.", - "Players walk through each other, so that no one\\nblocks a doorway. The AI's characters still block.", + # (co-op's options' rows: COOP_SETUP_SCREENS) + *(help_text for _, _, help_text, _ in COOP_SETUP_SCREENS), # (PASSWORD's) "Players from the Server Browser must type it to\\njoin (an invite link needs none). Enter sets it.", ]) lines = _screen(base, spec, rows, ["server settings update"], [''], extra) + # (co-op's options' screens) + for key, title, _, screen_rows in COOP_SETUP_SCREENS: + lines += _setup_option_screen(f"{base}/{key}", key, screen_rows) # (no Defaults: the bar is START and CANCEL; B cancels the name's edit # first) lines = [line.replace(f'', "") for line in lines] @@ -945,15 +887,102 @@ def _server_settings() -> list: MAXIMUM_PLAYERS = [2, 4, 8, 12, 16, 24, 32, 48, 64, 96, 128] -# network.coop_friendly_fire's values, as Server Setup's FRIENDLY FIRE shows -# them (menu_functions.c's cooperative_friendly_fire_modes, in this order) -COOP_FRIENDLY_FIRE_VALUES = ["off", "on", "shields_only", "explosives_only"] -# Server Setup's EXTRA ENEMIES: network.coop_enemies_mode's values (in -# coop_enemies.c's order), and the amounts of PER PLAYER (network.coop_enemies, -# percentages) and STATIC MULTIPLIER (network.coop_enemies_multiplier) -COOP_ENEMIES_MODES = ["none", "per_player", "multiplier"] +# CO-OP OPTIONS' amounts of PER PLAYER (network.coop_enemies, percentages) +# and STATIC MULTIPLIER (network.coop_enemies_multiplier), as menu_functions.c's +# gametype_options have their values COOP_ENEMIES_PERCENTAGES = [25, 50, 100, 150, 200] COOP_ENEMIES_MULTIPLIERS = [2, 4, 8, 16, 32] +# co-op's Server Setup options, in screens of their own as the gametype's +# are (its rows: _server_settings), each its key, its title, its row's help +# and its rows (as TEAMPLAY_ROWS: each spinner's key, label, words and the +# help of each of them; menu_functions.c's gametype_options give their +# settings' values) +COOP_SETUP_SCREENS = [ + ("coop_options", "CO-OP OPTIONS", "Friendly fire, the enemies and player collisions,\\nfor this game.", [ + ("coop_friendly_fire", "FRIENDLY FIRE:", ["OFF", "ON", "SHIELD ONLY", "EXPLOSIVES ONLY"], [ + "Players can not be hurt by weapons and explosives\\nfired by the other players.", + "Players can be hurt by weapons or explosives\\nfired by the other players.", + "Damage from the other players will only reduce\\nshields. Health will be unaffected.", + "Players can be hurt by damage from explosives\\nfired by the other players.", + ]), + ("coop_extra_enemies", "EXTRA ENEMIES:", ["NONE", "PER PLAYER", "STATIC MULTIPLIER"], [ + "Enemy squads are as the campaign has them.", + "Enemy squads grow with the players: by the amount\\nbelow for each player past the first.", + "Enemy squads are the amount below times as large,\\nhowever many players there are.", + ]), + ("coop_enemies_per_player", "PER PLAYER:", [f"{value}%" for value in COOP_ENEMIES_PERCENTAGES], + ["For each player past the first, enemy squads get\\nthis much more of themselves (100%: as many again)."] * + len(COOP_ENEMIES_PERCENTAGES)), + ("coop_enemies_multiplier", "MULTIPLIER:", [f"{value}X" for value in COOP_ENEMIES_MULTIPLIERS], + ["Each enemy squad is this many times as large."] * len(COOP_ENEMIES_MULTIPLIERS)), + ("coop_player_collisions", "PLAYER COLLISIONS:", ["ON", "OFF"], [ + "Players bump into each other, as in the campaign.", + "Players walk through each other, so that no one\\nblocks a doorway. The AI's characters still block.", + ]), + ]), + ("voice_options", "VOICE AND VOTING", "Voice chat and votes to kick a player, for this\\ngame.", TEAMPLAY_ROWS), +] +# (their screens' titles) +TITLES.update({f"{MT}/server_settings/{key}/header_{key}": title for key, title, _, _ in COOP_SETUP_SCREENS}) +# (the rows of a co-op options screen in one place, the one shown: PER +# PLAYER's and MULTIPLIER's, by EXTRA ENEMIES') +SETUP_OPTION_SAME_PLACE = {"coop_enemies_multiplier"} + + +def _setup_option_screen(base: str, key: str, rows: list) -> list: + """a screen of options laid out as the gametype's are (Teamplay + Options'), its spinners setting config.toml's settings (menu_functions.c's + "port setup options init" and "port setup options save", OK), each + choice's help shown as the gametype's are ("game settings lists text + update": the help strings, each spinner's choices' in turn)""" + lines = _widget(f"{base}/{key}_screen", [("width", 640), ("height", 480), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/gradient")], + [f'', f'']) + lines += _header(f"{base}/header_{key}", f"{base}/header_{key}") + lines += _widget(f"{base}/{key}_help", [("type", "text"), ("controller", 1), ("left", 68), ("top", 341), + ("width", 482), ("height", 79), ("string_list", f"{base}/cap_{key}"), + ("font", "ui\\large_ui"), ("color", "#FFFFFFFF")], []) + children = ['', + ''] + place = -1 + for row_key, *_ in rows: + if row_key not in SETUP_OPTION_SAME_PLACE: + place += 1 + children.append(f'') + children.append(f'') + lines += _widget(f"{base}/{key}_menu", [("type", "column_list"), ("width", 640), ("height", 480), + ("flags", "pass_unhandled_to_focused_child up_down_tabs_children"), + ("description", f"{base}/{key}_help")], children) + lines += _widget(f"{base}/{key}_button_bar", [("type", "column_list"), ("width", 640), ("height", 28), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items")], + ['', f'', + '']) + lines += _button(f"{base}/{key}_button_ok", 1, ['', + '']) + for index, (row_key, label, strings, _) in enumerate(rows): + lines += _widget(f"{base}/op_{row_key}", [("width", 512), ("height", 28), + ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', + f'']) + lines += _widget(f"{base}/{row_key}_label", [("type", "text"), ("controller", 1), ("width", 300), + ("height", 22), ("string_list", f"{base}/{key}_labels"), + ("string_index", index), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) + lines += _widget(f"{base}/{row_key}_spinner", + [("type", "spinner"), ("top", 2), ("width", 206), ("height", 20), + ("flags", "pass_unhandled_to_focused_child left_right_tabs_items"), + ("string_list", f"{base}/var_{row_key}"), ("font", "ui\\large_ui"), + ("color", "#FF2896FF"), ("align", "center"), ("text_y", 4), + ("list_flags", "items_from_strings"), + ("header_bitmap", "bitmaps/arrow_sm_left"), ("footer_bitmap", "bitmaps/arrow_sm_right"), + ("header_bounds", "7 -13 19 -7"), ("footer_bounds", "7 208 19 214")], + ['']) + lines += _strings(f"{base}/var_{row_key}", strings) + lines += _strings(f"{base}/{key}_labels", [label for _, label, *_ in rows]) + lines += _strings(f"{base}/cap_{key}", [text for *_, helps in rows for text in helps]) + return lines # Server Setup's rows of the gametype's options: the gametype editor's # screens SETUP_OPTION_SCREENS = [ From f39f7df5863408be2afa35e5e5a739a4f61669b8 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 22:43:59 -0400 Subject: [PATCH 075/187] Linux: the trace_marker refusal does nothing on the Steam Deck The README and posix_trace_marker.c said the Steam Deck runs the same gpu-trace service and Mesa as the Steam Frame, so the refusal would help there too. It runs the same service (tracing on; the trace buffer takes some 800,000 writes a second), but the Deck's Mesa, 25.3 in both its 32-bit and 64-bit libraries, has no trace marker code, so it writes none and the refusal changes nothing. Measured on a Steam Deck LCD (SteamOS 3.8.16, Game Mode, b30 with a bot, 90 s at 60 Hz): with HALO_GPU_TRACE_MARKERS=1, 16.67 ms frames, 0% late, 4.14 W on the APU; refused, 16.69 ms, 0.02% late, 4.12-4.35 W. --- port/linux/README.md | 4 +++- port/linux/src/posix_trace_marker.c | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index 4c030097..531df886 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -861,7 +861,9 @@ definition. Without this check, the linker gives the reference the address performance captures (`gpu-trace.service`), and its Mesa then writes a marker for each traced driver function: on the Steam Frame, some 480,000 writes a second, which took the game from the headset's 72 Hz to about - 50 frames a second. The Steam Deck runs the same service and Mesa. + 50 frames a second. The Steam Deck runs the same service, but its Mesa + (25.3, 32-bit and 64-bit) has no markers to write: there the refusal + changes nothing (measured: the same frame times and power either way). `HALO_GPU_TRACE_MARKERS=1` lets the driver write them, to capture with gpuvis. diff --git a/port/linux/src/posix_trace_marker.c b/port/linux/src/posix_trace_marker.c index 4a0a853e..764f41ee 100644 --- a/port/linux/src/posix_trace_marker.c +++ b/port/linux/src/posix_trace_marker.c @@ -8,7 +8,8 @@ buffer for each traced driver function while tracing is on, and SteamOS keeps tracing on for its GPU performance captures (gpu-trace.service). On the Steam Frame that is some 480,000 writes a second: the driver's thread spends its time in them, and the game ran at about 50 frames a second instead of -the headset's 72. The Steam Deck runs the same service (not measured there). A +the headset's 72. The Steam Deck runs the same service, but its Mesa (25.3) +has no markers to write, so there this changes nothing (measured). A trace_marker that cannot be opened turns the markers off (gpuvis then skips them). From 041211229a22c880f3eb4cc442bd10ebabcb7d94 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 21:36:26 -0400 Subject: [PATCH 076/187] Linux: the portable build starts on SteamOS and older distributions The Linux release did not start on a Steam Deck: it was linked against the build machine's glibc (Arch's, so it asked for GLIBC_2.43; SteamOS 3.8 has 2.41) and needed a 32-bit SDL3, which SteamOS, whose system is read-only, and most distributions do not have. The portable build (configure.py --portable, which tools/ci_build.py and so every GitHub Actions build uses) is now compiled and linked against the 32-bit glibc 2.31 of Debian 11, the Steam Runtime 3 ("sniper") baseline, and brings its own SDL 3.4.16, libSDL3.so.0 beside the executable, which finds it there first: a DT_RPATH of $ORIGIN (--disable-new-dtags), which comes before LD_LIBRARY_PATH, as Steam sets one for the games it starts. tools/linux_sysroot.py downloads the Debian packages and SDL's source release into build/linux/third_party, from a ninja edge, so only a Linux build fetches them. Each package comes from archive.debian.org, else deb.debian.org, else snapshot.debian.org (a permanent URL, at a time when the package was in the archive); SDL from its GitHub release, else libsdl.org. Each URL is tried three times, a moment apart, and each file checked against its SHA-256. tools/linux_sysroot.cmake builds SDL against the same system root, with X11, Wayland, libdecor (Debian 12's, for GNOME's window decorations), PipeWire, PulseAudio, ALSA, udev and D-Bus loaded when it starts, and the build stops if SDL lacks one of them or would link to libdecor. A build without --portable is as before. The workflow installs CMake, pkgconf and wayland-scanner instead of lib32-sdl3, and caches the downloads; the release zip gains libSDL3.so.0 and its license (SDL3-LICENSE.txt), and the self-updater deletes their .old copies. With --pgo=train, the instrumented build's copy of libSDL3.so.0 is made before it plays. The release build needs GLIBC_2.29 at most (halo and libSDL3.so.0, by readelf -V), and links only to libSDL3.so.0, libm, libpthread, libdl, libgcc_s and libc; libSDL3.so.0 only to libm, libdl, libpthread and libc (libdecor-0.so.0 is a name it loads, not a dependency). It should start on Debian 11, Ubuntu 20.04, Fedora 32 and later, untested: there, the 32-bit Wayland libraries are older than the 1.20 that SDL's Wayland driver needs, so SDL uses X11 (XWayland in a Wayland session). On a Steam Deck (SteamOS 3.8.16, Game Mode, started as Steam starts a non-Steam game), the upstream release stops at load time (libSDL3.so.0 not found; GLIBC_2.43 not found); this one loads b30 on the GPU (radeonsi, OpenGL 4.6) and exits with status 0 at debug.exit_after, with its own libSDL3.so.0 though LD_LIBRARY_PATH names a folder holding another. Debug and release builds from an empty build folder (every download), and tools/test_linux_port.py, tools/harness and tools/test_light_storage.py, pass in an Arch container with the workflow's packages. --- .github/workflows/build.yml | 18 ++- README.md | 2 +- configure.py | 6 +- port/linux/README.md | 58 ++++++- port/linux/src/updater.c | 3 +- tools/ci_build.py | 4 +- tools/linux_build.py | 122 ++++++++++++-- tools/linux_sysroot.cmake | 18 +++ tools/linux_sysroot.py | 315 ++++++++++++++++++++++++++++++++++++ 9 files changed, 521 insertions(+), 25 deletions(-) create mode 100644 tools/linux_sysroot.cmake create mode 100644 tools/linux_sysroot.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7e092a14..2440bc2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,8 +8,8 @@ name: Build # - one job per platform builds both configurations, so its set-up (tools, # downloads, caches) is paid once; # - compiles go through ccache, whose cache carries over from run to run, so -# a commit only recompiles what it changed; downloads (SDL, musl, Gradle) -# are cached too; +# a commit only recompiles what it changed; downloads (SDL, musl, Gradle, +# the Linux build's system root) are cached too; # - a newer push to the same branch cancels a run still going; # - pushes that only change documentation do not build; # - Windows, whose minutes cost twice Linux's, builds only Windows; @@ -41,7 +41,11 @@ env: jobs: linux: # Arch Linux has what the build needs as packages: clang 22 (for the - # profile-guided optimisation in pgo/) and 32-bit SDL 3. + # profile-guided optimisation in pgo/), and CMake, pkg-config and + # wayland-scanner for the SDL 3 that the portable build brings. The build + # links against Debian 11's 32-bit glibc (tools/linux_sysroot.py), not + # Arch's, so that it starts on SteamOS and older distributions; Arch's + # 32-bit glibc is for the tests. runs-on: ubuntu-24.04 container: archlinux:latest timeout-minutes: 40 @@ -50,7 +54,7 @@ jobs: run: | printf '\n[multilib]\nInclude = /etc/pacman.d/mirrorlist\n' >> /etc/pacman.conf pacman -Syu --noconfirm --needed git zstd python python-pytest ninja ccache \ - clang lld gcc lib32-glibc lib32-gcc-libs lib32-sdl3 + clang lld gcc lib32-glibc lib32-gcc-libs cmake pkgconf wayland - uses: actions/checkout@v4 @@ -60,6 +64,12 @@ jobs: key: ccache-linux-${{ github.sha }} restore-keys: ccache-linux- + # the system root and SDL's source, which the build downloads + - uses: actions/cache@v4 + with: + path: build/linux/third_party + key: linux-third-party-${{ hashFiles('tools/linux_sysroot.py') }} + - name: Debug run: python tools/ci_build.py linux debug diff --git a/README.md b/README.md index 2d9943fb..eb9010c1 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Give these options to `configure.py`: | --- | --- | | (none) | A debug build. A failed assertion stops the game. | | `--release` | A release build. The game does not examine assertions, as in the retail game. | -| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. | +| `--portable` | The Linux and Windows builds operate on all x86-64 processors. The Linux build also operates on older distributions and on SteamOS: refer to "Portable build" in [port/linux/README.md](port/linux/README.md#portable-build). Use this option for builds that you give to other persons. | | `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. | | `--pgo=off` | No profile-guided optimization. | | `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". | diff --git a/configure.py b/configure.py index 3bff193b..09259b5e 100644 --- a/configure.py +++ b/configure.py @@ -44,7 +44,11 @@ parser.add_argument( "--portable", action="store_true", help="x86 builds (Linux, Windows): code for any x86-64 processor (SSE2) rather than for this " - "machine's (-march=native, the default); use it for builds that run on other computers", + "machine's (-march=native, the default); use it for builds that run on other computers. On Linux it also " + "builds against Debian 11's glibc 2.31 rather than this machine's, and brings its own SDL 3 (libSDL3.so.0, " + "beside the executable), so that it starts on SteamOS and older distributions: the first build downloads " + "the Debian packages and SDL's source (build/linux/third_party, tools/linux_sysroot.py), and needs CMake, " + "pkgconf and wayland-scanner, not the 32-bit SDL 3", ) parser.add_argument( "--pgo", diff --git a/port/linux/README.md b/port/linux/README.md index 4c030097..fe1a3cf1 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -21,13 +21,65 @@ To build: - The 32-bit glibc development files: `lib32-glibc` on Arch Linux, `gcc-multilib` and `libc6-dev-i386` on Debian and Ubuntu. - The 32-bit SDL3: `lib32-sdl3` on Arch Linux, `libsdl3-dev:i386` on Debian - and Ubuntu. + and Ubuntu. The portable build does not need it: refer to "Portable + build". To start the game: +- glibc 2.29 or later (32-bit), for the builds from GitHub Actions, which + are built against glibc 2.31. They start on SteamOS 3 (Steam Deck). They + should also start on Debian 11, Ubuntu 20.04, Fedora 32 and later + distributions, but this is not tested. A build that you make without + `--portable` needs the glibc of the computer that built it, or a later + one. - The 32-bit OpenGL libraries (`lib32-mesa`). - The 32-bit PipeWire or PulseAudio client libraries (`lib32-pipewire` or `lib32-libpulse`). +- The 32-bit X11 libraries (`lib32-libx11`, `lib32-libxext`). In a Wayland + session, the game uses them through XWayland. SDL uses Wayland itself + only if the 32-bit Wayland libraries are version 1.20 or later + (`lib32-wayland`, `lib32-libxkbcommon`; Debian 11, Ubuntu 20.04 and + Fedora 32 have older ones) and the compositor has the fifo-v1 protocol. + On GNOME, the window then has borders only with the 32-bit libdecor + (`lib32-libdecor`). + +SteamOS has all of these. Its system is read-only, and the builds from +GitHub Actions need no package: they bring their own SDL3 +(`libSDL3.so.0`, next to the executable). + +### Portable build + +The builds from GitHub Actions are portable builds (`--portable`; refer to +"Build options" in the main [README](../../README.md#build-options)). The +portable build starts on more systems than the build machine's: + +- It is compiled and linked against the 32-bit glibc 2.31 of Debian 11, the + glibc of the Steam Runtime 3 ("sniper"), not against the glibc of the + build machine. An executable needs the glibc version that it was built + against, or a later one. +- It brings SDL 3 (`libSDL3.so.0`), built from source against the same + glibc. The executable looks for it in its own folder first (a `DT_RPATH` + of `$ORIGIN`, which comes before `LD_LIBRARY_PATH`), so that it uses this + SDL even when Steam sets `LD_LIBRARY_PATH`. Few distributions have a + 32-bit SDL 3. This SDL loads X11, Wayland, libdecor, PipeWire, PulseAudio + and ALSA when it starts, so it starts with whichever of them the system + has. It is linked only to glibc. Its license is `SDL3-LICENSE.txt`. + +At the first build, `tools/linux_sysroot.py` downloads the Debian packages +(about 25 MB) and the source of SDL. Each package comes from +`archive.debian.org` or `deb.debian.org`, or else from +`snapshot.debian.org`; the source of SDL comes from GitHub or from +`libsdl.org`. It checks each download against its SHA-256 sum. The system root is in +`build/linux/third_party/sysroot`. `tools/linux_sysroot.cmake` builds SDL +against it. + +To make the portable build you need, in addition to the tools above, +CMake, `pkg-config` (`pkgconf`) and `wayland-scanner` (`wayland` on Arch +Linux, `libwayland-bin` on Debian and Ubuntu). You do not need the 32-bit +SDL3. You need the 32-bit glibc only for the tests. + +To see the glibc version that a build needs, enter +`readelf -V build/linux/halo | grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1`. ## Build the game @@ -39,6 +91,10 @@ To start the game: Enter `build/linux/halo`. +To play on a Steam Deck, unpack `halo-linux-release.zip` into a folder (in +Desktop Mode), and add `halo` to Steam as a non-Steam game ("Add a Game" in +the Games menu of Steam). The game then starts in Game Mode as well. + The game data is the folder that contains `maps/`, from an Xbox disc image of any version of the game. The game looks for this folder in this sequence: diff --git a/port/linux/src/updater.c b/port/linux/src/updater.c index 1b26a2cb..bd2fd116 100644 --- a/port/linux/src/updater.c +++ b/port/linux/src/updater.c @@ -525,7 +525,8 @@ static void updater_clean_up(void) { static const char *const names[] = { - "halo.old", "halo.exe.old", "SDL3.dll.old", "extract-xiso-LICENSE.txt.old", "mbedtls-LICENSE.txt.old", + "halo.old", "halo.exe.old", "SDL3.dll.old", "libSDL3.so.0.old", "SDL3-LICENSE.txt.old", + "extract-xiso-LICENSE.txt.old", "mbedtls-LICENSE.txt.old", }; char path[1200]; size_t index; diff --git a/tools/ci_build.py b/tools/ci_build.py index 5bf9f441..1f4b580d 100644 --- a/tools/ci_build.py +++ b/tools/ci_build.py @@ -26,7 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent # what each port's build leaves, and what goes into dist/ OUTPUTS = { - "linux": ["build/linux/halo"], + # (and the SDL3 the portable build brings, tools/linux_build.py, with its + # zlib license, as the release carries the other libraries') + "linux": ["build/linux/halo", "build/linux/libSDL3.so.0", "build/linux/SDL3-LICENSE.txt"], "windows": ["build/windows/halo.exe", "build/windows/SDL3.dll"], "android": [], # the APK, below } diff --git a/tools/linux_build.py b/tools/linux_build.py index 4064a222..5ac13fde 100644 --- a/tools/linux_build.py +++ b/tools/linux_build.py @@ -228,6 +228,77 @@ def march_flag(sln: Any) -> str: return "-march=x86-64" if getattr(sln, "port_portable", False) else "-march=native" +# The portable build (configure.py --portable, as tools/ci_build.py builds) +# starts on other Linux systems than the one that built it, the Steam Deck's +# among them: it is compiled and linked against Debian 11's glibc 2.31, not +# the build machine's (an executable asks for the glibc version it was built +# with, or older), and it brings its own SDL3, libSDL3.so.0 beside the +# executable, which finds it there (an rpath of $ORIGIN), as few +# distributions have a 32-bit SDL3. tools/linux_sysroot.py downloads the +# system root and SDL's source, and tools/linux_sysroot.cmake builds SDL +# against it. +SYSROOT_SCRIPT = Path("tools/linux_sysroot.py") +SYSROOT_TOOLCHAIN = Path("tools/linux_sysroot.cmake") +# SDL's options: what it would build on a desktop distribution, its X11, +# Wayland (with libdecor's window decorations, which GNOME's Wayland asks +# for), PipeWire, PulseAudio and ALSA loaded when it starts (dlopen), less +# what needs libraries the system root does not have and the game does not +# use (KMS/DRM without a desktop, input methods, libusb, JACK, sndio, +# io_uring) and SDL's own tests +SDL_OPTIONS = [ + "-DCMAKE_BUILD_TYPE=Release", "-DSDL_SHARED=ON", "-DSDL_STATIC=OFF", "-DSDL_TEST_LIBRARY=OFF", + "-DSDL_TESTS=OFF", "-DSDL_EXAMPLES=OFF", "-DSDL_KMSDRM=OFF", "-DSDL_WAYLAND_LIBDECOR=ON", + "-DSDL_WAYLAND_LIBDECOR_SHARED=ON", "-DSDL_IBUS=OFF", "-DSDL_HIDAPI_LIBUSB=OFF", "-DSDL_JACK=OFF", + "-DSDL_SNDIO=OFF", "-DSDL_LIBURING=OFF", "-DSDL_FRIBIDI=OFF", "-DSDL_LIBTHAI=OFF", "-DSDL_X11_XTEST=OFF", +] + + +def portable_sdl(n: Writer, sln: Any, build_dir: Path, cc: str) -> Tuple[List[str], List[str], Path, Path, Path]: + """The portable build's system root and SDL3: the compiler's flags, the + linker's, what every unit waits for (the system root's stamp), the + libSDL3.so.0 that the executable links to, and SDL's license (which the + release carries beside it).""" + third_party = build_dir / "third_party" + sysroot = third_party / "sysroot" + sdl_source = third_party / "SDL3" + stamp = third_party / "stamp" + sdl_build = build_dir / "sdl3-build" + libsdl = sdl_build / "libSDL3.so.0" + n.rule( + name="linux_sysroot", + command=f"$python {SYSROOT_SCRIPT} {third_party}", + description="LINUX SYSROOT (Debian 11 i386, SDL3 source)", + restat=True, + ) + # (and SDL's license, which the release carries) + sdl_license_source = sdl_source / "LICENSE.txt" + n.build(outputs=stamp, rule="linux_sysroot", implicit=[SYSROOT_SCRIPT], implicit_outputs=[sdl_license_source]) + launcher = getattr(sln, "compiler_launcher", None) + options = SDL_OPTIONS + ([f"-DCMAKE_C_COMPILER_LAUNCHER={launcher}"] if launcher else []) + n.rule( + name="linux_sdl3", + command=(f"cmake -S {sdl_source} -B {sdl_build} -G Ninja --fresh " + f"-DCMAKE_TOOLCHAIN_FILE=$$PWD/{SYSROOT_TOOLCHAIN} -DHALO_SYSROOT=$$PWD/{sysroot} " + f"-DCMAKE_C_COMPILER={cc} {' '.join(options)} > {build_dir}/sdl3-configure.log && " + f"ninja -C {sdl_build} > {build_dir}/sdl3-build.log && " + f"$python {SYSROOT_SCRIPT} --check-sdl " + f"{sdl_build}/include-config-release/build_config/SDL_build_config.h"), + description="LINUX SDL3", + pool="console", + restat=True, + ) + n.build(outputs=libsdl, rule="linux_sdl3", implicit=[stamp, SYSROOT_TOOLCHAIN]) + cflags = [f"--sysroot={_quote(sysroot)}", f"-isystem {_quote(sdl_source / 'include')}"] + # (DT_RPATH rather than DT_RUNPATH: it comes before LD_LIBRARY_PATH, + # which Steam sets for the games it starts, so that the SDL beside the + # executable is the one loaded, whatever other SDL that path holds) + ldflags = [f"--sysroot={_quote(sysroot)}", f"-L{_quote(sdl_build)}", "'-Wl,-rpath,$$ORIGIN'", + "-Wl,--disable-new-dtags"] + sdl_license = build_dir / "SDL3-LICENSE.txt" + n.build(outputs=sdl_license, rule="linux_copy", inputs=sdl_license_source) + return cflags, ldflags, stamp, libsdl, sdl_license + + def lto_mode(sln: Any) -> str: """full, thin or off (configure.py --lto)""" return getattr(sln, "port_lto", "full") @@ -389,6 +460,8 @@ def generate_linux_build(n: Writer, sln: Any) -> None: rspfile_content="$in_newline", ) + n.rule(name="linux_copy", command="cp -L $in $out", description="LINUX COPY $out") + n.rule( name="linux_pgo_train", command="$python tools/pgo_train.py --binary $binary --work $work --output $out", @@ -399,11 +472,20 @@ def generate_linux_build(n: Writer, sln: Any) -> None: # the high-res HUD's textures (port/assets/hud; port/linux/src/hud_hires.c) embedded_assets = hud_assets_build(n, "linux", build_dir / "generated" / "hud_hires_assets.c") + # the portable build's system root and SDL3, or the build machine's + if getattr(sln, "port_portable", False): + sysroot_cflags, sysroot_ldflags, sysroot_stamp, libsdl, sdl_license = portable_sdl(n, sln, build_dir, cc) + sysroot_inputs = [sysroot_stamp] + sdl_outputs = [build_dir / libsdl.name, sdl_license] + else: + sysroot_cflags, sysroot_ldflags, sysroot_inputs, libsdl, sdl_outputs = [], [], [], None, [] + target = [march_flag(sln), *sysroot_cflags] + # (a debug build checks its stack frames, and stops at the first one # overrun, as it stops at the first failed assertion; a release build # does not, so that an overrun nobody has met cannot end a game) - abi = " ".join(LINUX_ABI_FLAGS + [march_flag(sln)] + (["-DHALO_RELEASE"] if getattr(sln, "port_release", False) - else ["-fstack-protector-strong"])) + abi = " ".join(LINUX_ABI_FLAGS + target + (["-DHALO_RELEASE"] if getattr(sln, "port_release", False) + else ["-fstack-protector-strong"])) port_include = PORT_DIR / "include" sdk_flags = f"-idirafter {XDK_INCLUDE}" libs = " ".join(f"-l{lib}" for lib in config.get("libraries", [])) @@ -429,7 +511,7 @@ def generate_linux_build(n: Writer, sln: Any) -> None: # (the SDK declarations are system headers, which the depfile # leaves out) implicit=[*xdk_headers(), prefix_header, semantics_header, platform_semantics_header, - *implicit_inputs], + *sysroot_inputs, *implicit_inputs], variables={"cflags": f"{cflags} {posix_extra if posix else extra}"}, ) @@ -472,7 +554,7 @@ def generate_linux_build(n: Writer, sln: Any) -> None: "-Isource -Isource/cseries", sdk_flags, ]) - posix_cflags = " ".join(POSIX_FLAGS + [march_flag(sln), f"-I{platform_dir}"]) + posix_cflags = " ".join(POSIX_FLAGS + target + [f"-I{platform_dir}"]) mbedtls_include = f"-I{MBEDTLS_DIR / 'include'}" for source in sorted(platform_dir.glob("*.c")): if source.name == "posix_update.c": @@ -492,15 +574,16 @@ def generate_linux_build(n: Writer, sln: Any) -> None: # wcslen, which linux_link_check.py rejects: the game's wchar_t is # 16-bit) for source in sorted((MBEDTLS_DIR / "library").glob("*.c")): - add_object(source, " ".join(POSIX_FLAGS + [march_flag(sln), mbedtls_include, - f"-I{MBEDTLS_DIR / 'library'}", "-fno-builtin-wcslen", - "-w"]), posix=True) + add_object(source, " ".join(POSIX_FLAGS + target + [mbedtls_include, + f"-I{MBEDTLS_DIR / 'library'}", "-fno-builtin-wcslen", + "-w"]), posix=True) # internet play's UPnP (port/third_party/miniupnpc), with the host's # ABI as posix_upnp.c, which uses it for source in miniupnpc_sources(): - add_object(source, " ".join(POSIX_FLAGS + [march_flag(sln), *MINIUPNPC_DEFINES, - f"-I{MINIUPNPC_DIR / 'include'}", f"-I{MINIUPNPC_DIR / 'src'}", - "-fno-builtin-wcslen", "-w"]), posix=True) + add_object(source, " ".join(POSIX_FLAGS + target + [*MINIUPNPC_DEFINES, + f"-I{MINIUPNPC_DIR / 'include'}", + f"-I{MINIUPNPC_DIR / 'src'}", + "-fno-builtin-wcslen", "-w"]), posix=True) # the settings file's parser (port/third_party/tomlc17), with the # platform layer's ABI (its structs hold doubles) and nothing else add_object(TOML_DIR / "tomlc17.c", " ".join([abi, "-std=gnu11", "-w"])) @@ -529,14 +612,18 @@ def generate_linux_build(n: Writer, sln: Any) -> None: rule="linux_link", inputs=objects, variables={ - "ldflags": " ".join(["--target=i686-linux-gnu", "-m32", "-no-pie", "-g", *extra_ldflags, + "ldflags": " ".join(["--target=i686-linux-gnu", "-m32", "-no-pie", "-g", *sysroot_ldflags, + *extra_ldflags, # (posix_trace_marker.c's, which the GPU driver's calls must reach) *(f"-Wl,--export-dynamic-symbol={name}" for name in ("open", "open64", "openat", "openat64"))]), "libs": libs, }, - implicit=[Path("tools/linux_link_check.py")], + implicit=[Path("tools/linux_link_check.py"), *([libsdl] if libsdl else [])], ) + # (the portable build's SDL3, beside the executable) + if libsdl is not None: + n.build(outputs=output.parent / libsdl.name, rule="linux_copy", inputs=libsdl) # the tag validator (port/linux/game/tag_validate.c, tag_schema*.c) # alone on map files: the game's objects of it, and a program that @@ -549,6 +636,7 @@ def generate_linux_build(n: Writer, sln: Any) -> None: outputs=tool_object, rule="linux_cc", inputs=tool, + implicit=sysroot_inputs, variables={"cflags": " ".join([posix_cflags, f"-I{ZLIB_DIR}", *ZLIB_DEFINES, posix_extra])}, ) # (and the Custom Edition maps' loader, cache_file_formats.c) @@ -560,7 +648,8 @@ def generate_linux_build(n: Writer, sln: Any) -> None: outputs=validator, rule="linux_tool_link", inputs=tool_objects, - variables={"ldflags": " ".join(["--target=i686-linux-gnu", "-m32", "-no-pie", "-g", *extra_ldflags])}, + variables={"ldflags": " ".join(["--target=i686-linux-gnu", "-m32", "-no-pie", "-g", *sysroot_ldflags, + *extra_ldflags])}, ) # Profile-guided optimisation: with the committed profile, or with @@ -574,7 +663,8 @@ def generate_linux_build(n: Writer, sln: Any) -> None: n.build( outputs=profile, rule="linux_pgo_train", - order_only=[instrumented], + # (and the portable build's SDL3 beside it, which it loads) + order_only=[instrumented, *([instrumented.parent / libsdl.name] if libsdl else [])], variables={"binary": str(instrumented), "work": str(sln.build_dir / "pgo" / "linux")}, ) @@ -586,7 +676,7 @@ def generate_linux_build(n: Writer, sln: Any) -> None: emit(obj_dir, output, cflags, ldflags, [profile] if profile else [], validator) # internet play's MQTT brokers, a file beside the game (network.brokers_file) brokers = build_dir / "brokers.txt" - n.rule(name="linux_copy", command="cp $in $out", description="LINUX COPY $out") n.build(outputs=brokers, rule="linux_copy", inputs=Path("port/assets/network/brokers.txt")) - n.build(outputs="linux", rule="phony", inputs=[output, brokers, validator]) + n.build(outputs="linux", rule="phony", + inputs=[output, brokers, validator, *sdl_outputs]) n.newline() diff --git a/tools/linux_sysroot.cmake b/tools/linux_sysroot.cmake new file mode 100644 index 00000000..e9583f9e --- /dev/null +++ b/tools/linux_sysroot.cmake @@ -0,0 +1,18 @@ +# CMake toolchain of the portable Linux build's SDL3 (tools/linux_build.py): +# clang for 32-bit x86, against the system root of tools/linux_sysroot.py, +# given as -DHALO_SYSROOT=. The libraries SDL finds there it loads +# when it starts (dlopen), by the names it reads from them. +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR i686) +set(CMAKE_C_COMPILER_TARGET i686-linux-gnu) +set(CMAKE_SYSROOT "${HALO_SYSROOT}") +set(CMAKE_LIBRARY_ARCHITECTURE i386-linux-gnu) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +set(ENV{PKG_CONFIG_SYSROOT_DIR} "${HALO_SYSROOT}") +set(ENV{PKG_CONFIG_LIBDIR} "${HALO_SYSROOT}/usr/lib/i386-linux-gnu/pkgconfig:${HALO_SYSROOT}/usr/share/pkgconfig") +set(ENV{PKG_CONFIG_PATH} "") +# (and in the projects CMake tries compiling in) +list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES HALO_SYSROOT) diff --git a/tools/linux_sysroot.py b/tools/linux_sysroot.py new file mode 100644 index 00000000..d774a67d --- /dev/null +++ b/tools/linux_sysroot.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +"""The 32-bit x86 system root of the portable Linux build (configure.py +--portable), and the SDL3 source it builds libSDL3.so.0 from. + +An executable built against the build machine's glibc asks for the symbols +of that version (GLIBC_2.43 on an up-to-date Arch Linux) and starts on no +older system. The portable build is therefore compiled and linked against +Debian 11's i386 glibc 2.31, the baseline of the Steam Runtime 3 ("sniper"), +which SteamOS 3, the Steam Deck and every distribution since 2020 have, and +against the headers of the libraries SDL loads when it starts (X11, Wayland +and libdecor, PipeWire, PulseAudio, ALSA, D-Bus, udev), which it opens itself +(dlopen) and does not link to. + +Each package comes from the first of these that has it: archive.debian.org, +which keeps every Debian release, deb.debian.org, and last +snapshot.debian.org, whose URLs (the archive as it was at a moment when the +package was in it) do not change. SDL's source comes from its GitHub release, +or libsdl.org's. Each download is tried a few times, and checked against the +SHA-256 digests here. + + python tools/linux_sysroot.py + +makes /sysroot and /SDL3 once, and writes /stamp +(the list of what it holds) last; a folder whose stamp matches is left as it +is. Python alone: no dpkg, no root. + + python tools/linux_sysroot.py --check-sdl + +stops the build if SDL was built without one of the drivers it needs. +""" + +import hashlib +import http.client +import io +import os +import shutil +import sys +import tarfile +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import List, Tuple + +DEBIAN_MIRRORS = [ + "https://archive.debian.org/debian/", + "https://deb.debian.org/debian/", +] + +# (and the last resort: the archive as snapshot.debian.org has it at the time +# given with each package, when the package was in it) +SNAPSHOT = "https://snapshot.debian.org/archive/debian/{}/" + +# (path in the Debian archive, SHA-256, a snapshot.debian.org time that has +# it), all i386 from Debian 11 "bullseye" but PipeWire's (bullseye-backports': +# SDL needs 0.3.44 or later, bullseye has 0.3.19) and Wayland's and +# libdecor's (below). Only the C library's and libgcc's are linked to; the +# rest are SDL's headers, and the libraries it reads the names of. +PACKAGES: List[Tuple[str, str, str]] = [ + # the C library and the compiler's runtime (crtbegin.o, libgcc) + ("pool/main/g/glibc/libc6_2.31-13+deb11u11_i386.deb", + "8198abd817cbcd615ce3207fb924317b53cc0b522bfe57d689757de13fe00d66", "20240817T023924Z"), + ("pool/main/g/glibc/libc6-dev_2.31-13+deb11u11_i386.deb", + "7d10b944894d2b6ccab08ccf51aae6cef6c5f5773f36696e6d9f34c21e0365b4", "20240817T023924Z"), + ("pool/main/l/linux/linux-libc-dev_5.10.223-1_i386.deb", + "e57d6065a43e763e54a0a80afd66fa2371bcdc3b035f9f4463816e9c6fc26049", "20240815T030336Z"), + ("pool/main/g/gcc-10/libgcc-s1_10.2.1-6_i386.deb", + "0a52edec5d626f8d9acf1998248d276760aff3950f44a308d27d4a2a488fe18d", "20210111T024320Z"), + ("pool/main/g/gcc-10/libgcc-10-dev_10.2.1-6_i386.deb", + "64a544dcf5463225603cb025a05c7b79dea9876004c09d81af2bc31ffe0ccc1e", "20210111T024320Z"), + # SDL's X11 video driver + ("pool/main/libx/libx11/libx11-6_1.7.2-1+deb11u2_i386.deb", + "a0307da50139370781beac6fe6d3b173493af11d0a35c43c053c539d5d67969c", "20231012T085604Z"), + ("pool/main/libx/libx11/libx11-dev_1.7.2-1+deb11u2_i386.deb", + "6674c5517140f65addb055b2fcbab42e83c299555ecf516daf40758f938cf2d0", "20231012T085604Z"), + ("pool/main/x/xorgproto/x11proto-dev_2020.1-1_all.deb", + "d5568d587d9ad2664c34c14b0ac538ccb3c567e126ee5291085a8de704a565f5", "20200415T024424Z"), + # (xcb/xcb.h, for SDL's Vulkan headers) + ("pool/main/libx/libxcb/libxcb1-dev_1.14-3_i386.deb", + "b1d522ad25e5d4f208c73c2f20e6c918a869d11171835b7f792f487ff367d788", "20210201T212616Z"), + ("pool/main/libx/libxext/libxext6_1.3.3-1.1_i386.deb", + "b93e2d194c4d0aa5ca0c056883787b1fac35941f5ce9ec4fa6baedfba4cdb5a9", "20201218T204051Z"), + ("pool/main/libx/libxext/libxext-dev_1.3.3-1.1_i386.deb", + "0f2ca4c3c5e74e6ce6c99da60cbf1a66de1665a5703ddfb725a5855cc9ef7281", "20201218T204051Z"), + ("pool/main/libx/libxcursor/libxcursor1_1.2.0-2_i386.deb", + "593d4ac35460cc397d1fa96d251b62c26eb3f807b2e0bfe21dc33abef68811eb", "20190714T093329Z"), + ("pool/main/libx/libxcursor/libxcursor-dev_1.2.0-2_i386.deb", + "c3db4042a513952624d6f06485599f1c84d4095ebfaa753ddc243fa07f4bf81c", "20190714T093329Z"), + ("pool/main/libx/libxi/libxi6_1.7.10-1_i386.deb", + "004182ad1167319b5153fa736a41b646806586739db93e6a06db857ff8553505", "20200603T030403Z"), + ("pool/main/libx/libxi/libxi-dev_1.7.10-1_i386.deb", + "7a1b570d9bdf70d7e79e8b03a87c48cbb40a68ed672920f6170c67ec270edfc2", "20200603T030403Z"), + ("pool/main/libx/libxrandr/libxrandr2_1.5.1-1_i386.deb", + "b9a2d67526ee1a6cd4eda834534a21bd4a5a3c795a4945214553933560d21ce2", "20161207T032837Z"), + ("pool/main/libx/libxrandr/libxrandr-dev_1.5.1-1_i386.deb", + "bffbcd2b6eb5459aebada2adba44b32415cfdb8e27e2ee736b908e4c29f03360", "20161207T032837Z"), + ("pool/main/libx/libxfixes/libxfixes3_5.0.3-2_i386.deb", + "2250bbc75dcf0d73fc84b69150dcaefefc071ad2a28985a5a69abfeac2290f11", "20200414T204951Z"), + ("pool/main/libx/libxfixes/libxfixes-dev_5.0.3-2_i386.deb", + "3c81c546f61ed7af6a6a30ad3709b75dc74a983dff8f00dcce30eaff5a8c18e1", "20200414T204951Z"), + ("pool/main/libx/libxrender/libxrender1_0.9.10-1_i386.deb", + "0b71b3f530a1b039b64f74c0223b433bd73fe0164916b60b25f9f3f11a6b9bfb", "20161205T212728Z"), + ("pool/main/libx/libxrender/libxrender-dev_0.9.10-1_i386.deb", + "7a4a1cbc6e2004c520dc5882e0367c8ce5c7094e07922e792992d1d27a5829a4", "20161205T212728Z"), + ("pool/main/libx/libxss/libxss1_1.2.3-1_i386.deb", + "eb50d23f8e849a7f86a6313f3e48c5c1fbf6cf82ca34ea5ef32bc99ae980c64a", "20180906T152804Z"), + ("pool/main/libx/libxss/libxss-dev_1.2.3-1_i386.deb", + "e3f4268c8c5ca5a1e7a2b549470f9eca58cf9974b0176a929dd2185026895e7e", "20180906T152804Z"), + # SDL's OpenGL (GL/gl.h, GL/glx.h) + ("pool/main/libg/libglvnd/libgl-dev_1.3.2-1_i386.deb", + "1603066c9e658adbd71296b9a5db08d771c92900ac90035b9e210b9d7e2b7a3d", "20200729T210030Z"), + ("pool/main/libg/libglvnd/libglx-dev_1.3.2-1_i386.deb", + "97c1367f7ecf6c7e103a0eb85e119b3e22d9d882d52587a53733ff93daa94c1d", "20200729T210030Z"), + # SDL's Wayland video driver: Debian 12's Wayland (1.21), whose headers + # match the protocol code that a recent wayland-scanner writes + # (wl_proxy_marshal_flags, Wayland 1.20) + ("pool/main/w/wayland/libwayland-client0_1.21.0-1_i386.deb", + "1454b74db0b175bd4ef2b14ca6e66269e5d05e1bafa4fdaa6efa15424872061a", "20220709T031205Z"), + ("pool/main/w/wayland/libwayland-cursor0_1.21.0-1_i386.deb", + "5ea0a94c004d546fc41e426a47269b4251af8a0e691db57a011d72f43c62d3b7", "20220709T031205Z"), + ("pool/main/w/wayland/libwayland-egl1_1.21.0-1_i386.deb", + "0b73ffd5ddd6b3c6fbbd74dc0b5d8259614515e89e791fafe3472168f6b0149c", "20220709T031205Z"), + ("pool/main/w/wayland/libwayland-dev_1.21.0-1_i386.deb", + "c5051589b50623bc71a0a54e006571740b066ec61476b38808ef5f93fc0c95d3", "20220709T031205Z"), + # (libdecor, the window decorations GNOME's Wayland asks clients to draw: + # Debian 12's, as it needs Wayland 1.20's headers) + ("pool/main/libd/libdecor-0/libdecor-0-0_0.1.1-2_i386.deb", + "74b39bf96b3870899883c8e3b6a16bca75e8c5828614a7e1b9d061439ee6bb35", "20230211T151657Z"), + ("pool/main/libd/libdecor-0/libdecor-0-dev_0.1.1-2_i386.deb", + "31a307669c759629853ae06d7898a4e6355a9c155278b2a1a482d583ca3eac66", "20230211T151657Z"), + ("pool/main/libf/libffi/libffi-dev_3.3-6_i386.deb", + "a699fc6b1dbeff5f3633be8b3f8d31526b69d11bf435c5865e50ac249b98f94c", "20210221T031914Z"), + ("pool/main/libx/libxkbcommon/libxkbcommon0_1.0.3-2_i386.deb", + "b6d8929647c0c182f63d1c886b7fe8b23674383b2585401b2377ff61d2fe670e", "20201126T153155Z"), + ("pool/main/libx/libxkbcommon/libxkbcommon-dev_1.0.3-2_i386.deb", + "1a88ce18baafa1404452b451b0b3ce1a8eebf6204eedd498e38b34c5d6f42c44", "20201126T153155Z"), + ("pool/main/libg/libglvnd/libegl1_1.3.2-1_i386.deb", + "aba3025fe2becdcf9ed7d3822a52cd98ba6921fd8ccdd7825ec5a1f99b437fc8", "20200729T210030Z"), + ("pool/main/libg/libglvnd/libegl-dev_1.3.2-1_i386.deb", + "5073d41e5be4bffad4b19a5ebf93c2ad43b420d00417a778e76d84f831a47a66", "20200729T210030Z"), + # SDL's audio drivers + ("pool/main/a/alsa-lib/libasound2_1.2.4-1.1_i386.deb", + "1e7da3fa4ba22b7079bceb18e8ce3494d9dced9b1cffe5c6bb9aae8e64ecc9f7", "20210104T204018Z"), + ("pool/main/a/alsa-lib/libasound2-dev_1.2.4-1.1_i386.deb", + "7ea775aaa87bc4d391b800c5cbdcacdc298bb52d9a82dff31073860dc9737309", "20210104T204018Z"), + ("pool/main/p/pulseaudio/libpulse0_14.2-2_i386.deb", + "c84c1a3dbd17355cdf6eaf23c2313db18faf3581ca7ec95cf3a2177d1c70a577", "20210227T024018Z"), + ("pool/main/p/pulseaudio/libpulse-dev_14.2-2_i386.deb", + "c530edf490a67c2ece2bf54563872693d9f02ce6069e73b2b9b5372d965ef8e8", "20210227T024018Z"), + ("pool/main/p/pipewire/libpipewire-0.3-0_0.3.65-2~bpo11+1_i386.deb", + "1d032b638081704393bc847e2e1f7a00c6894fa262ed0272fb7282962e8915a9", "20230217T033139Z"), + ("pool/main/p/pipewire/libpipewire-0.3-dev_0.3.65-2~bpo11+1_i386.deb", + "5f67d3e5e435251a34e55a10b4834a794e6b48b83d6da39aab267b35ce9aba33", "20230217T033139Z"), + ("pool/main/p/pipewire/libspa-0.2-dev_0.3.65-2~bpo11+1_i386.deb", + "5128545520aa6444af8c3f61c72e5f8d39ced7556ce07dfdedcbec291c4a5048", "20230217T033139Z"), + # the desktop's D-Bus (the screen saver, the system's settings) and + # udev (gamepads plugged in and out) + ("pool/main/d/dbus/libdbus-1-3_1.12.28-0+deb11u1_i386.deb", + "7d8d30ca9cfa726f61e5b71dc6294ca3266147208d13cdec1fbefb217b0a9d25", "20230625T033524Z"), + ("pool/main/d/dbus/libdbus-1-dev_1.12.28-0+deb11u1_i386.deb", + "8ca893c7e096f70fd87745744a042507bd335bd6c13d98ba57bb6ecfa1170210", "20230625T033524Z"), + ("pool/main/s/systemd/libudev1_247.3-7+deb11u5_i386.deb", + "07891a18db3527472eaafb26e07ba26d3bfb926dbe2c01d3282e6b5fe61454cc", "20240616T023746Z"), + ("pool/main/s/systemd/libudev-dev_247.3-7+deb11u5_i386.deb", + "634a5cc9d7bc226d75ced0414750e32b07e82531e8b2b091e41708d42978db3d", "20240616T023746Z"), +] + +# SDL3's source release (the version of the other ports' SDL), from GitHub +# or libsdl.org +SDL_VERSION = "3.4.16" +SDL_URLS = [ + f"https://github.com/libsdl-org/SDL/releases/download/release-{SDL_VERSION}/SDL3-{SDL_VERSION}.tar.gz", + f"https://www.libsdl.org/release/SDL3-{SDL_VERSION}.tar.gz", +] +SDL_SHA256 = "7322236cd12090c3eb40b9728be4d49c76f66ad17d04369584d4ecad5cf77c68" + + +# what SDL must have been built with (its SDL_build_config.h): the drivers a +# desktop needs, each loaded when SDL starts rather than linked to (libdecor +# too: without its library's name, SDL would link to it, and the Steam Deck +# has no 32-bit libdecor) +SDL_REQUIRED = [ + "SDL_VIDEO_DRIVER_X11_DYNAMIC", "SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC", "HAVE_LIBDECOR_H", + "SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR", "SDL_VIDEO_OPENGL_GLX", + "SDL_VIDEO_OPENGL_EGL", "SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC", "SDL_AUDIO_DRIVER_PULSEAUDIO_DYNAMIC", + "SDL_AUDIO_DRIVER_ALSA_DYNAMIC", "SDL_UDEV_DYNAMIC", "HAVE_DBUS_DBUS_H", +] + + +def check_sdl(build_config: Path) -> None: + """stops (exit status 1) if SDL was built without one of SDL_REQUIRED, + as when the system root lacks a header SDL looks for""" + defined = {line.split()[1] for line in build_config.read_text().splitlines() + if line.startswith("#define ") and len(line.split()) > 2} + missing = [name for name in SDL_REQUIRED if name not in defined] + if missing: + sys.exit(f"SDL3 was built without {', '.join(missing)}: refer to {build_config.parent.parent.parent}" + "/CMakeFiles/CMakeConfigureLog.yaml") + + +def stamp_text() -> str: + """what a complete folder's stamp holds""" + return "".join(f"{path} {digest}\n" for path, digest, _ in PACKAGES) + f"{SDL_URLS[0]} {SDL_SHA256}\n" + + +# (each URL's tries, a moment apart, before the next URL) +TRIES = 3 +RETRY_SECONDS = 2 + + +def download(urls: List[str], digest: str) -> bytes: + """the file at the first of urls that answers, checked against digest""" + errors = [] + for url in urls: + for attempt in range(1, TRIES + 1): + if attempt > 1: + time.sleep(RETRY_SECONDS * (attempt - 1)) + try: + with urllib.request.urlopen(url, timeout=60) as response: + data = response.read() + # (a connection cut short is http.client.IncompleteRead, not an + # OSError) + except (OSError, http.client.HTTPException) as error: + errors.append(f"{url}: {error!r}") + # (the mirror does not have it: no use asking again) + if isinstance(error, urllib.error.HTTPError) and error.code == 404: + break + continue + actual = hashlib.sha256(data).hexdigest() + if actual == digest: + return data + errors.append(f"{url}: SHA-256 {actual}, expected {digest}") + raise RuntimeError("cannot download " + "; ".join(errors)) + + +def deb_data(deb: bytes) -> tarfile.TarFile: + """the data.tar.* member of a .deb (an ar archive)""" + if not deb.startswith(b"!\n"): + raise RuntimeError("not a Debian package") + offset = 8 + while offset + 60 <= len(deb): + header = deb[offset:offset + 60] + name = header[:16].decode().strip().rstrip("/") + size = int(header[48:58].decode()) + body = deb[offset + 60:offset + 60 + size] + if name.startswith("data.tar"): + return tarfile.open(fileobj=io.BytesIO(body), mode="r:*") + offset += 60 + size + (size & 1) + raise RuntimeError("a Debian package without data.tar") + + +# what the packages hold that the build does not need +UNUSED = (("usr", "share", "doc"), ("usr", "share", "man"), ("usr", "share", "locale"), ("usr", "share", "lintian"), + ("usr", "share", "bug"), ("usr", "bin"), ("sbin",), ("usr", "sbin")) + + +def extract(archive: tarfile.TarFile, destination: Path, strip: int = 0) -> None: + """an archive's files, folders and links into destination, nothing + outside it (no device files, no absolute or upward names)""" + for member in archive.getmembers(): + parts = [part for part in Path(member.name).parts if part not in (".", "/")][strip:] + if not parts or ".." in parts or any(tuple(parts[:len(unused)]) == unused for unused in UNUSED): + continue + target = destination.joinpath(*parts) + if member.isdir(): + target.mkdir(parents=True, exist_ok=True) + elif member.isfile() or member.islnk(): + target.parent.mkdir(parents=True, exist_ok=True) + if target.is_symlink(): + target.unlink() + with archive.extractfile(member) as source, open(target, "wb") as output: + shutil.copyfileobj(source, output) + os.chmod(target, member.mode & 0o755 | 0o600) + elif member.issym(): + target.parent.mkdir(parents=True, exist_ok=True) + if target.is_symlink() or target.exists(): + target.unlink() + link = member.linkname + # (an absolute link, /lib/i386-linux-gnu/libudev.so.1, points + # into the system root instead of the build machine's) + if link.startswith("/"): + link = os.path.relpath(destination / link.lstrip("/"), target.parent) + os.symlink(link, target) + + +def make(folder: Path) -> None: + stamp = folder / "stamp" + if stamp.is_file() and stamp.read_text() == stamp_text(): + return + for name in ("stamp", "sysroot", "SDL3"): + path = folder / name + if path.is_dir(): + shutil.rmtree(path) + elif path.exists(): + path.unlink() + sysroot = folder / "sysroot" + sysroot.mkdir(parents=True) + for path, digest, snapshot in PACKAGES: + print(f"Downloading {path.rsplit('/', 1)[-1]}", flush=True) + mirrors = DEBIAN_MIRRORS + [SNAPSHOT.format(snapshot)] + with deb_data(download([mirror + path for mirror in mirrors], digest)) as archive: + extract(archive, sysroot) + print(f"Downloading SDL3-{SDL_VERSION}.tar.gz", flush=True) + with tarfile.open(fileobj=io.BytesIO(download(SDL_URLS, SDL_SHA256)), mode="r:gz") as archive: + extract(archive, folder / "SDL3", strip=1) + stamp.write_text(stamp_text()) + + +if __name__ == "__main__": + if len(sys.argv) == 3 and sys.argv[1] == "--check-sdl": + check_sdl(Path(sys.argv[2])) + elif len(sys.argv) == 2: + make(Path(sys.argv[1])) + else: + sys.exit(__doc__) From 444877f06d034bea44fa8437f69e8ca98ec19cea Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 22:00:52 -0400 Subject: [PATCH 077/187] Linux: Steam's on-screen keyboard comes up for the menus' text fields The menus' text fields (Server Setup's name and password, the password a listed game asks for) take only a keyboard's keys: on a Steam Deck in Game Mode, with no keyboard, there was no way to type into them but to bring up Steam's keyboard by hand (Steam + X). The game's own on-screen keyboard (profile and gametype names) works with the controller and is unchanged. While a field is typed into, the platform layer now runs SDL's text input where the system has an on-screen keyboard that text input brings up: Steam's, which Steam asks for with SDL_ENABLE_STEAM_SCREEN_KEYBOARD in Big Picture and Game Mode (SDL 3.4 opens steam://open/keyboard, and steam://close/keyboard when the field ends), or a Wayland touch screen with no keyboard. The field is single-line: Enter on Steam's keyboard ends it, as a keyboard's Enter does, and closes it. A password's field asks for a hidden password's keyboard (SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_HIDDEN; Steam's is the same). Elsewhere text input stays off, so that no input method takes the keys the field reads. SDL is called from the window's thread (the event pump), whichever thread the menus run on. Steam does not say when its keyboard goes (its Enter, or closed by hand), so SDL holds it to be up while text input runs. Each field begun brings it up again, even when the last field ended in the same frame, as the password screen's does after a wrong password (its field ends and begins again): text input still running is stopped, which closes Steam's keyboard, and started half a second later, which opens it (Steam takes each as a URL, each through a process of its own, which could otherwise arrive the other way round). A keyboard closed by hand comes back when the field is begun again. On a Steam Deck (SteamOS 3.8.16, Game Mode, the game started from Steam), the password screen (debug.menu_open) brings Steam's keyboard up at the bottom of the screen, below the field; keys pressed on it type into the field, and its Enter ends the field (the game tries to join) and the keyboard goes (Steam's log: open/keyboard ... Mode=0, then close/keyboard). --- port/linux/game/menu_functions.c | 21 +++++---- port/linux/src/sdl_platform.c | 75 ++++++++++++++++++++++++++++++++ port/linux/src/sdl_platform.h | 4 ++ port/linux/src/xinput_sdl.c | 8 +++- 4 files changed, 99 insertions(+), 9 deletions(-) diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 7b98f071..01f5fb10 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -2203,7 +2203,7 @@ void p2p_set_hosting_allowed(int allowed); int p2p_peer_address(unsigned char const *identifier, unsigned long *address); int platform_clipboard_get(char *text, int size); void platform_clipboard_set(char const *text); -void platform_text_field(int typing); +void platform_text_field(int typing, int password); int config_boolean(char const *name); void ui_widget_port_post_button(short controller_index, short button_index); @@ -2271,8 +2271,8 @@ static boolean text_field_editing(struct widget_instance *row) return text_field.row && (!row || text_field.row == row); } -static void text_field_begin(struct widget_instance *row, char const *text, short maximum, - void (*done)(char const *text)) +static void text_field_open(struct widget_instance *row, char const *text, short maximum, + void (*done)(char const *text), boolean masked) { struct key_stroke key; @@ -2281,26 +2281,31 @@ static void text_field_begin(struct widget_instance *row, char const *text, shor snprintf(text_field.before, sizeof(text_field.before), "%s", text); text_field.maximum = (short)MIN(maximum, TEXT_FIELD_LENGTH - 1); text_field.done = done; - text_field.masked = FALSE; + text_field.masked = masked; text_field_shown_time = system_milliseconds(); while (input_get_key(&key)) ; - platform_text_field(TRUE); + platform_text_field(TRUE, masked); +} + +static void text_field_begin(struct widget_instance *row, char const *text, short maximum, + void (*done)(char const *text)) +{ + text_field_open(row, text, maximum, done, FALSE); } /* a password's field: as text_field_begin, its text shown as stars */ static void text_field_begin_masked(struct widget_instance *row, char const *text, short maximum, void (*done)(char const *text)) { - text_field_begin(row, text, maximum, done); - text_field.masked = TRUE; + text_field_open(row, text, maximum, done, TRUE); } static void text_field_end(boolean keep) { void (*done)(char const *text) = text_field.done; - platform_text_field(FALSE); + platform_text_field(FALSE, FALSE); text_field.row = NULL; text_field.done = NULL; if (!keep) diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 3bb05134..e10030f4 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -95,6 +95,7 @@ void dsound_sdl_output_device_check(void); /* updater.c's: the desktop self-updater */ void updater_start(void); void updater_poll(SDL_Window *window); +static void screen_keyboard_update(void); #endif BOOL platform_sdl_initialize(void) @@ -1330,6 +1331,7 @@ void platform_pump_events(void) updater_poll(platform_window); /* (Settings > Audio's output device, as it changes: dsound_sdl.c) */ dsound_sdl_output_device_check(); + screen_keyboard_update(); #endif pthread_mutex_lock(&input_lock); #ifndef HALO_ANDROID @@ -1634,6 +1636,79 @@ void platform_video_window_size(int *width, int *height) SDL_GetWindowSize(platform_window, width, height); } +/* ---------- the system's on-screen keyboard */ + +/* A menu's text field is typed into (platform_text_field, xinput_sdl.c). +Where the system has an on-screen keyboard that text input brings up (Steam's, +in Big Picture and in the Steam Deck's Game Mode, which ask for it with +SDL_ENABLE_STEAM_SCREEN_KEYBOARD; a Wayland touch screen without a keyboard), +SDL's text input runs while the field is typed into: the keyboard comes up +with the field and goes with it, and what it types arrives as keys. Elsewhere +text input stays off, as before, so that no input method takes the keys the +field reads. */ +static SDL_AtomicInt screen_keyboard_wanted; +/* (each field begun, which brings the keyboard up again: Steam does not say +when its keyboard goes, by its own Enter or closed by hand, so SDL holds it +to be up still; after a field ended and another begun in the same frame, as +the password screen's is after a wrong password, it would not come back) */ +static SDL_AtomicInt screen_keyboard_requests; + +void platform_screen_keyboard(BOOL show, BOOL password) +{ + SDL_SetAtomicInt(&screen_keyboard_wanted, !show ? 0 : password ? 2 : 1); + if (show) + SDL_AddAtomicInt(&screen_keyboard_requests, 1); +} + +/* (on the window's thread, as SDL asks: platform_pump_events) */ +static void screen_keyboard_update(void) +{ + /* (a keyboard shown again is closed first, as SDL opens none that it + holds to be up, and opened a moment later: Steam takes each as a URL, + steam://close/keyboard then steam://open/keyboard, which must not + arrive the other way round) */ + enum { REOPEN_DELAY_MS = 500 }; + static int requests_handled; + static Uint64 open_time; + int requests = SDL_GetAtomicInt(&screen_keyboard_requests); + int wanted = SDL_GetAtomicInt(&screen_keyboard_wanted); + + if (!wanted) + { + open_time = 0; + if (SDL_TextInputActive(platform_window)) + SDL_StopTextInput(platform_window); + return; + } + if (requests != requests_handled) + { + requests_handled = requests; + if (!SDL_HasScreenKeyboardSupport()) + return; + open_time = SDL_GetTicks(); + if (SDL_TextInputActive(platform_window)) + { + SDL_StopTextInput(platform_window); + open_time += REOPEN_DELAY_MS; + } + } + if (open_time && SDL_GetTicks() >= open_time) + { + /* one line: the keyboard's Enter ends the field (and Steam's + keyboard goes with it); a password's, for the keyboards that hide + what is typed into one */ + SDL_PropertiesID properties = SDL_CreateProperties(); + + open_time = 0; + platform_log("text field: showing the on-screen keyboard"); + SDL_SetBooleanProperty(properties, SDL_PROP_TEXTINPUT_MULTILINE_BOOLEAN, false); + SDL_SetNumberProperty(properties, SDL_PROP_TEXTINPUT_TYPE_NUMBER, + wanted == 2 ? SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_HIDDEN : SDL_TEXTINPUT_TYPE_TEXT); + SDL_StartTextInputWithProperties(platform_window, properties); + SDL_DestroyProperties(properties); + } +} + #endif void platform_input_read(struct platform_input_state *state, BOOL consume_motion) { diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index 09d8f31e..9164158d 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -111,6 +111,10 @@ BOOL platform_ui_pointer_read(struct platform_ui_pointer *pointer); while a right click has freed it, with what it did since the last call */ BOOL platform_scoreboard_pointer(BOOL offered, struct platform_ui_pointer *pointer); void platform_video_window_size(int *width, int *height); +/* a menu's text field typed into or not (a password's or not): the system's +on-screen keyboard up while it is, where there is one that text input shows +(Steam's); each field begun brings it up again */ +void platform_screen_keyboard(BOOL show, BOOL password); #endif BOOL platform_next_keystroke(struct platform_keystroke *keystroke); /* the multiplayer scoreboard (game_engine.c) open or not: while it is, the diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index 2e42ebb7..b3317f72 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -231,10 +231,16 @@ void platform_text_typing(int typing) text_typing_update(); } -void platform_text_field(int typing) +void platform_text_field(int typing, int password) { text_typing_field = typing != 0; text_typing_update(); +#ifndef HALO_ANDROID + /* (with no keyboard: Steam's on-screen one, sdl_platform.c) */ + platform_screen_keyboard(text_typing_field, typing && password); +#else + (void)password; +#endif } static void typing_gamepad(const struct platform_input_state *input, XINPUT_GAMEPAD *pad) From 063c60381a0ff5a4b3ab8fa7d9618aa490818880 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 22:02:13 -0400 Subject: [PATCH 078/187] Linux README: playing on a Steam Deck The README said only how to add the game to Steam. A "Steam Deck" section now says what to set up in Desktop Mode and what to expect in Game Mode: the screen (borderless, the 3D view and HUD at the panel's 16:10 resolution), the controls under Steam Input's default template (the right trackpad as the right stick; the back buttons unbound), text entry (the game's own keyboard for names, Steam's for the menus' text fields), the frame rate (one frame each refresh, following Quick Access's refresh rate and frame limit) and sleep (the game's clocks stop with the Deck; a network game does not wait). It suggests no lower display.resolution to save power: on the Deck (b30 with a bot, 60 Hz, 90 s) 960x600 measured 7.8 W from the battery against 8.1 to 8.8 W at the native 1280x800, about the difference between two runs at the same settings, with the GPU 22% busy against 23 to 26%: the game is not limited by its pixels there, and the picture is softer. Checked on a Steam Deck LCD (SteamOS 3.8.16, Game Mode): the game fills the 1280x800 panel ("screen: 768x480 drawn at 1280x800"); Steam selects controller_neptune_gamepad_fps ("Gamepad With Joystick Trackpad") for it; with display_framerate, it shows 60 frames a second at 60 Hz and 40 at 40 Hz (gamescopectl debug_set_fps_limit 40); after the process was stopped for 8 seconds (SIGSTOP), its sound and frames came back at once. --- port/linux/README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/port/linux/README.md b/port/linux/README.md index fe1a3cf1..a923b86f 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -93,7 +93,8 @@ Enter `build/linux/halo`. To play on a Steam Deck, unpack `halo-linux-release.zip` into a folder (in Desktop Mode), and add `halo` to Steam as a non-Steam game ("Add a Game" in -the Games menu of Steam). The game then starts in Game Mode as well. +the Games menu of Steam). The game then starts in Game Mode as well. Refer +to "Steam Deck". The game data is the folder that contains `maps/`, from an Xbox disc image of any version of the game. The game looks for this folder in this @@ -116,6 +117,38 @@ The game writes the copy to `maps.partial`. When the copy is complete, the game changes the name to `maps`. If the copy stops before it is complete, the game asks for the disc image again at the next start. +### Steam Deck + +Do the set-up in Desktop Mode: unpack the release, put `maps/` next to +`halo` (or start the game one time to copy it from a disc image), and add +`halo` to Steam. Then start the game from the library in Game Mode. + +- Screen: the game fills the screen without borders (`display.mode` empty, + thus borderless). The 3D view and the HUD have the shape of the screen + (16:10) and are drawn at its resolution; the menus are at the center. +- Controls: Steam Input gives the game a virtual controller, which the game + operates as the controller of the Xbox. With the template that Steam selects + ("Gamepad With Joystick Trackpad"), the buttons, sticks and triggers have + the functions of the same controls on an Xbox controller, the right + trackpad operates as the right stick, and the back buttons (L4, L5, R4, + R5) do nothing until you assign them in the controller settings of the + game in Steam. With a template that makes a trackpad a mouse, the mouse + aims in the game and moves the pointer in the menus. +- Text: names of profiles and gametypes use the keyboard of the game, which + the controller operates. The text fields of the menus (the name and the + password in Server Setup, and the password of a game in the server + browser) open the keyboard of Steam. Type the text, then select Enter on + that keyboard. +- Frame rate: the game shows one frame for each refresh of the display. It + follows the refresh rate and the frame limit of Quick Access > + Performance (40 to 60 Hz on the LCD model, up to 90 Hz on the OLED model). + The world is calculated at 30 Hz at all rates. Keep `display.vsync = + true`. Refer to "Frame rate". +- Sleep: the clocks of the game do not count the time that the Deck sleeps, + so the game continues from where it stopped. A network game does not + wait: the host drops a machine that it has not heard from for 15 seconds, + and while a Deck that hosts sleeps, the other players have no host. + ## Files and folders | Xbox drive | Folder | From 988786d2c233ba8c7566c35e48fc2bfab7f23842 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Thu, 8 Oct 2026 22:01:05 -0400 Subject: [PATCH 079/187] Linux: the log names each gamepad the game opens Nothing said which controllers the game had found, so a controller that did nothing (Steam Input's virtual pad not seen, a pad SDL does not know) could not be told from one the game ignored. The log now has a line for each gamepad SDL opens, with SDL's name for it. Android's guest reaches SDL only through host_imports.list, which has no SDL_GetGamepadName, so it opens its pads as before. On a Steam Deck in Game Mode, under Steam Input's default template, the game sees one pad, Steam's virtual one, which SDL names for the controller behind it ("gamepad: Steam Deck Controller"); Steam keeps SDL off the Deck's own controller (SDL_GAMECONTROLLER_IGNORE_DEVICES), so its buttons arrive once. --- port/linux/src/sdl_platform.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 3bb05134..9b865c78 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -1536,7 +1536,24 @@ void platform_pump_events(void) #endif break; case SDL_EVENT_GAMEPAD_ADDED: +#ifdef HALO_ANDROID + /* (the guest reaches SDL only through host_imports.list, which + has no SDL_GetGamepadName) */ SDL_OpenGamepad(event.gdevice.which); +#else + { + SDL_Gamepad *gamepad = SDL_OpenGamepad(event.gdevice.which); + + /* (which pads the game drives: under Steam Input, Steam's + virtual ones, named for the controllers behind them) */ + if (gamepad) + { + const char *name = SDL_GetGamepadName(gamepad); + + platform_log("gamepad: %s", name ? name : "(unnamed)"); + } + } +#endif break; default: break; From a7c8eac379b46959c23a004e34757b6438a71de2 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:11:42 +1300 Subject: [PATCH 080/187] Lucide speaker icons for voice chat Voice chat's speaker and muted speaker are Lucide's volume-2 and volume-x (1.53.0, ISC: port/assets/icons/lucide, credited in AUTHORS.md, its licence shipped beside the game), drawn white into the menus' bitmap voice/speaker by tools/ce_menus.py and tinted, in place of the speaker made of rectangles. In the lobby, on the scoreboard and in the list of who talks, the icon is centred on the middle of the text's capitals (draw_unicode_string_capital_middle). --- AUTHORS.md | 9 +++ port/assets/icons/lucide/LICENSE | 43 +++++++++++ port/assets/icons/lucide/README.md | 11 +++ port/assets/icons/lucide/volume-2.svg | 15 ++++ port/assets/icons/lucide/volume-x.svg | 15 ++++ port/assets/menus/NON_HANDDRAWN.md | 2 +- port/assets/menus/ce/bitmaps.xml | 4 + .../assets/menus/ce/port/voice/speaker__0.png | Bin 0 -> 1380 bytes .../assets/menus/ce/port/voice/speaker__1.png | Bin 0 -> 930 bytes port/assets/menus/menus.json | 2 + .../menus/port_svg/voice/speaker__0.svg | 15 ++++ .../menus/port_svg/voice/speaker__1.svg | 15 ++++ port/linux/game/menu_functions.c | 5 +- port/linux/game/network_voice.c | 69 +++++------------- source/game/game_engine.c | 34 ++++++--- source/text/draw_string.c | 45 ++++++++++++ source/text/draw_string.h | 5 ++ tools/ce_menus.py | 6 ++ tools/ci_build.py | 3 + 19 files changed, 235 insertions(+), 63 deletions(-) create mode 100644 AUTHORS.md create mode 100644 port/assets/icons/lucide/LICENSE create mode 100644 port/assets/icons/lucide/README.md create mode 100644 port/assets/icons/lucide/volume-2.svg create mode 100644 port/assets/icons/lucide/volume-x.svg create mode 100644 port/assets/menus/ce/port/voice/speaker__0.png create mode 100644 port/assets/menus/ce/port/voice/speaker__1.png create mode 100644 port/assets/menus/port_svg/voice/speaker__0.svg create mode 100644 port/assets/menus/port_svg/voice/speaker__1.svg diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 00000000..2e71d80b --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,9 @@ +# Authors + +## Third-party art + +- **Voice chat's speaker icons** (who talks, and who is muted, in the lobby, + on the scoreboard and in the game): `volume-2` and `volume-x` from + [Lucide](https://lucide.dev) 1.53.0, copyright (c) 2026 Lucide Icons and + Contributors, under the ISC licence + (`port/assets/icons/lucide/LICENSE`). diff --git a/port/assets/icons/lucide/LICENSE b/port/assets/icons/lucide/LICENSE new file mode 100644 index 00000000..718bb3f0 --- /dev/null +++ b/port/assets/icons/lucide/LICENSE @@ -0,0 +1,43 @@ +ISC License + +Copyright (c) 2026 Lucide Icons and Contributors + +Permission to use, copy, modify, and/or 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. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +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/port/assets/icons/lucide/README.md b/port/assets/icons/lucide/README.md new file mode 100644 index 00000000..534da090 --- /dev/null +++ b/port/assets/icons/lucide/README.md @@ -0,0 +1,11 @@ +# Lucide icons + +Two icons of [Lucide](https://lucide.dev) 1.53.0 (ISC licence: `LICENSE`), +as `icons/` has them at the `1.53.0` tag of +https://github.com/lucide-icons/lucide: + +- `volume-2.svg`: voice chat's speaker, of a player who talks; +- `volume-x.svg`: ... of a player muted. + +`tools/ce_menus.py` draws them, white, as the menus' bitmap `voice/speaker` +(port/linux/game/network_voice.c tints it). Credited in `AUTHORS.md`. diff --git a/port/assets/icons/lucide/volume-2.svg b/port/assets/icons/lucide/volume-2.svg new file mode 100644 index 00000000..519e6cbd --- /dev/null +++ b/port/assets/icons/lucide/volume-2.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/port/assets/icons/lucide/volume-x.svg b/port/assets/icons/lucide/volume-x.svg new file mode 100644 index 00000000..3e260094 --- /dev/null +++ b/port/assets/icons/lucide/volume-x.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/port/assets/menus/NON_HANDDRAWN.md b/port/assets/menus/NON_HANDDRAWN.md index 167d8e6f..2177f58a 100644 --- a/port/assets/menus/NON_HANDDRAWN.md +++ b/port/assets/menus/NON_HANDDRAWN.md @@ -62,7 +62,7 @@ until it is redrawn. | `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | | `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | -13 of 159 frames are placeholders. +13 of 161 frames are placeholders. The PC version's `ui\gamespy` and `ui\ticker` fonts are drawn with `ui\small_ui`, which the Xbox's map has. diff --git a/port/assets/menus/ce/bitmaps.xml b/port/assets/menus/ce/bitmaps.xml index cef95f52..1fd31ec1 100644 --- a/port/assets/menus/ce/bitmaps.xml +++ b/port/assets/menus/ce/bitmaps.xml @@ -350,6 +350,10 @@ + + + + diff --git a/port/assets/menus/ce/port/voice/speaker__0.png b/port/assets/menus/ce/port/voice/speaker__0.png new file mode 100644 index 0000000000000000000000000000000000000000..6834009598d7b9e516c1b3ef8805e73fc527cd98 GIT binary patch literal 1380 zcmV-q1)KVbP)!cA4OQFlcUtX9N@V#SSQ zm5qX$qG2JO+W4YWN%FaP6Vn&+-kF(qf9AfN58mp`{ob7O&CI>? zbI%173WY+UP$(1%g+ifFC=`kuL2?<%9+DrE93eSL@*9$SNv_cdr5pV9B!^fXlO!M2 zNTn0}O(e%CV}|4|jaWXxpT&8SS83#u3H}z6=je~;HG0{%{0s4Q(fv1hw|oEx0IUe4 zC>die&9J@~1Gp1FJAfYm95BXA6}9De1`azF$hfDTnD_TbNv|`zg$rh5Q zt5o4+q2?x@E&p630z4#C<6Dv|L%5r=ZutP-a~$(00NfEtyJ;8fHvo9s-Xo$OcE&wg zewV#2h91!ESnrn`D))^u}$!i)S_%R7!>+%4}X$M~h z@(#QqxzqD$r?U!IGy>TAJmKimJxTrHI+FWHerK10lSb9x$0&fU(_0RH9VgjbZ!7Lj zDdvP9`dV4gNxp#^Zq^ zJni83b|1p;QwaRDeR8-mz`>*i=&wOHNYZ%nt+c(U7GC!uEF~25-$))L`7Fteb&Rt~ z3os#2J`%vWSKu6XUZ{(3;*+%iVV84-M>!R-gmb01UPXhJl0ZTWd z0_RS>2b6okc>vPx)fSR19-IX5nNX}l1(?Hr0HqrCB>=X1at;ahb`@Z*#g+hkC=|QJ zllGWkU#kLC;uFEXDPaNDr~sAtQn0V`IOFg*>VuLB;D^zKdw>eydoPwA z@6+a6>;kN+vxm&ba=|{S0#riUpBVGxlO}E@6`&Gbf_)@mK~;cxxK*$p_T*bD*iWed zvv)6Wbh46Tap{_@W-p_2PTR!SCCB2se_P(&On)JXbip2RCfz z2wudm=nEQ7b$ipn*IgmROGyssD@rx%wawn!YTf<;WJPfgqkApg|God5=)sf%+JVRIeBQQ?z!=e{X#j5%am z)kMJMA3>2@PtXrINZNGFA`e2`VO_LRZw4`R`r-j!r9B1zPxzqhU;A)(E_4l0nKWEO1O2!ytj4{R-V~jDz1OylY=7AT$8au22 z_kq(+sNC?+#$g}C58$SgDkuC&V4Y-KcVfkce-Zdba()7*oLu!Se=m2PUiB@1FK?V+ zn3liGhEvQQ{#4HJ0T}cgT7kl!khCCaxakeC6{6)!dM4>af^YA*RutUwfz^c7WgVFQ z-%C)f!;jx*0inXTfWYBfK;ZB#Abao;Fq>kWzaEc{)HDHy5`;M>Xj9eum6|WWYv498p5vD) zr3A%ct>!I}d#-oT`o?N133aQvB?@R4*iZFaTxwgu{_A!M8YU-M+3Td~1I!M?k`{YO zh+kV&j+MP83s}k{A!YbWE3=Y66r)%aTbtY!e%_aFDLva z3dkU#yzrYUpkN8v!*8;Hf+XY%zv%*UO2{9+1^gqSlOp`htl|5UaCf4L-s2~r%l*=f z^ZRbAv4CpAFHS;(hoO1H&m_Tpfc~CWcq-|5L9?h&l1gYf{8Zs@N!ln{LX#)sQctX9 znZUCEPe<2MKs(PT+>{c$Cfwz^oxIZvDI|DJI0JomZO5<8D#u|$uL)-iKc$3JYr;is z%=!u}19yP)1Jnrz-QK&4!`1+?sf=#GTQffD3~>EUseIF%JCp_Vj9K zb?H>zNqSh~J^SuB^W+p91{QrpDXfI54(UxdSwNIP#Hge>?Ht?~k#wxe4?O)sj-n`X z&FUW9j1OvumymYqcFF>3Ccy#%OR#{z5-cFF1Pdr$!c@x9kp-0WBowg+#{w#P5-gym z5(fV7iUwVhjHe$iOIpi#iH{bLU&6AatID>p{oHjVTsis$j5xV)66Ty(ITFqR?|TIA zf8tYtgrmR>;N_ktVg+~roN+?N7-Nhv#u#IaF=i0?1Bzj|k?Fc>AOHXW07*qoM6N<$ Ef;88+zyJUM literal 0 HcmV?d00001 diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json index cb46b1d6..ad0c56a7 100644 --- a/port/assets/menus/menus.json +++ b/port/assets/menus/menus.json @@ -67,6 +67,8 @@ "ce/port/pause/pausebox_left__1.png", "ce/port/pause/pausebox_right__0.png", "ce/port/pause/pausebox_right__1.png", + "ce/port/voice/speaker__0.png", + "ce/port/voice/speaker__1.png", "ce/shell/bitmaps/alert_bkd__0.png", "ce/shell/bitmaps/arrow_sm_down__0.png", "ce/shell/bitmaps/arrow_sm_down__1.png", diff --git a/port/assets/menus/port_svg/voice/speaker__0.svg b/port/assets/menus/port_svg/voice/speaker__0.svg new file mode 100644 index 00000000..030a3541 --- /dev/null +++ b/port/assets/menus/port_svg/voice/speaker__0.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/port/assets/menus/port_svg/voice/speaker__1.svg b/port/assets/menus/port_svg/voice/speaker__1.svg new file mode 100644 index 00000000..80dfa19c --- /dev/null +++ b/port/assets/menus/port_svg/voice/speaker__1.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 7b98f071..840f13b2 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -4237,7 +4237,8 @@ void menu_functions_text_box_drawn(struct widget_instance *widget, rectangle2d c size = (short)MIN(14, bounds->y1 - bounds->y0); icon.x1 = (short)(bounds->x1 - 4); icon.y0 = (short)((bounds->y0 + bounds->y1 - size) / 2); - /* (the name's ink: the icon just left of it) */ + /* (the name's ink: the icon just left of it, centred on its + capitals) */ if (widget->parameters.text_box.text && widget->parameters.text_box.text[0]) { rectangle2d text; @@ -4246,7 +4247,7 @@ void menu_functions_text_box_drawn(struct widget_instance *widget, rectangle2d c draw_unicode_string_compute_bounds(bounds, widget->parameters.text_box.text, &text, &cursor); if (text.x0 - size - 6 >= bounds->x0) icon.x1 = (short)(text.x0 - 6); - icon.y0 = (short)((text.y0 + text.y1 - size) / 2); + icon.y0 = (short)(draw_unicode_string_capital_middle(bounds, widget->parameters.text_box.text) - size / 2); } icon.x0 = (short)(icon.x1 - size); icon.y1 = (short)(icon.y0 + size); diff --git a/port/linux/game/network_voice.c b/port/linux/game/network_voice.c index d3d8f11b..c606e6cb 100644 --- a/port/linux/game/network_voice.c +++ b/port/linux/game/network_voice.c @@ -37,6 +37,10 @@ game_engine.c), which it does not tell anyone. #include "cseries/errors.h" #include "memory/data.h" #include "math/integer_math.h" +#include "bitmaps/bitmap_group.h" +#include "bitmaps/bitmap_group_lookup.h" +#include "interface/ui_widget.h" +#include "tag_files/tag_groups.h" #include "camera/observer.h" #include "game/game.h" #include "game/game_engine.h" @@ -64,8 +68,6 @@ boolean network_distributed_server_send_to_machine(long machine_index, void *mes boolean network_distributed_server_send_to_machine_reliably(long machine_index, void *message, word size); short network_distributed_server_machines(long *machine_indices, short maximum); int halo_push_to_talk_held(void); -/* cinematics.c's */ -void draw_quad(rectangle2d *rectangle, pixel32 color); void posix_random_bytes(void *buffer, unsigned int size); unsigned long system_milliseconds(void); @@ -99,6 +101,9 @@ enum VOICE_MAXIMUM_KBPS = 64, }; +/* the speaker's bitmap (network_voice_draw_icon): the menus' voice/speaker, +as their bitmaps are named (menu_tags.c) */ +#define VOICE_ICON_BITMAP "pc\\voice\\speaker" #define VOICE_OPEN_MIC_LEVEL 0.01f /* proximity: full volume this near, quieter further (world units) */ #define VOICE_NEAR_DISTANCE 3.0f @@ -988,60 +993,24 @@ boolean network_voice_available( return voice_session && voice_settings_heard; } -/* a speaker drawn in the rectangle (its height the icon's size, at its left; -muted: grey, struck through in red), fading with alpha */ +/* a speaker drawn in the rectangle (a square of its height, at its left; +muted: struck through, in red), fading with alpha: Lucide's icons +(port/assets/icons/lucide), drawn white as the menus' bitmap voice/speaker, +a frame each (tools/ce_menus.py), and tinted; none without the menus' +bitmaps */ void network_voice_draw_icon( rectangle2d const *bounds, boolean muted, real alpha) { - short size = (short)(bounds->y1 - bounds->y0); - short x = bounds->x0; - short y = bounds->y0; + long bitmap_index = tag_loaded(BITMAP_GROUP_TAG, VOICE_ICON_BITMAP); + struct bitmap_data *bitmap = bitmap_index != NONE ? bitmap_group_try_and_get_bitmap(bitmap_index, muted ? 1 : 0) : + NULL; pixel32 a = (pixel32)(PIN(alpha, 0.0f, 1.0f) * 255.0f + 0.5f) << 24; - pixel32 color = a | (muted ? 0x00909090 : 0x0050E050); - rectangle2d part; - short slice; + rectangle2d square = *bounds; - if (size < 4) + if (!bitmap || bounds->y1 - bounds->y0 < 4) return; - /* (the body, the cone in slices widening, and two waves) */ - part.x0 = x; - part.x1 = (short)(x + size * 3 / 10); - part.y0 = (short)(y + size * 35 / 100); - part.y1 = (short)(y + size * 65 / 100); - draw_quad(&part, color); - for (slice = 0; slice < 3; slice++) - { - part.x0 = (short)(x + size * (30 + slice * 8) / 100); - part.x1 = (short)(x + size * (38 + slice * 8) / 100); - part.y0 = (short)(y + size * (30 - slice * 10) / 100); - part.y1 = (short)(y + size * (70 + slice * 10) / 100); - draw_quad(&part, color); - } - if (!muted) - { - part.x0 = (short)(x + size * 66 / 100); - part.x1 = (short)(part.x0 + MAX(2, size / 8)); - part.y0 = (short)(y + size * 30 / 100); - part.y1 = (short)(y + size * 70 / 100); - draw_quad(&part, color); - part.x0 = (short)(x + size * 84 / 100); - part.x1 = (short)(part.x0 + MAX(2, size / 8)); - part.y0 = (short)(y + size * 15 / 100); - part.y1 = (short)(y + size * 85 / 100); - draw_quad(&part, color); - return; - } - /* (struck through: a slash of small squares) */ - for (slice = 0; slice < 6; slice++) - { - short step = (short)(size * slice / 6); - - part.x0 = (short)(x + step); - part.x1 = (short)(part.x0 + MAX(2, size / 6)); - part.y0 = (short)(y + step); - part.y1 = (short)(part.y0 + MAX(2, size / 6)); - draw_quad(&part, a | 0x00E03030); - } + square.x1 = (short)(square.x0 + (square.y1 - square.y0)); + draw_bitmap_in_rect(bitmap, &square, NULL, NULL, a | (muted ? 0x00E05A5A : 0x0050E050), NULL, TRUE); } diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 7227a76e..e448c91c 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -2351,12 +2351,25 @@ static void game_engine_rasterize_scoreboard( network_voice_machine_muted(player->network_player_data.machine_index))) { rectangle2d icon; + rectangle2d text; short row_left = (short)(left + column * (SCOREBOARD_COLUMN_WIDTH + SCOREBOARD_COLUMN_GAP)); + short size; + short middle; + /* (centred on the row's capitals: the middle measured as the row + is laid out, then scaled as it is drawn, as scoreboard_rectangle + has it) */ + text.x0 = row_left; + text.x1 = (short)(row_left + SCOREBOARD_COLUMN_WIDTH); + text.y0 = (short)(top + (2 + row) * line_height); + text.y1 = (short)(text.y0 + line_height); + draw_string_set_draw_mode(font_index, NONE, 0, 0, row_color); + middle = (short)(top + (draw_unicode_string_capital_middle(&text, L"H") - top) * SCOREBOARD_SCALE); scoreboard_rectangle(&icon, bounds.x0, top, line_height, (short)(row_left + SCOREBOARD_PLACE_WIDTH - 24), 20, 2 + row, 1); - icon.y0 = (short)(icon.y0 + (icon.y1 - icon.y0) / 6); - icon.y1 = (short)(icon.y1 - (icon.y1 - icon.y0) / 6); + size = (short)((icon.y1 - icon.y0) * 2 / 3); + icon.y0 = (short)(middle - size / 2); + icon.y1 = (short)(icon.y0 + size); network_voice_draw_icon(&icon, network_voice_machine_muted(player->network_player_data.machine_index), alpha); } @@ -3905,18 +3918,19 @@ static void game_engine_rasterize_voice_speakers( machines_listed[listed_count++] = machine_index; if (!network_voice_machine_speaking(machine_index)) continue; - icon.x0 = (short)(bounds.x0 + 16); - icon.y0 = (short)(bounds.y0 + (bounds.y1 - bounds.y0) * 55 / 100 + row * line_height); - icon.x1 = (short)(icon.x0 + line_height); - icon.y1 = (short)(icon.y0 + line_height - 2); - network_voice_draw_icon(&icon, FALSE, 1.0f); - text = icon; - text.x0 = (short)(icon.x1 + 4); + /* (the name after the speaker, which is centred on its capitals) */ + text.x0 = (short)(bounds.x0 + 16 + line_height + 4); text.x1 = bounds.x1; - text.y1 = (short)(icon.y0 + line_height); + text.y0 = (short)(bounds.y0 + (bounds.y1 - bounds.y0) * 55 / 100 + row * line_height); + text.y1 = (short)(text.y0 + line_height); color.alpha = 1.0f; color.red = color.green = color.blue = 0.9f; draw_string_set_draw_mode(font_index, NONE, 0, 0, &color); + icon.x0 = (short)(bounds.x0 + 16); + icon.x1 = (short)(icon.x0 + line_height); + icon.y0 = (short)(draw_unicode_string_capital_middle(&text, player->name) - line_height / 2); + icon.y1 = (short)(icon.y0 + line_height); + network_voice_draw_icon(&icon, FALSE, 1.0f); rasterizer_draw_unicode_string(&text, NULL, NULL, 0, player->name); row++; } diff --git a/source/text/draw_string.c b/source/text/draw_string.c index 4b9196b5..8f0a2f95 100644 --- a/source/text/draw_string.c +++ b/source/text/draw_string.c @@ -1459,6 +1459,51 @@ void draw_unicode_string_compute_bounds( return; } +/* port: the middle of a string's capitals as it is drawn in the bounds +(the draw mode's font: the rows of its H with ink, on the line's baseline), +for what is drawn beside it, centred on it (voice chat's speakers: +port/linux/game/network_voice.c) */ +short draw_unicode_string_capital_middle( + rectangle2d const *bounds, + wchar_t const *string) +{ + rectangle2d text_bounds; + rectangle2d cursor_bounds; + struct font_header *font; + struct font_character *capital; + short baseline; + short top = NONE; + short bottom = NONE; + short row; + + draw_unicode_string_compute_bounds(bounds, string, &text_bounds, &cursor_bounds); + font = text_bounds_globals.last_font_header; + if (!font) + return (short)((bounds->y0 + bounds->y1) / 2); + baseline = (short)(cursor_bounds.y0 + font->ascending_height); + capital = font_get_character_by_ascii_code(font, 'H'); + for (row = 0; capital && row < capital->bitmap_height; row++) + { + byte const *pixels = (byte const *)font->pixels.address + capital->pixels_offset + row * capital->bitmap_width; + short column; + + for (column = 0; column < capital->bitmap_width; column++) + { + if (pixels[column]) + { + if (top == NONE) + top = row; + bottom = row; + break; + } + } + } + /* (no H: the middle of the font's height above the baseline) */ + if (top == NONE) + return (short)(baseline - font->ascending_height / 2); + return (short)(baseline - capital->bitmap_origin_y + (top + bottom + 1) / 2); +} + short draw_string_pick( rectangle2d const *bounds, char const *string, diff --git a/source/text/draw_string.h b/source/text/draw_string.h index d33e1d7a..ea38ef1c 100644 --- a/source/text/draw_string.h +++ b/source/text/draw_string.h @@ -95,6 +95,11 @@ void draw_unicode_string_compute_bounds( wchar_t const *string, rectangle2d *text_bounds, rectangle2d *cursor_bounds); +/* port: the middle of the string's capitals as it is drawn in the bounds, +in the draw mode's font (draw_string_set_draw_mode) */ +short draw_unicode_string_capital_middle( + rectangle2d const *bounds, + wchar_t const *string); short draw_string_pick( rectangle2d const *bounds, char const *string, diff --git a/tools/ce_menus.py b/tools/ce_menus.py index c57a38e1..b3f6742b 100644 --- a/tools/ce_menus.py +++ b/tools/ce_menus.py @@ -626,6 +626,12 @@ def main() -> None: lines.append(art.port_picture(f"pause/pausebox_{piece}", [ (port_settings.pause_box_svg(piece, port_settings.pause_box_height(buttons)), 4 if piece == "center" else 16, 256) for buttons in port_settings.PAUSE_BOX_BUTTONS])) + # (voice chat's speaker, of a player who talks and of one muted: Lucide's + # icons, port/assets/icons/lucide, white for network_voice.c to tint) + lines.append(art.port_picture("voice/speaker", [ + ((ROOT / "port/assets/icons/lucide" / f"{icon}.svg").read_text().replace('stroke="currentColor"', + 'stroke="#FFFFFF"'), 24, 24) + for icon in ("volume-2", "volume-x")])) # (nothing: what a game map draws for a frame of ui.map's it has not, # menu_tags.c) lines.append(art.port_picture("blank", [ diff --git a/tools/ci_build.py b/tools/ci_build.py index 5bf9f441..eb79b03c 100644 --- a/tools/ci_build.py +++ b/tools/ci_build.py @@ -114,6 +114,9 @@ def main() -> int: # voice chat's codec (port/third_party/opus), in every build, whose BSD # license asks binaries to carry its notice shutil.copy2(ROOT / "port/third_party/opus/COPYING", dist / "opus-COPYING.txt") + # voice chat's speaker icons (port/assets/icons/lucide, drawn into the + # menus' bitmaps), whose ISC license asks copies to carry its notice + shutil.copy2(ROOT / "port/assets/icons/lucide/LICENSE", dist / "lucide-LICENSE.txt") # internet play's MQTT brokers, a file beside the game (network.brokers_file; # Android's APK has its own copy) if args.platform != "android": From b2990d420dae36f76175f9f3ba5c0354c81a430a Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:32:15 +1300 Subject: [PATCH 081/187] Leaving during a votekick passes it A player who leaves during a vote against them is kept out for network.votekick_ban_minutes, as if the vote passed, and while a vote runs against them the host refuses them joining again, so no one dodges a vote by leaving. --- port/linux/README.md | 4 +++ port/linux/game/network_votekick.c | 51 ++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index 4c030097..19f0235e 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -667,6 +667,10 @@ Only machines with the invite can find the game: - Nobody can vote to kick a player of the host. - A player kicked by a vote cannot join again for `network.votekick_ban_minutes` (30), by address and hardware id. + - A player who leaves during a vote against them is kept out the same + way, as if the vote passed. While the vote runs, the host refuses the + player if they try to join again. Thus nobody can avoid a vote by + leaving. So that every player can be named, the host trims the spaces around a name and removes characters that draw as nothing. A letter with a mark is typed as the plain letter (`ban jose` for "José"). diff --git a/port/linux/game/network_votekick.c b/port/linux/game/network_votekick.c index 9e4eb067..2b416a40 100644 --- a/port/linux/game/network_votekick.c +++ b/port/linux/game/network_votekick.c @@ -39,7 +39,9 @@ votes against: - Only the host speaks: what a client did wrong is told to it alone, at most once a second; the votes to everyone, only as they change. - A machine kicked by a vote is kept out of the host's games, by address - and hardware id, for votekick_ban_minutes. + and hardware id, for votekick_ban_minutes. One that leaves during a vote + against it is too, as if the vote passed, and while the vote runs it is + refused if it tries to join again: no one dodges a vote by leaving. A client hears the host's count every second (and as it changes), for the scoreboard. A build without votes drops both messages as kinds it does not @@ -400,7 +402,18 @@ boolean network_votekick_kept_out( unsigned long address, char const *hardware_id) { - return votekick_remembered(votekick_kept_out_identities, distributed_real_address(address), hardware_id, NULL); + unsigned long real_address = distributed_real_address(address); + + /* (nor the player a vote runs against, joining again during it: it + would drop the machine voted against for a new one, out of the vote) */ + if (votekick.active && votekick_host() && + ((real_address && real_address == votekick.target_address) || + (hardware_id && hardware_id[0] && votekick.target_hardware_id[0] && + !csstrcmp(hardware_id, votekick.target_hardware_id)))) + { + return TRUE; + } + return votekick_remembered(votekick_kept_out_identities, real_address, hardware_id, NULL); } /* the client machines in the game with a player who has not quit, and the @@ -545,18 +558,31 @@ static void votekick_end( votekick_send_status(); } +/* the vote passed: the player kicked and kept out; or (left) the player +left during it, which passes it too (no one dodges a vote by leaving), and +is kept out as well */ static void votekick_pass( - void) + boolean left) { char notice[160]; long ban_minutes = PIN(config_integer("network.votekick_ban_minutes"), 1, 24 * 60); - snprintf(notice, sizeof(notice), "%s kicked by vote (%d of %d), kept out for %ld minute%s", - votekick.target_names, votekick.votes, votekick.needed, ban_minutes, ban_minutes == 1 ? "" : "s"); + if (left) + { + snprintf(notice, sizeof(notice), "%s left during the vote to kick them, kept out for %ld minute%s", + votekick.target_names, ban_minutes, ban_minutes == 1 ? "" : "s"); + } + else + { + snprintf(notice, sizeof(notice), "%s kicked by vote (%d of %d), kept out for %ld minute%s", + votekick.target_names, votekick.votes, votekick.needed, ban_minutes, ban_minutes == 1 ? "" : "s"); + } distributed_send_notice(notice); votekick_remember(votekick_kept_out_identities, votekick.target_address, votekick.target_hardware_id, (unsigned long)ban_minutes * 60 * 1000); - network_game_server_kick_machine(votekick.target_machine, FALSE); + /* (one that left is gone: its slot may be another's now) */ + if (!left) + network_game_server_kick_machine(votekick.target_machine, FALSE); votekick_end(); } @@ -671,7 +697,7 @@ static void votekick_host_request( } } if (votekick.votes >= votekick.needed) - votekick_pass(); + votekick_pass(FALSE); else votekick_send_status(); return; @@ -732,7 +758,7 @@ static void votekick_host_request( distributed_send_notice(notice); error(_error_log, "votekick: started by machine %ld against machine %ld", machine_index, target_machine); if (votekick.votes >= votekick.needed) - votekick_pass(); + votekick_pass(FALSE); else votekick_send_status(); } @@ -792,13 +818,10 @@ void network_votekick_host_tick( } if (!votekick.active) return; + /* (the player left: the vote passes) */ if (!votekick_target_present()) { - char notice[160]; - - snprintf(notice, sizeof(notice), "%s left: the vote ended", votekick.target_names); - distributed_send_notice(notice); - votekick_end(); + votekick_pass(TRUE); return; } /* (those gone counted no more, and those who have since played long @@ -808,7 +831,7 @@ void network_votekick_host_tick( votekick_recount(); if (votekick.votes >= votekick.needed) { - votekick_pass(); + votekick_pass(FALSE); return; } if (votekick_time_reached(system_milliseconds(), votekick.started_at + VOTE_SECONDS * 1000)) From b08a95b3ba668c3b126c71383ab2d93d4d16b506 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:36:47 +1300 Subject: [PATCH 082/187] Smaller voice chat list The in-game list of who talks is drawn at 0.8 times the HUD's text, its speakers with it. --- source/game/game_engine.c | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/source/game/game_engine.c b/source/game/game_engine.c index e448c91c..9a048cc6 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -3875,7 +3875,10 @@ void game_engine_rasterize_message( /* port: who is talking in voice chat (network_voice.c), down the view's left from below its middle: each machine's first player's name after its -speaker (this machine's own too, as it talks); while the scores are hidden */ +speaker (this machine's own too, as it talks), VOICE_SPEAKERS_SCALE times +the HUD's text; while the scores are hidden */ +#define VOICE_SPEAKERS_SCALE 0.8f + static void game_engine_rasterize_voice_speakers( void) { @@ -3892,6 +3895,8 @@ static void game_engine_rasterize_voice_speakers( struct font_header *font; short line_height; short row = 0; + short left; + short top; if (font_index == NONE || !network_voice_available()) return; @@ -3900,6 +3905,11 @@ static void game_engine_rasterize_voice_speakers( line_height = (short)(font->leading_height + font->descending_height + font->ascending_height); if (line_height <= 0) return; + /* (laid out at full size, from the list's top left, and drawn scaled + about it) */ + left = (short)(bounds.x0 + 16); + top = (short)(bounds.y0 + (bounds.y1 - bounds.y0) * 55 / 100); + rasterizer_text_set_scale(VOICE_SPEAKERS_SCALE, (real)left, (real)top); data_iterator_new(&iterator, player_data); while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL && row < MAXIMUM_SPEAKER_ROWS) { @@ -3918,22 +3928,25 @@ static void game_engine_rasterize_voice_speakers( machines_listed[listed_count++] = machine_index; if (!network_voice_machine_speaking(machine_index)) continue; - /* (the name after the speaker, which is centred on its capitals) */ - text.x0 = (short)(bounds.x0 + 16 + line_height + 4); - text.x1 = bounds.x1; - text.y0 = (short)(bounds.y0 + (bounds.y1 - bounds.y0) * 55 / 100 + row * line_height); + /* (the name after the speaker, which is centred on its capitals: + where they are drawn, scaled) */ + text.x0 = (short)(left + line_height + 4); + text.x1 = (short)(left + (bounds.x1 - left) / VOICE_SPEAKERS_SCALE); + text.y0 = (short)(top + row * line_height); text.y1 = (short)(text.y0 + line_height); color.alpha = 1.0f; color.red = color.green = color.blue = 0.9f; draw_string_set_draw_mode(font_index, NONE, 0, 0, &color); - icon.x0 = (short)(bounds.x0 + 16); - icon.x1 = (short)(icon.x0 + line_height); - icon.y0 = (short)(draw_unicode_string_capital_middle(&text, player->name) - line_height / 2); - icon.y1 = (short)(icon.y0 + line_height); + icon.x0 = left; + icon.x1 = (short)(left + line_height * VOICE_SPEAKERS_SCALE); + icon.y0 = (short)(top + (draw_unicode_string_capital_middle(&text, player->name) - top) * VOICE_SPEAKERS_SCALE - + line_height * VOICE_SPEAKERS_SCALE / 2); + icon.y1 = (short)(icon.y0 + line_height * VOICE_SPEAKERS_SCALE); network_voice_draw_icon(&icon, FALSE, 1.0f); rasterizer_draw_unicode_string(&text, NULL, NULL, 0, player->name); row++; } + rasterizer_text_set_scale(1.0f, 0.0f, 0.0f); } static void game_engine_post_rasterize_in_game( From 348f8ad4f88c7c8f57f9c0c0f92ef7e8002bdb4c Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:44:55 +1300 Subject: [PATCH 083/187] Speaker beside talking players' name tags A player talking in voice chat has the speaker just left of their name above their head, centred on its capitals, at the name's scale and fade; only where the name itself is shown (display.player_names, and the gametype's motion tracker rules). --- port/linux/README.md | 8 +++++--- source/interface/hud.c | 26 +++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index 19f0235e..e4f28bd5 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -497,9 +497,11 @@ In co-op, Server Setup has no Teamplay Options: the same settings are in Server Setup > Voice and Voting. A speaker shows next to the name of a player who talks: in the lobby, on -the scoreboard, and in a list at the left of the screen during a game. To -mute a player, open the scoreboard, right-click, click the player, and -click Mute voice. You no longer hear them, and nobody else is told. +the scoreboard, in a list at the left of the screen during a game, and +beside the name above their head (when names are shown there: +`display.player_names`). To mute a player, open the scoreboard, +right-click, click the player, and click Mute voice. You no longer hear +them, and nobody else is told. The host sends each voice on to the players who can hear it. The host accepts a voice only from the machine of the player (with a key that it diff --git a/source/interface/hud.c b/source/interface/hud.c index 3c13a022..d962840c 100644 --- a/source/interface/hud.c +++ b/source/interface/hud.c @@ -997,7 +997,8 @@ static void hud_draw_players( } /* port: in multiplayer and network co-op, players' names above their heads -(display.player_names: "all", "allies", "enemies" or "none"). An ally's goes +(display.player_names: "all", "allies", "enemies" or "none"), a speaker +beside the name of one talking in voice chat. An ally's goes above the triangle the game draws over teammates; an enemy's only within the motion sensor's reach, while the view sees them and they are not camouflaged, so that it never gives away where they hide. Whose names show @@ -1008,6 +1009,10 @@ allies' if it shows only friends (game_engine_draw_object_in_motion_sensor). */ const char *config_string(const char *name); double config_real(const char *name); unsigned long config_changes(void); +/* port/linux/game/network_voice.c's: a player talking in voice chat (its +machine), and its speaker drawn */ +boolean network_voice_machine_speaking(long machine_index); +void network_voice_draw_icon(rectangle2d const *bounds, boolean muted, real alpha); enum { @@ -1175,6 +1180,25 @@ static void hud_draw_player_name( rasterizer_text_set_scale(hud_player_name_scale(), (real)x, (real)y); rasterizer_draw_unicode_string(&bounds, NULL, NULL, 0, name); rasterizer_text_set_scale(1.0f, 0.0f, 0.0f); + /* (talking in voice chat: the speaker just left of the name, centred on + its capitals, where they are drawn, scaled; fading with it) */ + if (network_voice_machine_speaking(player->network_player_data.machine_index)) + { + real scale = hud_player_name_scale(); + rectangle2d text; + rectangle2d cursor; + rectangle2d icon; + short size = (short)((font->ascending_height + font->descending_height) * scale); + short middle; + + draw_unicode_string_compute_bounds(&bounds, name, &text, &cursor); + middle = (short)(y + (draw_unicode_string_capital_middle(&bounds, name) - y) * scale); + icon.x1 = (short)(x + (text.x0 - x) * scale - 3.0f * scale); + icon.x0 = (short)(icon.x1 - size); + icon.y0 = (short)(middle - size / 2); + icon.y1 = (short)(icon.y0 + size); + network_voice_draw_icon(&icon, FALSE, color.alpha); + } return; } From c916c1c838bbb523827d17f009548150906250b1 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Fri, 9 Oct 2026 00:33:57 -0400 Subject: [PATCH 084/187] Test the mixer on known signals tools/harness/tests/test_mixer.py compiles dsound_sdl.c's mixer (its resampler, voices, reverb and limiter) against a fake world and checks it, so a faster mixer can be shown to sound the same: - the resampler against the same Kaiser-windowed sinc worked out in double precision tap by tap, on noise and an impulse at 22, 32, 44.1 and 48 kHz and pitched up and down (17 to 96 kHz): within -100 dB (it is at -105 dB, the table's interpolation); - sines as heard: a 22 kHz voice flat within 0.5 dB to 10 kHz and a 44.1 kHz one to 20 kHz, everything else that comes out at least 70 dB down (80 to 89 dB in the band); a tone a voice pitched up takes past 24 kHz does not come back as an alias (over 60 dB down); - a voice out of earshot, which is not resampled, moves on as one heard does and sounds the same once it is in earshot, also as its packets run out; - a mono voice is heard as a stereo one with both channels the same. Three faults (taps one frame off, half the phase rows, no narrowing of the low pass for a voice played fast) must each fail a case. The harness links libm for it. mixer.c also writes voices and a whole scene's mix as raw floats, to compare one mixer with another (MIXER_SOURCE), prints the figures ("metrics") and times a game's worth of voices ("bench"). --- tools/harness/__init__.py | 2 +- tools/harness/tests/mixer.c | 690 ++++++++++++++++++++++++++++++ tools/harness/tests/test_mixer.py | 60 +++ 3 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 tools/harness/tests/mixer.c create mode 100644 tools/harness/tests/test_mixer.py diff --git a/tools/harness/__init__.py b/tools/harness/__init__.py index 01b8bda3..79a8866d 100644 --- a/tools/harness/__init__.py +++ b/tools/harness/__init__.py @@ -76,7 +76,7 @@ def build(test, generated): executable = work / test command = [os.environ.get("CC", "clang"), "-m32", "-std=gnu99", "-O2", "-Wall", "-Werror", "-Wno-unused-function", "-Wno-unused-variable", "-I", str(HARNESS / "include"), "-I", str(work), - str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable)] + str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable), "-lm"] result = subprocess.run(command, capture_output=True, text=True) if result.returncode: raise RuntimeError(f"{test}.c does not compile:\n{result.stderr}") diff --git a/tools/harness/tests/mixer.c b/tools/harness/tests/mixer.c new file mode 100644 index 00000000..b51a9848 --- /dev/null +++ b/tools/harness/tests/mixer.c @@ -0,0 +1,690 @@ +/* +MIXER.C (test) + +The real mixer of port/linux/src/dsound_sdl.c (its resampler, voices, +reverb and limiter; test_mixer.py takes them as under_test.inc) on known +signals: + - the resampler against the same Kaiser-windowed sinc worked out in + double precision, tap by tap: noise, sines and an impulse at the + game's rates (22 and 44 kHz voices, others pitched up and down, a 48 + kHz one), so a faster resampler is still the same low pass; + - sines through it measured as the ear would hear them: the gain across + the band, and what else comes out (images and aliases) under the tone; + - a voice out of earshot, which is not resampled, moves on as one heard + does, and is heard the same once it comes into earshot; + - mono voices heard as stereo ones with both channels the same are. +"render" writes voices at each rate and a scene's whole mix as raw floats, +to compare one mixer with another; "metrics" prints the figures and "bench" +times a game's worth of voices. +*/ + +#include "harness.h" + +#include +#include + +typedef int BOOL; +typedef unsigned long DWORD; +typedef long LONG; +typedef unsigned long ULONG; +typedef void *LPVOID; +typedef void (*LPFNXMEDIAOBJECTCALLBACK)(void *, void *, DWORD); +typedef struct { void *lpVtbl; } IDirectSoundStream; +typedef struct { void *pvBuffer; DWORD dwMaxSize; } XMEDIAPACKET; +typedef struct +{ + LONG lRoom, lRoomHF; + float flRoomRolloffFactor, flDecayTime, flDecayHFRatio; + LONG lReflections; + float flReflectionsDelay; + LONG lReverb; + float flReverbDelay, flDiffusion, flDensity, flHFReference; +} DSI3DL2LISTENER; +#define DSBVOLUME_MIN (-10000) +#define DS3DMODE_NORMAL 0x00000000 +#define DS3DMODE_HEADRELATIVE 0x00000001 +#define DS3DMODE_DISABLE 0x00000002 + +/* the players' voices (voice_audio.c): none */ +static void voice_audio_mix(float *output, unsigned long frames) +{ + (void)output; + (void)frames; +} + +#include "under_test.inc" + +#define PI 3.14159265358979323846 +#define CHUNK 512 /* frames a mix, as SDL asks on Linux */ + +/* ---------- voices */ + +/* a voice of frames frames of channels channels at rate, played at frequency +(0: its rate), a 2D voice heard at full volume in both speakers */ +static struct sdl_stream *voice_new(const short *samples, unsigned long frames, unsigned long channels, + unsigned long rate, unsigned long frequency) +{ + struct sdl_stream *stream = calloc(1, sizeof(*stream)); + + stream->channels = channels; + stream->sample_rate = rate; + stream->frequency = frequency; + stream->volume = 1.0f; + stream->mix_left = stream->mix_right = 1.0f; + stream->room = stream->room_hf = DSBVOLUME_MIN; + stream->packets[0].samples = malloc(frames * channels * sizeof(short)); + memcpy(stream->packets[0].samples, samples, frames * channels * sizeof(short)); + stream->packets[0].frames = frames; + stream->packet_count = 1; + resampler_reset(stream); + return stream; +} + +static void voice_delete(struct sdl_stream *stream) +{ + free(stream->packets[0].samples); + free(stream); +} + +/* frames of the voice mixed alone, CHUNK at a time: the left channel, or +both (stereo: interleaved) */ +static void render(struct sdl_stream *stream, float *output, unsigned long frames, BOOL stereo) +{ + static float buffer[CHUNK * 2], send[CHUNK]; + unsigned long done, frame; + + for (done = 0; done < frames; done += CHUNK) + { + unsigned long count = frames - done < CHUNK ? frames - done : CHUNK; + + memset(buffer, 0, sizeof(buffer)); + memset(send, 0, sizeof(send)); + mix_voice(stream, buffer, send, count); + for (frame = 0; frame < count; frame++) + { + if (stereo) + { + output[(done + frame) * 2] = buffer[frame * 2]; + output[(done + frame) * 2 + 1] = buffer[frame * 2 + 1]; + } + else + { + output[done + frame] = buffer[frame * 2]; + } + } + } +} + +/* ---------- the reference: the same low pass in double precision */ + +static double reference_kernel(double distance) +{ + double edge, angle, sinc; + + distance = fabs(distance); + if (distance >= RESAMPLER_ZERO_CROSSINGS) + return 0.0; + edge = distance / RESAMPLER_ZERO_CROSSINGS; + angle = PI * RESAMPLER_CUTOFF * distance; + sinc = distance > 0.0 ? sin(angle) / angle : 1.0; + return RESAMPLER_CUTOFF * sinc * bessel_i0(RESAMPLER_KAISER_BETA * sqrt(1.0 - edge * edge)) / + bessel_i0(RESAMPLER_KAISER_BETA); +} + +/* output frame n of source (frames, mono, full scale 1) resampled at step: +the frames around source time n step, weighted by the low pass (narrowed for a +step over 1, as the mixer's) */ +static double reference_frame(const double *source, long frames, double step, unsigned long n) +{ + double scale = step > 1.0 ? 1.0 / (step < RESAMPLER_MAXIMUM_STRETCH ? step : RESAMPLER_MAXIMUM_STRETCH) : 1.0; + double time = (double)n * step, sum = 0.0; + long center = (long)floor(time), width = (long)ceil(RESAMPLER_ZERO_CROSSINGS / scale), k; + + for (k = center + 1 - width; k <= center + width; k++) + { + if (k >= 0 && k < frames) + sum += source[k] * scale * reference_kernel((time - (double)k) * scale); + } + return sum; +} + +/* ---------- signals */ + +static unsigned long random_state = 1; + +static double random_uniform(void) +{ + random_state = random_state * 1103515245UL + 12345UL; + return (double)((random_state >> 8) & 0xffffff) / 16777216.0 * 2.0 - 1.0; +} + +/* frames of a signal as 16-bit samples (and as the doubles they are) */ +static void quantize(const double *signal, short *samples, double *exact, unsigned long frames) +{ + unsigned long frame; + + for (frame = 0; frame < frames; frame++) + { + long value = lround(signal[frame] * 32767.0); + + samples[frame] = (short)(value > 32767 ? 32767 : value < -32768 ? -32768 : value); + exact[frame] = samples[frame] / 32768.0; + } +} + +/* ---------- figures */ + +/* the amplitude of a sine of frequency cycles a frame in signal[first, last), +and the power of the rest relative to the sine's, in dB */ +static void tone_fit(const float *signal, unsigned long first, unsigned long last, double frequency, + double *amplitude, double *rest_db) +{ + double ss = 0, sc = 0, cc = 0, ys = 0, yc = 0, a, b, determinant, rest = 0, tone = 0; + unsigned long n; + + for (n = first; n < last; n++) + { + double s = sin(2.0 * PI * frequency * n), c = cos(2.0 * PI * frequency * n); + + ss += s * s; + sc += s * c; + cc += c * c; + ys += signal[n] * s; + yc += signal[n] * c; + } + determinant = ss * cc - sc * sc; + a = (ys * cc - yc * sc) / determinant; + b = (yc * ss - ys * sc) / determinant; + for (n = first; n < last; n++) + { + double fit = a * sin(2.0 * PI * frequency * n) + b * cos(2.0 * PI * frequency * n); + + rest += (signal[n] - fit) * (signal[n] - fit); + tone += fit * fit; + } + *amplitude = sqrt(a * a + b * b); + *rest_db = 10.0 * log10(rest / tone + 1e-30); +} + +static double decibels(double ratio) +{ + return 20.0 * log10(ratio + 1e-30); +} + +/* the mixer's output against the reference's for a signal at rate played at +frequency: the error's power relative to the reference's, in dB, and its +largest relative to the largest sample */ +static void against_reference(const double *signal, unsigned long frames, unsigned long rate, unsigned long frequency, + double *error_db, double *peak_error_db) +{ + short *samples = malloc(frames * sizeof(short)); + double *exact = malloc(frames * sizeof(double)); + double step = (double)(frequency ? frequency : rate) / OUTPUT_RATE, error = 0, power = 0, peak = 0, worst = 0; + unsigned long outputs = (unsigned long)(frames / step), n; + float *output = calloc(outputs, sizeof(float)); + struct sdl_stream *stream; + + quantize(signal, samples, exact, frames); + stream = voice_new(samples, frames, 1, rate, frequency); + render(stream, output, outputs, FALSE); + for (n = 0; n < outputs; n++) + { + double expected = reference_frame(exact, (long)frames, step, n); + double difference = output[n] - expected; + + error += difference * difference; + power += expected * expected; + peak = fabs(expected) > peak ? fabs(expected) : peak; + worst = fabs(difference) > worst ? fabs(difference) : worst; + } + *error_db = 10.0 * log10(error / power + 1e-30); + *peak_error_db = decibels(worst / peak); + voice_delete(stream); + free(output); + free(exact); + free(samples); +} + +/* a sine of hertz at rate, played at frequency: its gain through the mixer +(dB) and the power of everything else that comes out, relative to it (dB); a +sine the voice plays above the output's band is all "else", relative to the +sine put in */ +static void sine_figures(double hertz, unsigned long rate, unsigned long frequency, double *gain_db, double *rest_db) +{ + unsigned long frames = rate, outputs, n; + double step = (double)(frequency ? frequency : rate) / OUTPUT_RATE, played = hertz * step * OUTPUT_RATE / rate; + double *signal = malloc(frames * sizeof(double)), *exact = malloc(frames * sizeof(double)), amplitude; + short *samples = malloc(frames * sizeof(short)); + float *output; + struct sdl_stream *stream; + + for (n = 0; n < frames; n++) + signal[n] = 0.5 * sin(2.0 * PI * hertz * n / rate); + quantize(signal, samples, exact, frames); + outputs = (unsigned long)(frames / step); + output = calloc(outputs, sizeof(float)); + stream = voice_new(samples, frames, 1, rate, frequency); + render(stream, output, outputs, FALSE); + if (played < 0.5 * OUTPUT_RATE) + { + tone_fit(output, outputs / 4, outputs * 3 / 4, played / OUTPUT_RATE, &litude, rest_db); + *gain_db = decibels(amplitude / 0.5); + } + else + { + double power = 0; + + for (n = outputs / 4; n < outputs * 3 / 4; n++) + power += (double)output[n] * output[n]; + *gain_db = -999.0; + *rest_db = 10.0 * log10(power / (outputs / 2) / (0.5 * 0.5 / 2) + 1e-30); + } + voice_delete(stream); + free(output); + free(samples); + free(exact); + free(signal); +} + +/* a logarithmic sweep from 20 Hz to top at rate over the voice: what comes out +against the sweep itself at the output's times (an ideal resampler), in dB */ +static double sweep_figure(unsigned long rate, double top) +{ + unsigned long frames = 2 * rate, outputs = (unsigned long)((double)frames * OUTPUT_RATE / rate), n; + double *signal = malloc(frames * sizeof(double)), *exact = malloc(frames * sizeof(double)); + double duration = (double)frames / rate, k = log(top / 20.0), error = 0, power = 0; + short *samples = malloc(frames * sizeof(short)); + float *output = calloc(outputs, sizeof(float)); + struct sdl_stream *stream; + + for (n = 0; n < frames; n++) + { + double t = (double)n / rate; + + signal[n] = 0.5 * sin(2.0 * PI * 20.0 * duration / k * (exp(t / duration * k) - 1.0)); + } + quantize(signal, samples, exact, frames); + stream = voice_new(samples, frames, 1, rate, 0); + render(stream, output, outputs, FALSE); + for (n = OUTPUT_RATE / 10; n + OUTPUT_RATE / 10 < outputs; n++) + { + double t = (double)n / OUTPUT_RATE; + double ideal = 0.5 * sin(2.0 * PI * 20.0 * duration / k * (exp(t / duration * k) - 1.0)); + + error += (output[n] - ideal) * (output[n] - ideal); + power += ideal * ideal; + } + voice_delete(stream); + free(output); + free(samples); + free(exact); + free(signal); + return 10.0 * log10(error / power + 1e-30); +} + +static double *noise(unsigned long frames) +{ + double *signal = malloc(frames * sizeof(double)); + unsigned long n; + + random_state = 1; + for (n = 0; n < frames; n++) + signal[n] = 0.5 * random_uniform(); + return signal; +} + +/* ---------- a scene: a game's worth of voices, for "render" and "bench" */ + +/* voices: mono 22 kHz sounds in the world (indoors sending to the reverb, +half muffled, silent ones out of earshot), stereo 44 kHz music, a few +pitched; each plays for seconds */ +static void scene(unsigned long voices, unsigned long silent, BOOL indoor, double seconds) +{ + unsigned long frames = 22050 * 4, v, n; + short *mono = malloc(frames * sizeof(short)), *stereo = malloc(frames * 2 * sizeof(short)); + + random_state = 7; + for (n = 0; n < frames; n++) + { + mono[n] = (short)(8000 * random_uniform()); + stereo[n * 2] = (short)(8000 * random_uniform()); + stereo[n * 2 + 1] = (short)(8000 * random_uniform()); + } + if (indoor) + { + DSI3DL2LISTENER room = { -1000, -100, 0.0f, 1.49f, 0.83f, -2602, 0.007f, 200, 0.011f, 100.0f, 100.0f, 5000.0f }; + + environment = room; + environment_serial++; + } + resampler_initialize(); + resampler_phases_initialize(); + reverb_initialize(); + for (v = 0; v < voices + silent; v++) + { + struct sdl_stream *stream; + + if (v == 0) + stream = voice_new(stereo, frames, 2, 44100, 0); + else + stream = voice_new(mono, frames, 1, 22050, v % 5 == 0 ? 22050 + 2000 * (v % 3) : 0); + if (v) + { + stream->has_3d = TRUE; + stream->mode = DS3DMODE_NORMAL; + stream->position[0] = (float)(v % 7) - 3.0f; + stream->position[2] = v < voices ? 2.0f : 1000.0f; + stream->minimum_distance = 1.0f; + stream->maximum_distance = v < voices ? 50.0f : 100.0f; + stream->direct = v < voices ? 0 : DSBVOLUME_MIN; + stream->direct_hf = v % 2 ? -500 : 0; + stream->room = v < voices ? 0 : DSBVOLUME_MIN; + stream->room_hf = 0; + } + /* a packet as long as the scene */ + free(stream->packets[0].samples); + stream->packets[0].frames = (unsigned long)(seconds * 50000); + stream->packets[0].samples = malloc(stream->packets[0].frames * stream->channels * sizeof(short)); + for (n = 0; n < stream->packets[0].frames * stream->channels; n++) + stream->packets[0].samples[n] = (v == 0 ? stereo : mono)[n % frames]; + stream->next = streams; + streams = stream; + } + free(mono); + free(stereo); +} + +/* the scene's voices out of earshot come into it */ +static void scene_approach(void) +{ + struct sdl_stream *stream; + + for (stream = streams; stream; stream = stream->next) + { + if (stream->direct == DSBVOLUME_MIN) + { + stream->position[2] = 3.0f; + stream->direct = stream->room = 0; + } + } +} + +static void bench(unsigned long voices, unsigned long silent, BOOL indoor, double seconds) +{ + unsigned long chunks = (unsigned long)(seconds * OUTPUT_RATE / CHUNK), chunk; + static float output[CHUNK * 2]; + struct timespec start, end; + double elapsed; + + scene(voices, silent, indoor, seconds); + clock_gettime(CLOCK_MONOTONIC, &start); + for (chunk = 0; chunk < chunks; chunk++) + mix(output, CHUNK); + clock_gettime(CLOCK_MONOTONIC, &end); + elapsed = (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) * 1e-9; + printf("%lu voices (%lu out of earshot)%s: %.2f%% of a core, %.1f ns a frame\n", voices, silent, + indoor ? " indoors" : "", 100.0 * elapsed / seconds, elapsed * 1e9 / ((double)chunks * CHUNK)); +} + +int main(int argc, char **argv) +{ + char const *case_name = argc > 1 ? argv[1] : ""; + static const unsigned long rates[][2] = { + { 22050, 0 }, { 44100, 0 }, { 48000, 0 }, { 22050, 17000 }, { 22050, 30000 }, { 44100, 60000 }, + { 44100, 96000 }, { 32000, 0 }, + }; + unsigned long index; + + resampler_initialize(); + resampler_phases_initialize(); + + /* the resampler is the low pass worked out exactly: noise at every rate, + the error well under 16-bit's */ + CASE("reference") + { + double *signal = noise(30000); + + for (index = 0; index < sizeof(rates) / sizeof(rates[0]); index++) + { + double error_db, peak_db; + + against_reference(signal, 30000, rates[index][0], rates[index][1], &error_db, &peak_db); + CHECK(error_db < -100.0 && peak_db < -90.0, "%lu Hz played at %lu: the error %.1f dB, its peak %.1f dB", + rates[index][0], rates[index][1], error_db, peak_db); + } + free(signal); + return 0; + } + /* an impulse: the low pass itself */ + CASE("impulse") + { + double *signal = calloc(4000, sizeof(double)); + + signal[2000] = 1.0; + for (index = 0; index < sizeof(rates) / sizeof(rates[0]); index++) + { + double error_db, peak_db; + + against_reference(signal, 4000, rates[index][0], rates[index][1], &error_db, &peak_db); + CHECK(peak_db < -90.0, "%lu Hz played at %lu: the impulse's error peaks at %.1f dB", rates[index][0], + rates[index][1], peak_db); + } + free(signal); + return 0; + } + /* sines as heard: a 22 kHz voice flat within half a dB to 10 kHz, a 44 + kHz one to 20 kHz, the rest 70 dB down in the band, more past it */ + CASE("sines") + { + static const struct { unsigned long rate, frequency; double hertz, gain_db, rest_db; } sines[] = { + { 22050, 0, 100.0, 0.05, -85.0 }, { 22050, 0, 1000.0, 0.05, -85.0 }, { 22050, 0, 5000.0, 0.05, -80.0 }, + { 22050, 0, 8000.0, 0.1, -75.0 }, { 22050, 0, 10000.0, 0.5, -70.0 }, + { 44100, 0, 1000.0, 0.05, -85.0 }, { 44100, 0, 10000.0, 0.05, -80.0 }, + { 44100, 0, 16000.0, 0.1, -75.0 }, { 44100, 0, 20000.0, 0.5, -70.0 }, + { 48000, 0, 1000.0, 0.05, -85.0 }, { 48000, 0, 20000.0, 0.5, -70.0 }, + { 22050, 30000, 5000.0, 0.1, -75.0 }, + }; + + for (index = 0; index < sizeof(sines) / sizeof(sines[0]); index++) + { + double gain_db, rest_db; + + sine_figures(sines[index].hertz, sines[index].rate, sines[index].frequency, &gain_db, &rest_db); + CHECK(fabs(gain_db) < sines[index].gain_db && rest_db < sines[index].rest_db, + "%.0f Hz at %lu Hz, played at %lu: gain %.2f dB, the rest %.1f dB", sines[index].hertz, + sines[index].rate, sines[index].frequency, gain_db, rest_db); + } + return 0; + } + /* a tone a voice played faster than the output rate takes over the + output's band does not come out as an alias */ + CASE("aliases") + { + double gain_db, rest_db; + + /* 18 kHz at 44.1 kHz, played at 96 kHz: 39 kHz */ + sine_figures(18000.0, 44100, 96000, &gain_db, &rest_db); + CHECK(rest_db < -60.0, "an alias of 39 kHz %.1f dB", rest_db); + /* 15 kHz at 44.1 kHz, played at 88.2 kHz: 30 kHz */ + sine_figures(15000.0, 44100, 88200, &gain_db, &rest_db); + CHECK(rest_db < -60.0, "an alias of 30 kHz %.1f dB", rest_db); + return 0; + } + /* a voice out of earshot moves on as one heard does: once it is audible + again, both play the same */ + CASE("silent-voice") + { + double *signal = noise(60000); + short *samples = malloc(60000 * sizeof(short)); + double *exact = malloc(60000 * sizeof(double)); + float *heard = calloc(40000 * 2, sizeof(float)), *skipped = calloc(40000 * 2, sizeof(float)); + struct sdl_stream *loud, *quiet; + unsigned long n, sizes[] = { 1, 7, 512, 333, 2000, 1024 }; + + quantize(signal, samples, exact, 60000); + for (index = 0; index < 3; index++) + { + unsigned long frequency = index == 0 ? 0 : index == 1 ? 23456 : 70000, done = 0, size = 0; + + loud = voice_new(samples, 60000, 1, 22050, frequency); + quiet = voice_new(samples, 60000, 1, 22050, frequency); + /* the quiet one turned down for a while, at various mix sizes, + then up */ + quiet->volume = 0.0f; + while (done < 20000) + { + unsigned long count = sizes[size++ % 6]; + static float scratch[2000 * 2], send[2000]; + + memset(scratch, 0, sizeof(scratch)); + memset(send, 0, sizeof(send)); + mix_voice(quiet, scratch, send, count); + for (n = 0; n < count * 2; n++) + CHECK(scratch[n] == 0.0f, "a voice turned down was heard"); + memset(scratch, 0, sizeof(scratch)); + mix_voice(loud, scratch, send, count); + done += count; + } + CHECK(quiet->center == loud->center && quiet->phase == loud->phase && + quiet->history_count == loud->history_count && quiet->cursor == loud->cursor, + "played at %lu: turned down, the voice is at %lu + %f, heard at %lu + %f", frequency, + quiet->center, quiet->phase, loud->center, loud->phase); + /* (it fades in over a mix from 0; the loud one does the same) */ + quiet->volume = 1.0f; + loud->current_left = loud->current_right = 0.0f; + render(quiet, skipped, 40000, TRUE); + render(loud, heard, 40000, TRUE); + for (n = 0; n < 40000 * 2; n++) + CHECK(fabsf(heard[n] - skipped[n]) < 1.0e-6f, "played at %lu: frame %lu differs (%f, %f)", + frequency, n / 2, heard[n], skipped[n]); + voice_delete(loud); + voice_delete(quiet); + } + /* a voice that runs out of packets while it is turned down ends as + one heard does */ + loud = voice_new(samples, 3000, 1, 22050, 0); + quiet = voice_new(samples, 3000, 1, 22050, 0); + quiet->volume = 0.0f; + render(loud, heard, 20000, TRUE); + render(quiet, skipped, 20000, TRUE); + CHECK(quiet->center == loud->center && quiet->silence == loud->silence && + quiet->packets[0].finished == loud->packets[0].finished, "turned down, the voice ends at %lu (%lu), " + "heard at %lu (%lu)", quiet->center, quiet->silence, loud->center, loud->silence); + voice_delete(loud); + voice_delete(quiet); + free(heard); + free(skipped); + free(exact); + free(samples); + free(signal); + return 0; + } + /* a mono voice is heard as a stereo one with the same two channels */ + CASE("mono") + { + double *signal = noise(30000); + short *samples = malloc(30000 * sizeof(short)), *pairs = malloc(30000 * 2 * sizeof(short)); + double *exact = malloc(30000 * sizeof(double)); + float *mono = calloc(50000 * 2, sizeof(float)), *stereo = calloc(50000 * 2, sizeof(float)); + unsigned long n; + + quantize(signal, samples, exact, 30000); + for (n = 0; n < 30000; n++) + pairs[n * 2] = pairs[n * 2 + 1] = samples[n]; + for (index = 0; index < sizeof(rates) / sizeof(rates[0]); index++) + { + struct sdl_stream *one = voice_new(samples, 30000, 1, rates[index][0], rates[index][1]); + struct sdl_stream *two = voice_new(pairs, 30000, 2, rates[index][0], rates[index][1]); + + render(one, mono, 50000, TRUE); + render(two, stereo, 50000, TRUE); + for (n = 0; n < 50000 * 2; n++) + CHECK(fabsf(mono[n] - stereo[n]) < 1.0e-6f, "%lu Hz played at %lu: frame %lu differs (%f, %f)", + rates[index][0], rates[index][1], n / 2, mono[n], stereo[n]); + voice_delete(one); + voice_delete(two); + } + free(mono); + free(stereo); + free(exact); + free(pairs); + free(samples); + free(signal); + return 0; + } + CASE("metrics") + { + static const double tones[] = { 100, 1000, 5000, 8000, 10000, 10500, 15000, 18000, 20000, 21000 }; + double *signal = noise(30000), *impulse = calloc(4000, sizeof(double)); + + impulse[2000] = 1.0; + for (index = 0; index < sizeof(rates) / sizeof(rates[0]); index++) + { + unsigned long rate = rates[index][0], frequency = rates[index][1], tone; + double error_db, peak_db, impulse_db, impulse_peak_db; + + against_reference(signal, 30000, rate, frequency, &error_db, &peak_db); + against_reference(impulse, 4000, rate, frequency, &impulse_db, &impulse_peak_db); + printf("%lu Hz played at %lu: against the exact low pass, noise %.1f dB (peak %.1f), impulse peak %.1f dB\n", + rate, frequency ? frequency : rate, error_db, peak_db, impulse_peak_db); + for (tone = 0; tone < sizeof(tones) / sizeof(tones[0]); tone++) + { + double gain_db, rest_db; + + if (tones[tone] >= 0.5 * rate) + continue; + sine_figures(tones[tone], rate, frequency, &gain_db, &rest_db); + printf(" %6.0f Hz: gain %+.3f dB, the rest %.1f dB\n", tones[tone], gain_db, rest_db); + } + if (!frequency) + printf(" sweep to 0.9 of its band: %.1f dB from ideal\n", sweep_figure(rate, 0.45 * rate)); + } + free(signal); + free(impulse); + return 0; + } + /* noise, mono and stereo, at each rate through the mixer: the output as + raw floats on stdout, to compare one mixer with another */ + CASE("render") + { + static float mixed[CHUNK * 2]; + double *signal = noise(60000); + short *samples = malloc(30000 * 2 * sizeof(short)); + double *exact = malloc(60000 * sizeof(double)); + float *output = calloc(40000 * 2, sizeof(float)); + + quantize(signal, samples, exact, 60000); + for (index = 0; index < sizeof(rates) / sizeof(rates[0]); index++) + { + unsigned long channels; + + for (channels = 1; channels <= 2; channels++) + { + struct sdl_stream *stream = voice_new(samples, 30000, channels, rates[index][0], rates[index][1]); + + render(stream, output, 40000, TRUE); + fwrite(output, sizeof(float), 40000 * 2, stdout); + voice_delete(stream); + } + } + /* and a scene's whole mix, reverb and limiter, its voices out of + earshot coming into it halfway */ + scene(24, 8, TRUE, 3.0); + for (index = 0; index < 2 * OUTPUT_RATE / CHUNK; index++) + { + if (index == OUTPUT_RATE / CHUNK) + scene_approach(); + mix(mixed, CHUNK); + fwrite(mixed, sizeof(float), CHUNK * 2, stdout); + } + return 0; + } + CASE("bench") + { + bench(argc > 2 ? strtoul(argv[2], NULL, 0) : 24, argc > 3 ? strtoul(argv[3], NULL, 0) : 16, + argc > 4 && !strcmp(argv[4], "indoor"), 10.0); + return 0; + } + fprintf(stderr, "no case %s\n", case_name); + return 2; +} diff --git a/tools/harness/tests/test_mixer.py b/tools/harness/tests/test_mixer.py new file mode 100644 index 00000000..a62571a1 --- /dev/null +++ b/tools/harness/tests/test_mixer.py @@ -0,0 +1,60 @@ +"""The real mixer of dsound_sdl.c on known signals (mixer.c): its resampler against the same low pass worked out in +double precision, sines through it as the ear hears them, voices out of earshot and mono voices. + + python tools/harness/tests/test_mixer.py metrics the figures + python tools/harness/tests/test_mixer.py bench [voices] [out of earshot] [indoor] a game's worth of voices, timed + python tools/harness/tests/test_mixer.py render > out.raw noise through the mixer, to compare with another's +(MIXER_SOURCE=path: of another dsound_sdl.c) +""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, mutated, read, run # noqa: E402 + +SOURCE = "port/linux/src/dsound_sdl.c" +CASES = ["reference", "impulse", "sines", "aliases", "silent-voice", "mono"] + +# a fault in the mixer, and the case that must catch it +NEGATIVE_CONTROLS = { + "taps-off-by-one": (("unsigned long first = stream->center + 1 - RESAMPLER_ZERO_CROSSINGS;", + "unsigned long first = stream->center + 2 - RESAMPLER_ZERO_CROSSINGS;"), "reference"), + "phase-not-blended": (("for (phase = 0; phase <= RESAMPLER_TABLE_STEPS; phase++)", + "for (phase = 0; phase <= RESAMPLER_TABLE_STEPS; phase += 2)"), "impulse"), + "no-stretch": (("#define RESAMPLER_MAXIMUM_STRETCH 2", "#define RESAMPLER_MAXIMUM_STRETCH 1"), "aliases"), +} + + +def generated(source=None, fault=None): + """the mixer: dsound_sdl.c from its constants to the end of the mixing (before the output)""" + text = source if source is not None else read(SOURCE) + code = text[text.index("#define OUTPUT_RATE"):text.index("/* ---------- output")] + if fault: + code = mutated(code, *fault) + return (("under_test.inc", code),) + + +@pytest.mark.parametrize("case", CASES) +def test_case(case): + status, output = run(build("mixer", generated()), case) + assert status == 0, output + + +@pytest.mark.parametrize("control", NEGATIVE_CONTROLS) +def test_negative_control(control): + fault, case = NEGATIVE_CONTROLS[control] + status, output = run(build("mixer", generated(fault=fault)), case) + assert status == CHECK_FAILED, f"the mixer with a fault ({control}) passed '{case}': the test cannot see it" + + +if __name__ == "__main__": + import os + import subprocess + + # (MIXER_SOURCE: another dsound_sdl.c, to compare) + other = os.environ.get("MIXER_SOURCE") + executable = build("mixer", generated(Path(other).read_text(encoding="latin-1") if other else None)) + sys.exit(subprocess.run([str(executable), *(sys.argv[1:] or ["metrics"])]).returncode) From 81b198b8812f16c7aed982171ecc537b2eb24960 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Fri, 9 Oct 2026 00:51:21 -0400 Subject: [PATCH 085/187] Resample four taps at a time The mixer's audio thread spent about 70% of its time in the resampler's low pass (perf on a Steam Deck in b30 and d20): 48 taps a voice and output frame, each blending two phase rows, read one stereo frame at a time from a ring buffer and added one by one, for both channels even of a mono voice. Now each voice's history keeps each channel apart and each frame twice, RESAMPLER_HISTORY apart, so the frames a low pass reaches are side by side however the ring wraps; a table of each phase row's step to the next makes the blend a multiply and an add; the taps are summed four at a time in vector lanes (SSE on x86, NEON on ARM, the compiler's vector extensions, with a plain loop for any other compiler or with RESAMPLER_SCALAR); a mono voice works out one channel; and the voice's low pass states stay in locals while it is mixed rather than in the stream, which the output's stores could alias. The filter itself (24 zero crossings, cutoff 0.96, Kaiser 6.5) is unchanged. Audio thread CPU (per-thread /proc, % of one core): - Steam Deck, b30 with a bot (30 s): 14.7 -> 9.0, the baseline 15.6-15.8 over three 90 s runs; - desktop (Ryzen AI Max+ 395, the game under Xvfb, SDL's dummy audio driver, b30 with a bot, 90 s): 6.0 -> 2.0 and 5.5 -> 1.8; - the mixer bench (tools/harness/tests/test_mixer.py bench, 24 voices, 16 out of earshot, indoors): desktop x86 32-bit 753 -> 368 ns a frame, arm64 (Apple A18 Pro, NEON, as the Android build's) 696 -> 330 ns. Only the order of the additions changes. Built tap by tap (RESAMPLER_SCALAR) the mixer's output is bit for bit the old one's, for every test voice and a whole scene's mix (reverb and limiter); with vectors it differs from it by at most -126 dB of the peak (-132 to -138 dB RMS), float's rounding. Against the low pass worked out in double precision both are at -105 dB; a 22 kHz voice is flat to 0.001 dB to 8 kHz, -0.47 dB at 10 kHz, the rest 79 to 89 dB down; a 44.1 kHz voice the same to 20 kHz. test_mixer.py checks the vector and tap-by-tap mixers agree. --- port/linux/src/dsound_sdl.c | 151 +++++++++++++++++++++++++----- tools/harness/tests/mixer.c | 7 +- tools/harness/tests/test_mixer.py | 37 ++++++-- 3 files changed, 159 insertions(+), 36 deletions(-) diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 7f165488..0c6686ec 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -62,6 +62,11 @@ turns the reverb off; audio.enabled = false skips opening a device #define RESAMPLER_KAISER_BETA 6.5 #define RESAMPLER_MAXIMUM_STRETCH 2 #define RESAMPLER_HISTORY 128 +#if defined(__GNUC__) || defined(__clang__) +#define RESAMPLER_ALIGNED __attribute__((aligned(16))) +#else +#define RESAMPLER_ALIGNED +#endif /* ---------- voices */ @@ -121,9 +126,12 @@ struct sdl_stream /* the next frame to take from the first packet not finished */ unsigned long cursor; /* the resampler's (resampler_reset): the last RESAMPLER_HISTORY frames - taken, how many were taken, the frame the output is at and how far past - it, and the frames of silence taken since the packets ran out */ - float history[RESAMPLER_HISTORY][2]; + taken, each channel's apart and each frame twice, RESAMPLER_HISTORY apart, + so that the frames a low pass reaches are side by side however the ring + wraps (resampler_dot); how many were taken, the frame the output is at and + how far past it, and the frames of silence taken since the packets ran + out */ + float history[2][2 * RESAMPLER_HISTORY]; unsigned long history_count; unsigned long center; double phase; @@ -469,8 +477,11 @@ static float resampler_table[RESAMPLER_ZERO_CROSSINGS * RESAMPLER_TABLE_STEPS + to RESAMPLER_ZERO_CROSSINGS, at each of RESAMPLER_TABLE_STEPS phases between two source frames (and one row more, for the last's blend): a voice at the output rate or slower blends two rows by its phase, which is what the table gave tap -by tap, a third of the work */ -static float resampler_phases[RESAMPLER_TABLE_STEPS + 1][2 * RESAMPLER_ZERO_CROSSINGS]; +by tap, a third of the work. resampler_deltas holds each row's step to the +next, so the blend is a multiply and an add a tap */ +#define RESAMPLER_TAPS (2 * RESAMPLER_ZERO_CROSSINGS) +static float resampler_phases[RESAMPLER_TABLE_STEPS + 1][RESAMPLER_TAPS] RESAMPLER_ALIGNED; +static float resampler_deltas[RESAMPLER_TABLE_STEPS][RESAMPLER_TAPS] RESAMPLER_ALIGNED; static double bessel_i0(double x) { @@ -529,6 +540,77 @@ static void resampler_phases_initialize(void) resampler_phases[phase][tap] = resampler_weight(distance * RESAMPLER_TABLE_STEPS); } } + for (phase = 0; phase < RESAMPLER_TABLE_STEPS; phase++) + { + for (tap = 0; tap < RESAMPLER_TAPS; tap++) + resampler_deltas[phase][tap] = resampler_phases[phase + 1][tap] - resampler_phases[phase][tap]; + } +} + +/* The low pass over a voice's frames, the sum of each frame times its weight: +four taps at a time where the compiler has vectors (SSE, NEON), summed in four +lanes, then the lanes together. Only the order of the additions differs from +tap by tap, a difference of the order of float's rounding (-140 dB); +RESAMPLER_SCALAR adds them tap by tap (tools/harness/tests/test_mixer.py). */ +#if (defined(__GNUC__) || defined(__clang__)) && !defined(RESAMPLER_SCALAR) +typedef float resampler_vector __attribute__((vector_size(16))); +typedef float resampler_unaligned_vector __attribute__((vector_size(16), aligned(4))); +/* (each row of the phase tables whole vectors, aligned) */ +typedef char resampler_taps_in_fours[RESAMPLER_TAPS % 4 ? -1 : 1]; +#define RESAMPLER_LANES 4 +#else +#define RESAMPLER_LANES 1 +#endif + +/* frames[0, RESAMPLER_TAPS) weighted by the phase table's row blended blend of +the way to the next */ +static float resampler_dot(const float *frames, const float *weights, const float *deltas, float blend) +{ +#if RESAMPLER_LANES == 4 + resampler_vector sum = { 0.0f, 0.0f, 0.0f, 0.0f }, blends = { blend, blend, blend, blend }; + int tap; + + for (tap = 0; tap < RESAMPLER_TAPS; tap += 4) + { + resampler_vector weight = *(const resampler_vector *)(weights + tap) + + *(const resampler_vector *)(deltas + tap) * blends; + + sum += *(const resampler_unaligned_vector *)(frames + tap) * weight; + } + return (sum[0] + sum[1]) + (sum[2] + sum[3]); +#else + float sum = 0.0f; + int tap; + + for (tap = 0; tap < RESAMPLER_TAPS; tap++) + sum += frames[tap] * (weights[tap] + deltas[tap] * blend); + return sum; +#endif +} + +/* resampler_dot of two channels' frames, with the same weights */ +static void resampler_dot2(const float *left_frames, const float *right_frames, const float *weights, + const float *deltas, float blend, float *left, float *right) +{ +#if RESAMPLER_LANES == 4 + resampler_vector left_sum = { 0.0f, 0.0f, 0.0f, 0.0f }, right_sum = left_sum; + resampler_vector blends = { blend, blend, blend, blend }; + int tap; + + for (tap = 0; tap < RESAMPLER_TAPS; tap += 4) + { + resampler_vector weight = *(const resampler_vector *)(weights + tap) + + *(const resampler_vector *)(deltas + tap) * blends; + + left_sum += *(const resampler_unaligned_vector *)(left_frames + tap) * weight; + right_sum += *(const resampler_unaligned_vector *)(right_frames + tap) * weight; + } + *left = (left_sum[0] + left_sum[1]) + (left_sum[2] + left_sum[3]); + *right = (right_sum[0] + right_sum[1]) + (right_sum[2] + right_sum[3]); +#else + *left = resampler_dot(left_frames, weights, deltas, blend); + *right = resampler_dot(right_frames, weights, deltas, blend); +#endif } /* a voice starting (over): silence before its first frame, which the output @@ -591,6 +673,7 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns float target_left, target_right, target_room, left, right, room, ramp_left, ramp_right, ramp_room, scale; float target_direct_lowpass, target_room_lowpass, direct_lowpass, room_lowpass; float ramp_direct_lowpass, ramp_room_lowpass; + float direct_state[2], room_state; long width; unsigned long frame; /* (a stereo voice panned towards its sound: voice_gains) */ @@ -620,6 +703,11 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns room = stream->current_room; direct_lowpass = stream->current_direct_lowpass; room_lowpass = stream->current_room_lowpass; + /* the low passes' states, here while the mix writes the output (which + the compiler cannot tell from them) */ + direct_state[0] = stream->direct_lowpass[0]; + direct_state[1] = stream->direct_lowpass[1]; + room_state = stream->room_lowpass; ramp_left = (target_left - left) / (float)frames; ramp_right = (target_right - right) / (float)frames; ramp_room = (target_room - room) / (float)frames; @@ -635,17 +723,20 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns silence, until what was taken has played out */ while ((long)(stream->history_count - stream->center) <= width) { - float *slot = stream->history[stream->history_count % RESAMPLER_HISTORY]; + unsigned long slot = stream->history_count % RESAMPLER_HISTORY; + float taken[2]; - if (take_frame(stream, slot)) + if (take_frame(stream, taken)) { stream->silence = 0; } else { - slot[0] = slot[1] = 0.0f; + taken[0] = taken[1] = 0.0f; stream->silence++; } + stream->history[0][slot] = stream->history[0][slot + RESAMPLER_HISTORY] = taken[0]; + stream->history[1][slot] = stream->history[1][slot + RESAMPLER_HISTORY] = taken[1]; stream->history_count++; } if (stream->silence > (unsigned long)(2 * width)) @@ -661,46 +752,53 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns double position = stream->phase * RESAMPLER_TABLE_STEPS; unsigned long row = (unsigned long)position; float blend = (float)(position - (double)row); - const float *weights = resampler_phases[row], *next_weights = resampler_phases[row + 1]; - unsigned long first = stream->center + 1 - RESAMPLER_ZERO_CROSSINGS; + unsigned long first = (stream->center + 1 - RESAMPLER_ZERO_CROSSINGS) % RESAMPLER_HISTORY; - for (tap = 0; tap < 2 * RESAMPLER_ZERO_CROSSINGS; tap++) + /* (a mono voice's two channels are the same: take_frame) */ + if (stream->channels == 1) { - const float *source = stream->history[(first + (unsigned long)tap) % RESAMPLER_HISTORY]; - float weight = weights[tap] + (next_weights[tap] - weights[tap]) * blend; - - sample_left += source[0] * weight; - sample_right += source[1] * weight; + sample_left = sample_right = resampler_dot(stream->history[0] + first, resampler_phases[row], + resampler_deltas[row], blend); + } + else + { + resampler_dot2(stream->history[0] + first, stream->history[1] + first, resampler_phases[row], + resampler_deltas[row], blend, &sample_left, &sample_right); } } else { + unsigned long first = (stream->center + 1 - (unsigned long)width) % RESAMPLER_HISTORY; + const float *left_frames = stream->history[0] + first, *right_frames = stream->history[1] + first; + for (tap = 1 - width; tap <= width; tap++) { - const float *source = stream->history[(stream->center + (unsigned long)tap) % RESAMPLER_HISTORY]; float weight = scale * resampler_weight(fabsf((float)tap - (float)stream->phase) * scale * RESAMPLER_TABLE_STEPS); - sample_left += source[0] * weight; - sample_right += source[1] * weight; + sample_left += left_frames[tap + width - 1] * weight; + if (stream->channels == 2) + sample_right += right_frames[tap + width - 1] * weight; } + if (stream->channels == 1) + sample_right = sample_left; } /* the room send, with its own low pass */ if (room || ramp_room) { float mono = stream->channels == 1 ? sample_left : 0.5f * (sample_left + sample_right); - stream->room_lowpass = flush_denormal(mono + room_lowpass * (stream->room_lowpass - mono)); - send[frame] += stream->room_lowpass * room; + room_state = flush_denormal(mono + room_lowpass * (room_state - mono)); + send[frame] += room_state * room; } /* the direct path, muffled, or as it is; a mono voice's mix bins or pan split it across the speakers */ if (direct_lowpass || ramp_direct_lowpass) { - sample_left = flush_denormal(sample_left + direct_lowpass * (stream->direct_lowpass[0] - sample_left)); - sample_right = flush_denormal(sample_right + direct_lowpass * (stream->direct_lowpass[1] - sample_right)); + sample_left = flush_denormal(sample_left + direct_lowpass * (direct_state[0] - sample_left)); + sample_right = flush_denormal(sample_right + direct_lowpass * (direct_state[1] - sample_right)); } - stream->direct_lowpass[0] = sample_left; - stream->direct_lowpass[1] = sample_right; + direct_state[0] = sample_left; + direct_state[1] = sample_right; if (stream->channels == 1) { output[frame * 2] += sample_left * left; @@ -735,6 +833,9 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns stream->center++; } } + stream->direct_lowpass[0] = direct_state[0]; + stream->direct_lowpass[1] = direct_state[1]; + stream->room_lowpass = room_state; stream->current_left = target_left; stream->current_right = target_right; stream->current_room = target_room; diff --git a/tools/harness/tests/mixer.c b/tools/harness/tests/mixer.c index b51a9848..c337600c 100644 --- a/tools/harness/tests/mixer.c +++ b/tools/harness/tests/mixer.c @@ -13,9 +13,10 @@ signals: - a voice out of earshot, which is not resampled, moves on as one heard does, and is heard the same once it comes into earshot; - mono voices heard as stereo ones with both channels the same are. -"render" writes voices at each rate and a scene's whole mix as raw floats, -to compare one mixer with another; "metrics" prints the figures and "bench" -times a game's worth of voices. +"render" writes voices at each rate and a scene's whole mix as raw floats +(test_mixer.py compares the resampler adding four taps at a time with it +adding tap by tap), "metrics" prints the figures and "bench" times a game's +worth of voices. */ #include "harness.h" diff --git a/tools/harness/tests/test_mixer.py b/tools/harness/tests/test_mixer.py index a62571a1..63d95c45 100644 --- a/tools/harness/tests/test_mixer.py +++ b/tools/harness/tests/test_mixer.py @@ -4,7 +4,7 @@ double precision, sines through it as the ear hears them, voices out of earshot python tools/harness/tests/test_mixer.py metrics the figures python tools/harness/tests/test_mixer.py bench [voices] [out of earshot] [indoor] a game's worth of voices, timed python tools/harness/tests/test_mixer.py render > out.raw noise through the mixer, to compare with another's -(MIXER_SOURCE=path: of another dsound_sdl.c) +(MIXER_SOURCE=path: of another dsound_sdl.c; MIXER_SCALAR=1: its resampler adding tap by tap) """ import sys @@ -20,21 +20,32 @@ CASES = ["reference", "impulse", "sines", "aliases", "silent-voice", "mono"] # a fault in the mixer, and the case that must catch it NEGATIVE_CONTROLS = { - "taps-off-by-one": (("unsigned long first = stream->center + 1 - RESAMPLER_ZERO_CROSSINGS;", - "unsigned long first = stream->center + 2 - RESAMPLER_ZERO_CROSSINGS;"), "reference"), + "taps-off-by-one": (("(stream->center + 1 - RESAMPLER_ZERO_CROSSINGS) % RESAMPLER_HISTORY", + "(stream->center + 2 - RESAMPLER_ZERO_CROSSINGS) % RESAMPLER_HISTORY"), "reference"), "phase-not-blended": (("for (phase = 0; phase <= RESAMPLER_TABLE_STEPS; phase++)", "for (phase = 0; phase <= RESAMPLER_TABLE_STEPS; phase += 2)"), "impulse"), "no-stretch": (("#define RESAMPLER_MAXIMUM_STRETCH 2", "#define RESAMPLER_MAXIMUM_STRETCH 1"), "aliases"), } -def generated(source=None, fault=None): - """the mixer: dsound_sdl.c from its constants to the end of the mixing (before the output)""" +def generated(source=None, fault=None, scalar=False): + """the mixer: dsound_sdl.c from its constants to the end of the mixing (before the output), its resampler adding + tap by tap if scalar""" text = source if source is not None else read(SOURCE) code = text[text.index("#define OUTPUT_RATE"):text.index("/* ---------- output")] if fault: code = mutated(code, *fault) - return (("under_test.inc", code),) + return (("under_test.inc", ("#define RESAMPLER_SCALAR\n" if scalar else "") + code),) + + +def rendered(scalar): + """mixer.c's render: voices at each rate, then a scene's whole mix, as floats""" + import array + import subprocess + + result = subprocess.run([str(build("mixer", generated(scalar=scalar))), "render"], capture_output=True, + timeout=120, check=True) + return array.array("f", result.stdout) @pytest.mark.parametrize("case", CASES) @@ -43,6 +54,15 @@ def test_case(case): assert status == 0, output +def test_vectors_add_as_taps_do(): + """the resampler four taps at a time is the resampler tap by tap but for the order of its additions: the same + sound to float's rounding, through the whole mix (reverb and limiter)""" + vector, scalar = rendered(False), rendered(True) + assert len(vector) == len(scalar) > 0 + worst = max(abs(a - b) for a, b in zip(vector, scalar)) + assert 0 < worst < 1e-5, f"the vector resampler differs by {worst} (none: no vectors)" + + @pytest.mark.parametrize("control", NEGATIVE_CONTROLS) def test_negative_control(control): fault, case = NEGATIVE_CONTROLS[control] @@ -54,7 +74,8 @@ if __name__ == "__main__": import os import subprocess - # (MIXER_SOURCE: another dsound_sdl.c, to compare) + # (MIXER_SOURCE: another dsound_sdl.c, to compare; MIXER_SCALAR=1: its resampler tap by tap) other = os.environ.get("MIXER_SOURCE") - executable = build("mixer", generated(Path(other).read_text(encoding="latin-1") if other else None)) + executable = build("mixer", generated(Path(other).read_text(encoding="latin-1") if other else None, + scalar=bool(os.environ.get("MIXER_SCALAR")))) sys.exit(subprocess.run([str(executable), *(sys.argv[1:] or ["metrics"])]).returncode) From d80590c930bb73ec068f6392e3190b7a0598b331 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Fri, 9 Oct 2026 00:51:21 -0400 Subject: [PATCH 086/187] Limiter: look for the smallest gain only while limiting limit looked through its 64 frames ahead for the smallest gain they need on every output frame, even while nothing is loud enough to need any (the menus, most of a game). It now counts the frames that need a gain under 1; while there are none the smallest is 1 without looking. The output is bit for bit the same (test_mixer.py's scene, whose voices go over full scale). With no voices playing the mix costs 4.8 instead of 16.9 ns a frame on the desktop bench, 2.7 instead of 20.5 on arm64. --- port/linux/src/dsound_sdl.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 0c6686ec..70953f46 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -1156,6 +1156,9 @@ static struct /* the frames the output is delayed by, and the gain each needs */ float delay[LIMITER_LOOKAHEAD][OUTPUT_CHANNELS]; float needed[LIMITER_LOOKAHEAD]; + /* how many of them are under 1: none while nothing is too loud, when the + smallest is 1 without looking */ + unsigned long limiting; /* the gain held down to what the frames ahead need, coming back up */ float held; /* its last LIMITER_LOOKAHEAD values, and their sum */ @@ -1195,12 +1198,18 @@ static void limit(float *output, unsigned long frames) peak = fabsf(sample[channel]); limiter.delay[position][channel] = sample[channel]; } + limiter.limiting -= limiter.needed[position] < 1.0f; limiter.needed[position] = peak > LIMITER_CEILING ? LIMITER_CEILING / peak : 1.0f; - lowest = limiter.needed[0]; - for (index = 1; index < LIMITER_LOOKAHEAD; index++) + limiter.limiting += limiter.needed[position] < 1.0f; + lowest = 1.0f; + if (limiter.limiting) { - if (limiter.needed[index] < lowest) - lowest = limiter.needed[index]; + lowest = limiter.needed[0]; + for (index = 1; index < LIMITER_LOOKAHEAD; index++) + { + if (limiter.needed[index] < lowest) + lowest = limiter.needed[index]; + } } if (lowest < limiter.held) limiter.held = lowest; From e7e5777a8832fdae65fec7fd95358d3026efeb7a Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:55:00 +1300 Subject: [PATCH 087/187] Voice chat list in name tag colours The in-game list of who talks colours each name as the names above players' heads are (hud_player_name_color, now shared): allies in the HUD's blue, enemies red, this machine's own players white; in co-op every other player is an ally. --- source/game/game_engine.c | 19 ++++++++++++++++--- source/interface/hud.c | 26 ++++++++++++++++++++++---- source/interface/hud.h | 6 ++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 9a048cc6..09c442de 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -3876,7 +3876,9 @@ void game_engine_rasterize_message( /* port: who is talking in voice chat (network_voice.c), down the view's left from below its middle: each machine's first player's name after its speaker (this machine's own too, as it talks), VOICE_SPEAKERS_SCALE times -the HUD's text; while the scores are hidden */ +the HUD's text, in the colours of the names above players' heads (an ally's +or an enemy's: hud_player_name_color; in co-op every player an ally), this +machine's own white; while the scores are hidden */ #define VOICE_SPEAKERS_SCALE 0.8f static void game_engine_rasterize_voice_speakers( @@ -3897,6 +3899,7 @@ static void game_engine_rasterize_voice_speakers( short row = 0; short left; short top; + struct player_datum *viewer; if (font_index == NONE || !network_voice_available()) return; @@ -3910,6 +3913,8 @@ static void game_engine_rasterize_voice_speakers( left = (short)(bounds.x0 + 16); top = (short)(bounds.y0 + (bounds.y1 - bounds.y0) * 55 / 100); rasterizer_text_set_scale(VOICE_SPEAKERS_SCALE, (real)left, (real)top); + viewer = local_player_get_next(NONE) != NONE ? + player_try_and_get(local_player_get_player_index(local_player_get_next(NONE))) : NULL; data_iterator_new(&iterator, player_data); while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL && row < MAXIMUM_SPEAKER_ROWS) { @@ -3934,8 +3939,16 @@ static void game_engine_rasterize_voice_speakers( text.x1 = (short)(left + (bounds.x1 - left) / VOICE_SPEAKERS_SCALE); text.y0 = (short)(top + row * line_height); text.y1 = (short)(text.y0 + line_height); - color.alpha = 1.0f; - color.red = color.green = color.blue = 0.9f; + /* (allies as the names above heads have them: the same team) */ + if (player->local_player_index != NONE) + { + color.alpha = 1.0f; + color.red = color.green = color.blue = 1.0f; + } + else + { + hud_player_name_color(!game_engine || (viewer && player->team_index == viewer->team_index), &color); + } draw_string_set_draw_mode(font_index, NONE, 0, 0, &color); icon.x0 = left; icon.x1 = (short)(left + line_height * VOICE_SPEAKERS_SCALE); diff --git a/source/interface/hud.c b/source/interface/hud.c index d962840c..41485e7c 100644 --- a/source/interface/hud.c +++ b/source/interface/hud.c @@ -1105,6 +1105,27 @@ static real hud_player_name_enemy_range( return hud_globals ? hud_globals->defaults.motion_sensor_range : 0.0f; } +/* port: a player's name's colour above their head, an ally's (the HUD's +text) or an enemy's (red), whole (game_engine.c's list of who talks in +voice chat colours its names the same) */ +void hud_player_name_color( + boolean ally, + real_argb_color *color) +{ + if (ally) + { + hud_get_text_color(color); + color->alpha = 1.0f; + } + else + { + color->alpha = 1.0f; + color->red = 1.0f; + color->green = 0.3f; + color->blue = 0.25f; + } +} + static void hud_draw_player_name( long player_index, boolean ally, @@ -1160,18 +1181,15 @@ static void hud_draw_player_name( for (index = 0; index < (short)NUMBEROF(player->name); index++) name[index] = player->name[index]; name[NUMBEROF(player->name)] = 0; + hud_player_name_color(ally, &color); if (ally) { - hud_get_text_color(&color); /* (whole up to 15 world units away, then fading to 0.4 at 75) */ depth_factor = 1.0f - (-view_position.z - 15.0f) / 60.0f; color.alpha = PIN(depth_factor, 0.4f, 1.0f); } else { - color.red = 1.0f; - color.green = 0.3f; - color.blue = 0.25f; /* (whole up to four fifths of the range, then fading out) */ color.alpha = PIN((enemy_range - distance) / (0.2f * enemy_range), 0.0f, 1.0f); } diff --git a/source/interface/hud.h b/source/interface/hud.h index 2164b34e..c7f634d5 100644 --- a/source/interface/hud.h +++ b/source/interface/hud.h @@ -136,6 +136,12 @@ void hud_play_sound( word *played_flags, short maximum_sound_count); /* port: what sound_indices holds */ +/* port: a player's name's colour above their head: an ally's, or an +enemy's (hud.c) */ +void hud_player_name_color( + boolean ally, + union real_argb_color *color); + /* ---------- globals */ extern struct hud_globals_definition *hud_globals; From f5fc3d76ef91d8dd9c3f3794611b5b3b84d78ea1 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:00:38 +1300 Subject: [PATCH 088/187] Scoreboard speaker just right of the name --- source/game/game_engine.c | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 09c442de..0cb15bde 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -2345,8 +2345,8 @@ static void game_engine_rasterize_scoreboard( status_string, ping_string); row_color = has_teams ? &team_colors[PIN(player->team_index, 0, 1)] : &color; - /* port: a player talking (or muted) in voice chat, its speaker at the - right of the place column */ + /* port: a player talking (or muted) in voice chat, its speaker just + right of the name */ if (network && (network_voice_machine_speaking(player->network_player_data.machine_index) || network_voice_machine_muted(player->network_player_data.machine_index))) { @@ -2356,18 +2356,26 @@ static void game_engine_rasterize_scoreboard( short size; short middle; - /* (centred on the row's capitals: the middle measured as the row - is laid out, then scaled as it is drawn, as scoreboard_rectangle - has it) */ - text.x0 = row_left; - text.x1 = (short)(row_left + SCOREBOARD_COLUMN_WIDTH); + rectangle2d ink; + rectangle2d cursor; + short name_end; + + /* (after the name's last letter, centred on its capitals: both + measured as the row is laid out, the name at its column's tab + stop, then scaled as it is drawn, as scoreboard_rectangle has it; + within the name's column) */ + text.x0 = (short)(row_left + SCOREBOARD_PLACE_WIDTH); + text.x1 = (short)(text.x0 + SCOREBOARD_NAME_WIDTH); text.y0 = (short)(top + (2 + row) * line_height); text.y1 = (short)(text.y0 + line_height); draw_string_set_draw_mode(font_index, NONE, 0, 0, row_color); - middle = (short)(top + (draw_unicode_string_capital_middle(&text, L"H") - top) * SCOREBOARD_SCALE); - scoreboard_rectangle(&icon, bounds.x0, top, line_height, (short)(row_left + SCOREBOARD_PLACE_WIDTH - 24), - 20, 2 + row, 1); + draw_unicode_string_compute_bounds(&text, player->name, &ink, &cursor); + middle = (short)(top + (draw_unicode_string_capital_middle(&text, player->name) - top) * SCOREBOARD_SCALE); + scoreboard_rectangle(&icon, bounds.x0, top, line_height, text.x0, SCOREBOARD_NAME_WIDTH, 2 + row, 1); size = (short)((icon.y1 - icon.y0) * 2 / 3); + name_end = (short)(bounds.x0 + (MAX(ink.x1, text.x0) - bounds.x0) * SCOREBOARD_SCALE); + icon.x0 = (short)MIN(name_end + 4, icon.x1 - size); + icon.x1 = (short)(icon.x0 + size); icon.y0 = (short)(middle - size / 2); icon.y1 = (short)(icon.y0 + size); network_voice_draw_icon(&icon, network_voice_machine_muted(player->network_player_data.machine_index), From b22dacb026f7e9e50377d9f44f1e101b1c717559 Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Fri, 9 Oct 2026 01:51:37 -0400 Subject: [PATCH 089/187] Fix Profiles routing after creating a profile --- port/assets/menus/ce/main_menu.xml | 2 +- source/interface/virtual_keyboard.c | 27 +++-- tools/harness/tests/profile_keyboard.c | 116 +++++++++++++++++++ tools/harness/tests/test_profile_keyboard.py | 66 +++++++++++ 4 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 tools/harness/tests/profile_keyboard.c create mode 100644 tools/harness/tests/test_profile_keyboard.py diff --git a/port/assets/menus/ce/main_menu.xml b/port/assets/menus/ce/main_menu.xml index 63953084..7bed3d12 100644 --- a/port/assets/menus/ce/main_menu.xml +++ b/port/assets/menus/ce/main_menu.xml @@ -32,7 +32,7 @@ - + diff --git a/source/interface/virtual_keyboard.c b/source/interface/virtual_keyboard.c index d12181f7..aa2bafcc 100644 --- a/source/interface/virtual_keyboard.c +++ b/source/interface/virtual_keyboard.c @@ -347,6 +347,16 @@ static rectangle2d keyboard_rect[NUMBER_OF_VIRTUAL_KEYS] = static struct virtual_keyboard_globals virtual_keyboard_globals= {0}; +/* Keep the port's keyboard input mode in step with the keyboard's lifetime. +Processing stops once it closes, so the next process call cannot clear it. */ +static void virtual_keyboard_set_active(boolean active) +{ + extern void platform_text_typing(int typing); + + virtual_keyboard_globals.active = active; + platform_text_typing(active); +} + /* ---------- public code */ boolean virtual_keyboard_initialize( @@ -354,7 +364,7 @@ boolean virtual_keyboard_initialize( { long keyboard_index; - virtual_keyboard_globals.active = FALSE; + virtual_keyboard_set_active(FALSE); virtual_keyboard_globals.shift_active = FALSE; virtual_keyboard_globals.caps_active = FALSE; virtual_keyboard_globals.symbols_active = FALSE; @@ -399,7 +409,7 @@ boolean virtual_keyboard_initialize( void virtual_keyboard_dispose( void) { - virtual_keyboard_globals.active = FALSE; + virtual_keyboard_set_active(FALSE); virtual_keyboard_globals.shift_active = FALSE; virtual_keyboard_globals.caps_active = FALSE; virtual_keyboard_globals.symbols_active = FALSE; @@ -440,7 +450,7 @@ boolean virtual_keyboard_launch( event_manager_flush(); virtual_keyboard_globals.row = 0; virtual_keyboard_globals.column = 0; - virtual_keyboard_globals.active = TRUE; + virtual_keyboard_set_active(TRUE); virtual_keyboard_globals.text_buffer = text_buffer; virtual_keyboard_globals.cursor = text_buffer + ustrlen(text_buffer); virtual_keyboard_globals.buffer_size = buffer_size; @@ -553,7 +563,7 @@ static boolean virtual_keyboard_tab_down( static boolean virtual_keyboard_cancel( void) { - virtual_keyboard_globals.active = FALSE; + virtual_keyboard_set_active(FALSE); if (virtual_keyboard_globals.text_buffer) { ustrncpy( @@ -957,13 +967,6 @@ void virtual_keyboard_render( void virtual_keyboard_process( void) { - /* port: while it is up the computer's keyboard types into it - (port/linux/src/xinput_sdl.c) */ - { - extern void platform_text_typing(int typing); - - platform_text_typing(virtual_keyboard_globals.active); - } if (virtual_keyboard_globals.active) virtual_keyboard_process_internal(); @@ -1013,7 +1016,7 @@ static boolean virtual_keyboard_select( virtual_keyboard_globals.last_exit_saved_text = TRUE; } ui_play_audio_feedback_sound(_ui_audio_feedback_back); - virtual_keyboard_globals.active = FALSE; + virtual_keyboard_set_active(FALSE); event_manager_flush(); break; diff --git a/tools/harness/tests/profile_keyboard.c b/tools/harness/tests/profile_keyboard.c new file mode 100644 index 00000000..6a30b337 --- /dev/null +++ b/tools/harness/tests/profile_keyboard.c @@ -0,0 +1,116 @@ +/* Real virtual keyboard exits and keyboard mapping over an in-memory name. */ +#include "harness.h" +#include +typedef unsigned short word; +typedef int BOOL; +typedef unsigned char BYTE; +typedef short SHORT; +#include "config.inc" + +struct platform_input_state { unsigned char keys[TEST_SCANCODE_COUNT], mouse_buttons[8]; BOOL mouse_released; }; +typedef struct { unsigned short wButtons; BYTE bAnalogButtons[8]; SHORT sThumbLX, sThumbLY; } XINPUT_GAMEPAD; +#define SDL_BUTTON_X1 4 +static BOOL text_typing, text_typing_enter_armed, text_typing_keyboard, text_typing_field; +static struct { boolean active, shift_active, caps_active, symbols_active; + void *keyboard; short row, column; word buffer_size; short last_event, last_key, + number_of_event_repeats, caption_index; boolean last_exit_saved_text, first_key_replaces_buffer; + wchar_t *text_buffer, *cursor; unsigned long time_of_last_event; + long caret_bitmap_index; wchar_t saved_text[MAXIMUM_VIRTUAL_KEYBOARD_SAVED_TEXT_LENGTH]; +} virtual_keyboard_globals; +static boolean unique = TRUE, clean = TRUE, keyboard_available = TRUE; +static int processed, errors; +#define VIRTUAL_KEYBOARD_TAG 1 +#define BITMAP_GROUP_TAG 2 +#define tag_loaded(group, name) (keyboard_available ? 0 : NONE) +#define virtual_keyboard_definition_get(index) ((void *)1) +#define error(...) ((void)0) +#define _error_already_a_saved_game_file_with_that_name 1 +#define _error_cannot_create_saved_game_file_with_empty_name 2 +#define display_error(...) (++errors) +#define player_name_clean(buffer, count) (clean) +#define saved_game_file_name_unique(buffer) (unique) +#define event_manager_flush() ((void)0) +#define ui_play_audio_feedback_sound(sound) ((void)0) +#define system_milliseconds() 0UL +#define ustrlen wcslen +#define ustrncpy wcsncpy +#define ustrcmp wcscmp +#define ustrcpy wcscpy +#define csmemset memset +#define csmemmove memmove +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define virtual_keyboard_backspace() ((void)0) +#define virtual_keyboard_free_space_in_text_buffer() 0 +#define virtual_keyboard_get_current_character() L'a' +static void virtual_keyboard_process_internal(void) { ++processed; } +static boolean virtual_keyboard_cancel(void); +#include "under_test.inc" + +static void launch(wchar_t *name) +{ + CHECK(virtual_keyboard_initialize(), "keyboard initialization failed"); + CHECK(virtual_keyboard_launch(name, 12 * sizeof(wchar_t), 8), "keyboard launch failed"); + CHECK(text_typing, "launch did not immediately enable typing"); + /* The old code enabled typing only when the active keyboard processed. */ + virtual_keyboard_process(); + CHECK(text_typing && virtual_keyboard_globals.active, "keyboard did not own typing"); + struct platform_input_state input = {0}; XINPUT_GAMEPAD pad = {0}; + keyboard_gamepad(&input, &pad); /* let go of the Enter that opened it */ + input.keys[SDL_SCANCODE_RETURN] = 1; + keyboard_gamepad(&input, &pad); + CHECK(pad.wButtons & XINPUT_GAMEPAD_START, "typing Enter stopped choosing Done"); + CHECK(!pad.bAnalogButtons[XINPUT_GAMEPAD_A], "typing Enter selected a character"); +} + +static void ordinary_input(void) +{ + struct platform_input_state input = {0}; XINPUT_GAMEPAD pad = {0}; + CHECK(!virtual_keyboard_globals.active, "keyboard remained active"); + CHECK(!text_typing, "closed keyboard left typing mode enabled"); + input.keys[SDL_SCANCODE_RETURN] = 1; + input.keys[SDL_SCANCODE_W] = 1; + keyboard_gamepad(&input, &pad); + CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A] && !(pad.wButtons & XINPUT_GAMEPAD_START), + "menu Enter was not A after closing keyboard"); + CHECK(pad.sThumbLY == 32767, "keyboard movement still suppressed"); + memset(&pad, 0, sizeof(pad)); input.keys[SDL_SCANCODE_RETURN] = 0; + input.keys[SDL_SCANCODE_KP_ENTER] = 1; + keyboard_gamepad(&input, &pad); + CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A] && !(pad.wButtons & XINPUT_GAMEPAD_START), + "numpad Enter was not A"); +} + +int main(int argc, char **argv) +{ + const char *case_name = argc > 1 ? argv[1] : ""; + wchar_t name[12] = L"Probe"; + launch(name); + CASE("done") { virtual_keyboard_select(); CHECK(virtual_keyboard_globals.last_exit_saved_text, "Done lost the name"); } + else CASE("cancel") { wcscpy(name, L"Changed"); virtual_keyboard_cancel(); CHECK(!wcscmp(name,L"Probe"), "cancel did not restore name"); } + else CASE("external-close") { virtual_keyboard_close(); } + else CASE("dispose") { virtual_keyboard_dispose(); } + else CASE("initialize") { virtual_keyboard_initialize(); } + else CASE("invalid-name") { clean = FALSE; virtual_keyboard_select(); CHECK(errors==1 && !virtual_keyboard_globals.last_exit_saved_text, "invalid name accepted"); } + else CASE("duplicate-name") { unique = FALSE; wcscpy(name,L"Other"); virtual_keyboard_select(); CHECK(errors==1 && !virtual_keyboard_globals.last_exit_saved_text, "duplicate name accepted"); } + else CASE("field-owner") + { + platform_text_field(TRUE); virtual_keyboard_select(); + CHECK(!virtual_keyboard_globals.active && text_typing, "closing keyboard cleared text field ownership"); + platform_text_field(FALSE); + } + else CASE("reopen") + { + for (int i=0; i<20; ++i) { virtual_keyboard_select(); ordinary_input(); launch(name); } + virtual_keyboard_cancel(); + } + else CASE("launch-failure") + { + virtual_keyboard_dispose(); keyboard_available=FALSE; virtual_keyboard_initialize(); + CHECK(!virtual_keyboard_launch(name,sizeof(name),8), "missing keyboard launched"); + } + else CHECK(FALSE, "unknown case"); + ordinary_input(); + int before = processed; virtual_keyboard_process(); + CHECK(before == processed, "inactive keyboard processed input"); + return 0; +} diff --git a/tools/harness/tests/test_profile_keyboard.py b/tools/harness/tests/test_profile_keyboard.py new file mode 100644 index 00000000..93fa6d51 --- /dev/null +++ b/tools/harness/tests/test_profile_keyboard.py @@ -0,0 +1,66 @@ +"""Keyboard lifetime and actual Enter mapping, without maps or profile files.""" +import re +import sys +from pathlib import Path +import xml.etree.ElementTree as ET + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from harness import CHECK_FAILED, build, enum_with, function, mutated, read, run # noqa: E402 + +CASES = ['done', 'cancel', 'external-close', 'dispose', 'initialize', + 'invalid-name', 'duplicate-name', 'field-owner', 'reopen', 'launch-failure'] + + +def generated(fault=False): + vk = read('source/interface/virtual_keyboard.c') + inputs = read('port/linux/src/xinput_sdl.c') + config = '\n'.join(enum_with(vk, member) for member in + ['VIRTUAL_KEYBOARD_ROW_COUNT', '_vkey_done', '_ui_audio_feedback_none']) + config += '\n' + '\n'.join(re.findall(r'^#define XINPUT_GAMEPAD_\w+ [^\n]+', + read('port/include/xdk/xdk_xbox.h'), re.M)) + keyboard = function(inputs, 'keyboard_gamepad') + names = sorted(set(re.findall(r'SDL_SCANCODE_\w+', keyboard + function(inputs, 'typing_gamepad')))) + config += '\nenum {' + ','.join(names) + ', TEST_SCANCODE_COUNT};\n' + start = vk.index('static char const virtual_keyboard_layout_table') + config += vk[start:vk.index('\n};', start)+3] + '\n' + names = ['text_typing_update', 'platform_text_typing', 'platform_text_field', + 'analog', 'typing_gamepad', 'keyboard_gamepad'] + code = '\n'.join(function(inputs, name) for name in names) + '\n' + setter = function(vk, 'virtual_keyboard_set_active') + if fault: + setter = mutated(setter, 'platform_text_typing(active);', 'if (active) platform_text_typing(active);') + code += setter + '\n' + code += '\n'.join(function(vk, name) for name in + ['virtual_keyboard_initialize', 'virtual_keyboard_dispose', + 'virtual_keyboard_launch', 'virtual_keyboard_cancel', + 'virtual_keyboard_close', 'virtual_keyboard_select', 'virtual_keyboard_process']) + return (('config.inc', config), ('under_test.inc', code)) + + +@pytest.mark.parametrize('case', CASES) +def test_lifetime(case): + status, output = run(build('profile_keyboard', generated()), case) + assert status == 0, output + + +@pytest.mark.parametrize('case', ['done', 'cancel', 'external-close', 'dispose']) +def test_original_sticky_typing_detected(case): + status, output = run(build('profile_keyboard', generated(True)), case) + assert status == CHECK_FAILED, output + + +def test_profiles_accept_destinations(): + root = ET.fromstring(read('port/assets/menus/ce/main_menu.xml')) + widgets = {widget.get('name'): widget for widget in root.findall('widget')} + profile = widgets['main_menu/main_menu_item_profile'] + for event in ['a', 'start']: + handler = profile.find(f"on[@event='{event}']") + assert handler.get('open') == 'main_menu/profile_manager/player_profile_manager' + assert handler.get('run') == 'new game if no plyr profiles' + assert profile.find('conditional').get('widget') == 'main_menu/profile_manager/pm_entry_creating_profile' + multiplayer = widgets['main_menu/main_menu_item_multiplayer'] + for event in ['a', 'start']: + assert multiplayer.find(f"on[@event='{event}']").get('open') == \ + 'main_menu/multiplayer_type_select/multiplayer_type_select_screen' From 378cb0d4272499d412c4e29a8ac83b79d74bb247 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:52:31 +1300 Subject: [PATCH 090/187] Refused widget functions fail, resampler comment's figure A function a map's widget may not run (#203) failed as an invalid one does, so the handler opens and closes no screens after it. The four-taps resampler's difference from tap by tap (#221) is measured at -115 to -126 dB, not -140. --- port/linux/src/dsound_sdl.c | 2 +- source/interface/ui_widget_event_handler_functions.c | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 70953f46..515658eb 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -550,7 +550,7 @@ static void resampler_phases_initialize(void) /* The low pass over a voice's frames, the sum of each frame times its weight: four taps at a time where the compiler has vectors (SSE, NEON), summed in four lanes, then the lanes together. Only the order of the additions differs from -tap by tap, a difference of the order of float's rounding (-140 dB); +tap by tap, a difference of the order of float's rounding (-115 dB or less); RESAMPLER_SCALAR adds them tap by tap (tools/harness/tests/test_mixer.py). */ #if (defined(__GNUC__) || defined(__clang__)) && !defined(RESAMPLER_SCALAR) typedef float resampler_vector __attribute__((vector_size(16))); diff --git a/source/interface/ui_widget_event_handler_functions.c b/source/interface/ui_widget_event_handler_functions.c index 3e9ebbc0..b124ef8a 100644 --- a/source/interface/ui_widget_event_handler_functions.c +++ b/source/interface/ui_widget_event_handler_functions.c @@ -3449,9 +3449,11 @@ boolean ui_widget_event_handler_function_invoke( widget != NULL && widget_deleted != NULL, "(widget != NULL) && (widget_deleted != NULL)"); /* port: a map's own widgets (not the menus' tags the port adds) may not - run what changes the player's files or settings: ui_widget_function_denied */ + run what changes the player's files or settings: ui_widget_function_denied + (failed, as an invalid function is, so the handler opens and closes no + screens after it) */ if (ui_widget_function_denied(widget, function_index)) - return TRUE; + return FALSE; /* port: the menus' own functions (port/linux/game/menu_functions.c) */ if (function_index >= PC_MENU_FUNCTION_BASE && function_index < 0x8000) { From 051b3aa695d64b44afa5f58e8fc10cb3575ddec1 Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Fri, 9 Oct 2026 01:55:53 -0400 Subject: [PATCH 091/187] Suppress held Enter across typing mode changes --- port/linux/src/xinput_sdl.c | 7 ++++--- tools/harness/tests/profile_keyboard.c | 20 +++++++++++++++++++- tools/harness/tests/test_profile_keyboard.py | 14 +++++++++++--- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index 2e42ebb7..d1994f4a 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -305,12 +305,13 @@ the one that closes it pause the game again */ static void keys_held_over_switch(struct platform_input_state *input) { static unsigned char held[SDL_SCANCODE_COUNT]; - static int menus = -1; + static int context = -1; + int next_context = (input->menus != FALSE) | (text_typing ? 2 : 0); int scancode; - if (menus != (input->menus != FALSE)) + if (context != next_context) { - menus = input->menus != FALSE; + context = next_context; memcpy(held, input->keys, sizeof(held)); } for (scancode = 0; scancode < SDL_SCANCODE_COUNT; scancode++) diff --git a/tools/harness/tests/profile_keyboard.c b/tools/harness/tests/profile_keyboard.c index 6a30b337..5b33f50a 100644 --- a/tools/harness/tests/profile_keyboard.c +++ b/tools/harness/tests/profile_keyboard.c @@ -7,7 +7,9 @@ typedef unsigned char BYTE; typedef short SHORT; #include "config.inc" -struct platform_input_state { unsigned char keys[TEST_SCANCODE_COUNT], mouse_buttons[8]; BOOL mouse_released; }; +struct platform_input_state { unsigned char keys[TEST_SCANCODE_COUNT], mouse_buttons[8]; BOOL mouse_released, menus; }; +#define SDL_SCANCODE_COUNT TEST_SCANCODE_COUNT +#define console_is_active() FALSE typedef struct { unsigned short wButtons; BYTE bAnalogButtons[8]; SHORT sThumbLX, sThumbLY; } XINPUT_GAMEPAD; #define SDL_BUTTON_X1 4 static BOOL text_typing, text_typing_enter_armed, text_typing_keyboard, text_typing_field; @@ -108,6 +110,22 @@ int main(int argc, char **argv) virtual_keyboard_dispose(); keyboard_available=FALSE; virtual_keyboard_initialize(); CHECK(!virtual_keyboard_launch(name,sizeof(name),8), "missing keyboard launched"); } + else CASE("held-enter") + { + struct platform_input_state input = { .menus = TRUE }; XINPUT_GAMEPAD pad = {0}; + keys_held_over_switch(&input); + input.keys[SDL_SCANCODE_RETURN] = 1; + keys_held_over_switch(&input); keyboard_gamepad(&input,&pad); + CHECK(pad.wButtons & XINPUT_GAMEPAD_START, "Enter did not accept keyboard"); + virtual_keyboard_select(); + memset(&pad,0,sizeof(pad)); + keys_held_over_switch(&input); keyboard_gamepad(&input,&pad); + CHECK(!pad.wButtons && !pad.bAnalogButtons[XINPUT_GAMEPAD_A], "held Done also activated the next menu"); + input.keys[SDL_SCANCODE_RETURN] = 0; keys_held_over_switch(&input); + input.keys[SDL_SCANCODE_RETURN] = 1; keys_held_over_switch(&input); + keyboard_gamepad(&input,&pad); + CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A], "fresh Enter was still suppressed"); + } else CHECK(FALSE, "unknown case"); ordinary_input(); int before = processed; virtual_keyboard_process(); diff --git a/tools/harness/tests/test_profile_keyboard.py b/tools/harness/tests/test_profile_keyboard.py index 93fa6d51..25b41ec9 100644 --- a/tools/harness/tests/test_profile_keyboard.py +++ b/tools/harness/tests/test_profile_keyboard.py @@ -10,7 +10,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from harness import CHECK_FAILED, build, enum_with, function, mutated, read, run # noqa: E402 CASES = ['done', 'cancel', 'external-close', 'dispose', 'initialize', - 'invalid-name', 'duplicate-name', 'field-owner', 'reopen', 'launch-failure'] + 'invalid-name', 'duplicate-name', 'field-owner', 'reopen', 'launch-failure', 'held-enter'] def generated(fault=False): @@ -26,10 +26,13 @@ def generated(fault=False): start = vk.index('static char const virtual_keyboard_layout_table') config += vk[start:vk.index('\n};', start)+3] + '\n' names = ['text_typing_update', 'platform_text_typing', 'platform_text_field', - 'analog', 'typing_gamepad', 'keyboard_gamepad'] + 'analog', 'typing_gamepad', 'keyboard_gamepad', 'keys_held_over_switch'] code = '\n'.join(function(inputs, name) for name in names) + '\n' + if fault == 'held-enter': + code, count = re.subn(r' \| \(text_typing \? [24] : 0\)', '', code) + assert count == 1 setter = function(vk, 'virtual_keyboard_set_active') - if fault: + if fault is True: setter = mutated(setter, 'platform_text_typing(active);', 'if (active) platform_text_typing(active);') code += setter + '\n' code += '\n'.join(function(vk, name) for name in @@ -51,6 +54,11 @@ def test_original_sticky_typing_detected(case): assert status == CHECK_FAILED, output +def test_held_enter_guard_detected(): + status, output = run(build('profile_keyboard', generated('held-enter')), 'held-enter') + assert status == CHECK_FAILED, output + + def test_profiles_accept_destinations(): root = ET.fromstring(read('port/assets/menus/ce/main_menu.xml')) widgets = {widget.get('name'): widget for widget in root.findall('widget')} From a510083b4976ca5faafece15e0bde9f29c65e036 Mon Sep 17 00:00:00 2001 From: Mootjuh Date: Fri, 9 Oct 2026 06:14:35 +0200 Subject: [PATCH 092/187] Add a profiling build: CPU recordings for Perfetto and a report tool --- .github/workflows/build.yml | 2 +- README.md | 38 + configure.py | 13 +- port/linux/README.md | 2 + port/linux/game/network_distributed.c | 64 +- port/linux/game/network_objects.c | 18 +- port/linux/game/profile_console.c | 304 +++++ port/linux/game/profile_console.h | 25 + port/linux/include/profile_console_gametype.h | 30 + port/linux/include/profile_trace.h | 144 +++ port/linux/src/p2p.c | 60 + port/linux/src/port_config.c | 15 + port/linux/src/profile_json.c | 522 ++++++++ port/linux/src/profile_part.h | 97 ++ port/linux/src/profile_trace.c | 1006 ++++++++++++++++ source/cseries/profile.c | 113 ++ source/cseries/profile_sections.h | 31 + source/game/game.c | 71 +- source/game/game_time.c | 12 +- source/hs/hs.c | 9 + source/main/console.c | 7 + source/main/main.c | 51 +- tools/android_build.py | 4 +- tools/linux_build.py | 23 +- tools/net_report.py | 306 +++++ tools/profile_check.c | 1059 +++++++++++++++++ tools/profile_fixture.py | 66 + tools/profile_fixture.summary.txt | 35 + tools/test_profile.py | 569 +++++++++ tools/windows_build.py | 6 +- 30 files changed, 4640 insertions(+), 62 deletions(-) create mode 100644 port/linux/game/profile_console.c create mode 100644 port/linux/game/profile_console.h create mode 100644 port/linux/include/profile_console_gametype.h create mode 100644 port/linux/include/profile_trace.h create mode 100644 port/linux/src/profile_json.c create mode 100644 port/linux/src/profile_part.h create mode 100644 port/linux/src/profile_trace.c create mode 100644 source/cseries/profile_sections.h create mode 100644 tools/net_report.py create mode 100644 tools/profile_check.c create mode 100644 tools/profile_fixture.py create mode 100644 tools/profile_fixture.summary.txt create mode 100644 tools/test_profile.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7e092a14..07b4ffcf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: - name: Test run: | - python -m pytest -q tools/test_linux_port.py tools/harness + python -m pytest -q tools/test_linux_port.py tools/harness tools/test_profile.py python tools/test_light_storage.py - name: Compile cache diff --git a/README.md b/README.md index 2d9943fb..0bd435bb 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Give these options to `configure.py`: | `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. | | `--pgo=off` | No profile-guided optimization. | | `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". | +| `--profile` | A profiling build. It records CPU times. Refer to "Profiling builds". | Without `--portable`, the Linux and Windows builds use all the instructions of the processor that builds them (`-march=native`). Such a build does not @@ -138,3 +139,40 @@ To record a new profile: The build then plays the main menu and the first minute of each campaign level. This procedure continues for approximately 15 minutes. The game data must be in `assets/`. + +### Profiling builds + +A profiling build records where the game spends its time. + +1. Enter `python configure.py --profile`, then build as usual. +2. Start a recording. Enter `profile_record` in the console (or the telnet + console), or set `debug.profile_record = true` in `config.toml`. To + record each game of a session, also set + `debug.profile_record_when = "game"`. +3. Stop the recording. Enter `profile_stop`, load another map, or quit. + +The game writes numbered `.part.json` files under `profiles/` in its data root +and logs their paths. Parts are retained after the recording stops. Logs go to +standard error; a Windows release build without standard error writes them to +`halo.log` next to `halo.exe`. + +`tools/net_report.py` is a helper for developers. Enter +`python tools/net_report.py ` to read a recording +as tables. The command also writes `.summary.txt`, a short text +file. + +| Command | Result | +| --- | --- | +| `profile_record [seconds]` | Starts a recording. With a number of seconds, the recording stops after that time. | +| `profile_stop` | Stops the recording. | + +`debug.profile_memory` sets the memory of a recording in MB (default 256, +from 4 to 1024). A recording has no length limit: +the game writes a part each time the memory is full. Refer to "Settings" in +[port/linux/README.md](port/linux/README.md). + +On Android, the files are in `/sdcard/Android/data/com.halo.decomp/files/profiles/`. +Enter `adb pull` to copy the recording parts to a computer. + +A profiling build plays with normal builds. It does not use +`--pgo=train`. diff --git a/configure.py b/configure.py index 3bff193b..3e5d042c 100644 --- a/configure.py +++ b/configure.py @@ -12,7 +12,7 @@ from types import SimpleNamespace from tools import ninja_syntax from tools.android_build import android_configure_inputs, generate_android_build -from tools.linux_build import generate_linux_build, linux_configure_inputs +from tools.linux_build import check_profile_options, generate_linux_build, linux_configure_inputs from tools.windows_build import generate_windows_build, windows_configure_inputs # arguments @@ -33,6 +33,12 @@ parser.add_argument( action="store_true", help="release builds (Linux, Windows, Android): assertions are not checked", ) +parser.add_argument( + "--profile", + action="store_true", + help="profiling builds (Linux, Windows, Android): CPU scopes recorded on a " + "console command or launch setting (README, \"Profiling builds\"); not with --pgo=train", +) parser.add_argument( "--lto", choices=["full", "thin", "off"], @@ -74,6 +80,10 @@ parser.add_argument( help="clang with the arm64_32 target for the Android guest (default: clang)", ) args = parser.parse_args() +try: + check_profile_options(args.profile, args.pgo) +except ValueError as error: + parser.error(str(error)) # the settings the builds read sln = SimpleNamespace( @@ -81,6 +91,7 @@ sln = SimpleNamespace( linux_cc=args.linux_cc, compiler_launcher=args.compiler_launcher, port_release=args.release, + port_profile=args.profile, port_lto=args.lto, port_portable=args.portable, port_pgo=args.pgo, diff --git a/port/linux/README.md b/port/linux/README.md index adc6f996..4f98f3ef 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -287,6 +287,8 @@ the setting for one start of the game. It has priority over the file. | `debug.network_test`, `debug.network_test_start`, `debug.network_test_kill`, `debug.network_test_score`, `debug.network_test_shoot`, `debug.network_test_vehicle`, `debug.network_test_pickup`, `debug.network_test_pickup_weapon`, `debug.test_input` | off | `HALO_NETWORK_TEST`, `HALO_NETWORK_TEST_START`, `HALO_NETWORK_TEST_KILL`, `HALO_NETWORK_TEST_SCORE`, `HALO_NETWORK_TEST_SHOOT`, `HALO_NETWORK_TEST_VEHICLE`, `HALO_NETWORK_TEST_PICKUP`, `HALO_NETWORK_TEST_PICKUP_WEAPON`, `HALO_TEST_INPUT` | Automatic tests of system link (`game/network_test.c`). Refer to `NETCODE.md`. | | `debug.network_latency`, `debug.network_loss`, `debug.network_corrupt`, `debug.network_corrupt_stream`, `debug.network_corrupt_after` | `0` | `HALO_NETWORK_LATENCY`, `HALO_NETWORK_LOSS`, `HALO_NETWORK_CORRUPT`, `HALO_NETWORK_CORRUPT_STREAM`, `HALO_NETWORK_CORRUPT_AFTER` | The game holds all the data that it receives for this number of milliseconds, ignores this percentage of the datagrams, and damages this percentage of the datagrams it receives, and this percentage of its reads of streams, at random (bytes changed, cut short, stretched or replaced), from this many seconds after the start. Use the first two to test the netcode as on the internet, and the others to test that nothing another machine sends can crash the game (a damaged stream is closed, so a little goes a long way; a host's messages to its own client are damaged too, so start damaging once the game has started). | | `debug.telnet_console`, `debug.telnet_console_port` | `false`, `2323` | `HALO_TELNET_CONSOLE`, `HALO_TELNET_CONSOLE_PORT` | The game listens on 127.0.0.1, on this port, for a script console (connect with telnet). The console has no password, so only this computer can reach it. | +| `debug.profile_record`, `debug.profile_record_when`, `debug.profile_memory` | `false`, `"start"`, `256` | `HALO_PROFILE_RECORD`, `HALO_PROFILE_RECORD_WHEN`, `HALO_PROFILE_MEMORY` | Profiling builds only (`configure.py --profile`). Record without a command; `"start"` records from the first frame until the first map change, `"game"` records each game outside the main menu. Memory is 4–1024 MB. Numbered `profile__.part.json` files remain in `profiles/` after recording. Refer to "Profiling builds" in the main README. | + With Mesa drivers, the game sends its GL calls through the GL thread of Mesa. To stop this, set the environment variable `mesa_glthread=false`. diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index 24cc1048..5bd843f4 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -72,6 +72,7 @@ machine (their datum identifiers need not be). #include "units/bipeds.h" #include "network_coop.h" #include "network_distributed.h" +#include "cseries/profile_sections.h" #include #include @@ -457,6 +458,28 @@ struct distributed_packer static long distributed_last_sent_time = NONE; +/* the tick's steps, timed in the profiling build */ +PROFILE_SECTION(distributed_tick_apply_predictions_section, "network_distributed_tick.apply_predictions") +PROFILE_SECTION(distributed_tick_apply_vehicle_predictions_section, "network_distributed_tick.apply_vehicle_predictions") +PROFILE_SECTION(distributed_tick_objects_host_tick_section, "network_distributed_tick.objects_host_tick") +PROFILE_SECTION(distributed_tick_plan_players_section, "network_distributed_tick.plan_players") +PROFILE_SECTION(distributed_tick_damage_host_tick_section, "network_distributed_tick.damage_host_tick") +PROFILE_SECTION(distributed_tick_send_statistics_section, "network_distributed_tick.send_statistics") +PROFILE_SECTION(distributed_tick_send_pings_section, "network_distributed_tick.send_pings") +PROFILE_SECTION(distributed_tick_send_structure_bsp_section, "network_distributed_tick.send_structure_bsp") +PROFILE_SECTION(distributed_tick_send_players_section, "network_distributed_tick.send_players") +PROFILE_SECTION(distributed_tick_actors_host_tick_section, "network_distributed_tick.actors_host_tick") +PROFILE_SECTION(distributed_tick_coop_host_tick_section, "network_distributed_tick.coop_host_tick") +PROFILE_SECTION(distributed_tick_send_pickups_section, "network_distributed_tick.send_pickups") +PROFILE_SECTION(distributed_tick_send_game_state_section, "network_distributed_tick.send_game_state") +PROFILE_SECTION(distributed_tick_note_own_positions_section, "network_distributed_tick.note_own_positions") +PROFILE_SECTION(distributed_tick_send_inputs_section, "network_distributed_tick.send_inputs") +PROFILE_SECTION(distributed_tick_send_predictions_section, "network_distributed_tick.send_predictions") +PROFILE_SECTION(distributed_tick_objects_client_tick_section, "network_distributed_tick.objects_client_tick") +PROFILE_SECTION(distributed_tick_damage_client_tick_section, "network_distributed_tick.damage_client_tick") +PROFILE_SECTION(distributed_tick_coop_client_tick_section, "network_distributed_tick.coop_client_tick") +PROFILE_SECTION(distributed_tick_batches_flush_section, "network_distributed_tick.batches_flush") + /* how each player last died, by absolute index: the host's own, which it sends its clients, and a client's copy of the host's */ static struct distributed_death @@ -3331,40 +3354,41 @@ void network_distributed_tick( decided first: the damage goes where the players it hurt do), and a kill's statistics before the kill, so that a client announcing it counts it (a double kill, a killing spree) */ - distributed_apply_predictions(); - network_objects_apply_vehicle_predictions(); - network_objects_host_tick(); - distributed_host_plan_players(); - network_damage_host_tick(); + profile_scope(distributed_tick_apply_predictions_section, distributed_apply_predictions();) + profile_scope(distributed_tick_apply_vehicle_predictions_section, network_objects_apply_vehicle_predictions();) + profile_scope(distributed_tick_objects_host_tick_section, network_objects_host_tick();) + profile_scope(distributed_tick_plan_players_section, distributed_host_plan_players();) + profile_scope(distributed_tick_damage_host_tick_section, network_damage_host_tick();) if (distributed_statistics_due || game_time_get() % STATISTICS_INTERVAL_TICKS == 0) - distributed_send_statistics(game_time_get() % STATISTICS_REFRESH_TICKS == 0); + profile_scope(distributed_tick_send_statistics_section, + distributed_send_statistics(game_time_get() % STATISTICS_REFRESH_TICKS == 0);) distributed_statistics_due = FALSE; if (game_time_get() % PING_INTERVAL_TICKS == 0) - distributed_send_pings(); + profile_scope(distributed_tick_send_pings_section, distributed_send_pings();) /* (before the players, so clients load a new BSP before they hear where the host moved everyone into it) */ if (network_coop_active() && (global_structure_bsp_index_get() != distributed_sent_structure_bsp_index || game_time_get() % STRUCTURE_BSP_INTERVAL_TICKS == 0)) { - distributed_send_structure_bsp(); + profile_scope(distributed_tick_send_structure_bsp_section, distributed_send_structure_bsp();) } - distributed_host_send_players(); - network_actors_host_tick(); - network_coop_host_tick(); - distributed_send_pickups(); + profile_scope(distributed_tick_send_players_section, distributed_host_send_players();) + profile_scope(distributed_tick_actors_host_tick_section, network_actors_host_tick();) + profile_scope(distributed_tick_coop_host_tick_section, network_coop_host_tick();) + profile_scope(distributed_tick_send_pickups_section, distributed_send_pickups();) if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0) - distributed_send_game_state(NONE); + profile_scope(distributed_tick_send_game_state_section, distributed_send_game_state(NONE);) } else if (connection == _game_connection_network_client) { - distributed_note_own_positions(); - distributed_client_send_inputs(); - distributed_client_send_predictions(); - network_objects_client_tick(); - network_damage_client_tick(); - network_coop_client_tick(); + profile_scope(distributed_tick_note_own_positions_section, distributed_note_own_positions();) + profile_scope(distributed_tick_send_inputs_section, distributed_client_send_inputs();) + profile_scope(distributed_tick_send_predictions_section, distributed_client_send_predictions();) + profile_scope(distributed_tick_objects_client_tick_section, network_objects_client_tick();) + profile_scope(distributed_tick_damage_client_tick_section, network_damage_client_tick();) + profile_scope(distributed_tick_coop_client_tick_section, network_coop_client_tick();) } - distributed_batches_flush(); + profile_scope(distributed_tick_batches_flush_section, distributed_batches_flush();) distributed_machines.in_tick = FALSE; distributed_machines.valid = FALSE; } diff --git a/port/linux/game/network_objects.c b/port/linux/game/network_objects.c index 8ee2020e..5138a877 100644 --- a/port/linux/game/network_objects.c +++ b/port/linux/game/network_objects.c @@ -64,6 +64,7 @@ same datum index (identifier and all), so that any message can name one: #include "cutscene/cinematics.h" #include "network_coop.h" #include "network_distributed.h" +#include "cseries/profile_sections.h" #include @@ -1976,15 +1977,22 @@ void network_objects_apply_vehicle_predictions( } } +/* the host tick's steps, timed in the profiling build */ +PROFILE_SECTION(objects_host_tick_update_objects_section, "network_objects_host_tick.update_objects") +PROFILE_SECTION(objects_host_tick_find_viewers_section, "network_objects_host_tick.find_viewers") +PROFILE_SECTION(objects_host_tick_send_states_section, "network_objects_host_tick.send_states") +PROFILE_SECTION(objects_host_tick_send_inventories_section, "network_objects_host_tick.send_inventories") +PROFILE_SECTION(objects_host_tick_send_damage_animations_section, "network_objects_host_tick.send_damage_animations") + void network_objects_host_tick( void) { - distributed_host_update_objects(); - distributed_host_find_viewers(); - distributed_host_send_states(); + profile_scope(objects_host_tick_update_objects_section, distributed_host_update_objects();) + profile_scope(objects_host_tick_find_viewers_section, distributed_host_find_viewers();) + profile_scope(objects_host_tick_send_states_section, distributed_host_send_states();) if (game_time_get() % INVENTORY_INTERVAL_TICKS == 0) - distributed_host_send_inventories(); - distributed_host_send_damage_animations(); + profile_scope(objects_host_tick_send_inventories_section, distributed_host_send_inventories();) + profile_scope(objects_host_tick_send_damage_animations_section, distributed_host_send_damage_animations();) } /* ---------- a client */ diff --git a/port/linux/game/profile_console.c b/port/linux/game/profile_console.c new file mode 100644 index 00000000..7e0057c0 --- /dev/null +++ b/port/linux/game/profile_console.c @@ -0,0 +1,304 @@ +/* +PROFILE_CONSOLE.C + +Connect profiling commands and launch settings to game recordings. +*/ + +#ifdef HALO_PROFILE + +#include "cseries.h" +#include "game/game.h" +#include "game/game_engine.h" +#include "main/console.h" +#include "networking/network_game_globals.h" +#include "network_distributed.h" +#include "profile_console.h" +#include "profile_console_gametype.h" +#include "profile_trace.h" + +#include +#include +#include + +/* the platform layer's (platform.h, port_config.h), and main.c's */ +void platform_log(const char *format, ...); +const char *platform_data_root(void); +int posix_make_directory(const char *path); +void *posix_directory_open(const char *path); +void posix_directory_close(void *directory); +void p2p_profile_lock(void); +void p2p_profile_unlock(void); +int config_boolean(const char *name); +long config_integer(const char *name); +const char *config_string(const char *name); +boolean main_menu_is_loaded(void); + + +/* ---------- globals */ + +static boolean profile_console_installed; + +static void profile_console_map_component(const char *map, char *name, int size) +{ + const char *component = map; + const char *cursor; + int used = 0; + + for (cursor = map; *cursor; cursor++) + if (*cursor == '/' || *cursor == '\\') + component = cursor + 1; + for (cursor = component; *cursor && used + 1 < size; cursor++) + { + unsigned char character = (unsigned char)*cursor; + if (character >= 'A' && character <= 'Z') + character = (unsigned char)(character + ('a' - 'A')); + name[used++] = (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || + character == '_' || character == '-' ? (char)character : '-'; + } + name[used] = 0; +} + +static const char *profile_console_gametype(void) +{ + long engine; + + if (!game_in_progress() || main_menu_is_loaded() || game_connection() == _game_connection_film_playback) + return "none"; + if (!game_engine_running()) + return profile_console_engine_gametype(FALSE, 0); + engine = game_engine_get_variant()->game_engine_index; + return profile_console_engine_gametype(TRUE, engine); +} + +/* the map the header names: the one loading takes its place only after the +frame boundary that stops a recording on it (profile_console_frame) */ +static char profile_console_map[64]; +static char profile_console_next_map[64]; + +/* ---------- private code */ + +static void profile_console_log( + const char *text) +{ + platform_log("profile: %s", text); +} + +static void profile_console_notify( + const char *text) +{ + console_printf(FALSE, "%s", text); +} + +static int profile_console_ready( + void) +{ + return game_in_progress() && !main_menu_is_loaded(); +} + +static void profile_console_session( + struct profile_trace_session *session) +{ + short player_index; + const char *scenario = profile_console_scenario_map(profile_console_next_map, profile_console_map); + void *folder; + + csmemset(session, 0, sizeof(*session)); + snprintf(session->folder, sizeof(session->folder), "%s/profiles", platform_data_root()); + posix_make_directory(session->folder); + /* (a folder that can't be made must not lose the recording: the parts go + to the data root itself then) */ + folder = posix_directory_open(session->folder); + if (folder) + posix_directory_close(folder); + else + strncpy(session->folder, platform_data_root(), sizeof(session->folder) - 1); +#ifdef HALO_RELEASE + strcpy(session->build, "release profile"); +#else + strcpy(session->build, "debug profile"); +#endif + session->own_machine = -1; + switch (game_connection()) + { + case _game_connection_network_server: + strcpy(session->role, "host"); + break; + case _game_connection_network_client: + strcpy(session->role, "client"); + session->own_machine = network_game_client_get_local_machine_index(); + break; + default: + strcpy(session->role, "local"); + break; + } + strncpy(session->map, scenario, sizeof(session->map) - 1); + profile_console_map_component(scenario, session->map_name, sizeof(session->map_name)); + strncpy(session->gametype, profile_console_gametype(), sizeof(session->gametype) - 1); + for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++) + { + if (distributed_player(player_index)) + session->players++; + } + session->players_most = session->players; +} + +/* (once a second on the game thread: unlike the start-time description it +makes and opens no folder, which would land in the measured frame) */ +static void profile_console_sample_session( + struct profile_trace_session *session) +{ + short player_index; + const char *scenario = profile_console_scenario_map(profile_console_next_map, profile_console_map); + + if (!session->map_name[0] && scenario[0]) + { + strncpy(session->map, scenario, sizeof(session->map) - 1); + profile_console_map_component(scenario, session->map_name, sizeof(session->map_name)); + strncpy(session->gametype, profile_console_gametype(), sizeof(session->gametype) - 1); + } + for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++) + if (distributed_player(player_index)) + session->players++; +} + +/* the recording's seams. Not left to the first frame: init.txt's +profile_record is handled before it, and the recording's name (folder and +role) is chosen from the session when it is requested */ +static void profile_console_install( + void) +{ + if (profile_console_installed) + return; + profile_console_installed = TRUE; + profile_trace_set_log(profile_console_log); + profile_trace_set_notify(profile_console_notify); + profile_trace_set_ready(profile_console_ready); + profile_trace_set_session(profile_console_session); + profile_trace_set_session_sampler(profile_console_sample_session); + profile_trace_set_track_lock(p2p_profile_lock, p2p_profile_unlock); + /* (the window's close, debug.exit_after and XLaunchNewImage end the + process with exit(), which main_exit never sees) */ + atexit(profile_trace_shutdown); +} + +/* what follows the command word an expression starts with (past spaces and +an opening parenthesis, as the console and init.txt give it), else NULL */ +static char const *profile_console_word( + char const *expression, + char const *word) +{ + long length = (long)strlen(word); + long index; + + while (*expression == ' ' || *expression == '\t' || *expression == '(') + expression++; + for (index = 0; index < length; index++) + { + char character = expression[index] >= 'A' && expression[index] <= 'Z' ? expression[index] - 'A' + 'a' : expression[index]; + + if (character != word[index]) + return NULL; + } + expression += length; + if (*expression && *expression != ' ' && *expression != '\t' && *expression != ')') + return NULL; + while (*expression == ' ' || *expression == '\t') + expression++; + return expression; +} + +/* ---------- public code */ + +void profile_console_launch( + void) +{ + char const *when = config_string("debug.profile_record_when"); + + strcpy(profile_console_map, profile_console_next_map); + profile_console_install(); + profile_trace_thread_register(_profile_track_game); + if (strcmp(when, "start") != 0 && strcmp(when, "game") != 0) + platform_log("profile: debug.profile_record_when is \"%s\": \"start\" taken", when); + if (config_boolean("debug.profile_record")) + { + profile_trace_request_start(0.0, strcmp(when, "game") == 0 ? _profile_trace_when_game : _profile_trace_when_now, + config_integer("debug.profile_memory")); + } +} + +void profile_console_frame( + void) +{ + + strcpy(profile_console_map, profile_console_next_map); +} + +void profile_console_map_loaded( + char const *map_name) +{ + strncpy(profile_console_next_map, map_name ? map_name : "", sizeof(profile_console_next_map) - 1); + profile_trace_map_loaded(); +} + +/* Parse these before the script compiler: compiled map scripts index the +script function table, so profiling commands must not add entries to it. */ +boolean profile_console_command( + char const *expression) +{ + struct profile_trace_status status; + char const *rest; + + profile_console_install(); + if ((rest = profile_console_word(expression, "profile_record")) != NULL) + { + char *end; + double seconds = strtod(rest, &end); + + if (end == rest) + seconds = 0.0; + if ((*rest && end == rest) || seconds < 0.0 || + (*end && *end != ' ' && *end != '\t' && *end != ')')) + { + console_printf(FALSE, "profile: profile_record takes a number of seconds, 0 or more"); + return TRUE; + } + switch (profile_trace_request_start(seconds, _profile_trace_when_now, config_integer("debug.profile_memory"))) + { + case _profile_trace_answer_armed: + profile_trace_status(&status); + if (seconds > 0.0) + console_printf(FALSE, "profile: recording %s, %g s", status.name, seconds); + else + console_printf(FALSE, "profile: recording %s, until profile_stop", status.name); + break; + case _profile_trace_answer_still_writing: + profile_trace_status(&status); + console_printf(FALSE, "profile: still writing %s", status.name); + break; + default: + console_printf(FALSE, "profile: already recording"); + break; + } + return TRUE; + } + if (profile_console_word(expression, "profile_stop")) + { + profile_trace_status(&status); + switch (profile_trace_request_stop()) + { + case _profile_trace_answer_stopping: + console_printf(FALSE, "profile: writing %s, %ld parts", status.name, status.part); + break; + case _profile_trace_answer_disarmed: + console_printf(FALSE, "profile: not waiting for a game any more"); + break; + default: + console_printf(FALSE, "profile: not recording"); + break; + } + return TRUE; + } + return FALSE; +} + +#endif diff --git a/port/linux/game/profile_console.h b/port/linux/game/profile_console.h new file mode 100644 index 00000000..8502bc7f --- /dev/null +++ b/port/linux/game/profile_console.h @@ -0,0 +1,25 @@ +/* +PROFILE_CONSOLE.H + +Expose the game recording console hooks. +*/ + +#ifndef __PROFILE_CONSOLE_H +#define __PROFILE_CONSOLE_H +#pragma once + +#ifdef HALO_PROFILE + +/* profile_record [seconds], profile_stop: TRUE +when the expression was one of them (hs.c, before the script compiler) */ +boolean profile_console_command(char const *expression); +/* the first frame: the recording's seams, and the launch settings */ +void profile_console_launch(void); +/* each frame, right after the frame boundary */ +void profile_console_frame(void); +/* a map starts loading (game_load) */ +void profile_console_map_loaded(char const *map_name); + +#endif + +#endif diff --git a/port/linux/include/profile_console_gametype.h b/port/linux/include/profile_console_gametype.h new file mode 100644 index 00000000..259cb397 --- /dev/null +++ b/port/linux/include/profile_console_gametype.h @@ -0,0 +1,30 @@ +/* +PROFILE_CONSOLE_GAMETYPE.H + +Name the current scenario and running game engine for recordings. +*/ + +#ifndef __PROFILE_CONSOLE_GAMETYPE_H +#define __PROFILE_CONSOLE_GAMETYPE_H + +static inline const char *profile_console_scenario_map(const char *pending, const char *current) +{ + return pending && pending[0] ? pending : current; +} + +/* (engine indices none, ctf, slayer, oddball, king, race; the check program +cannot include game_engine.h. The variant outlives the engine into campaign +and co-op, so it is read only while an engine runs) */ +static inline const char *profile_console_engine_gametype(int running, long engine) +{ + static const char *const names[] = { "none", "ctf", "slayer", "oddball", "king", "race" }; + if (!running) + return "campaign"; + if (engine == 0) + return "none"; + if (engine >= 1 && engine <= 5) + return names[engine]; + return "none"; +} + +#endif diff --git a/port/linux/include/profile_trace.h b/port/linux/include/profile_trace.h new file mode 100644 index 00000000..324a95d2 --- /dev/null +++ b/port/linux/include/profile_trace.h @@ -0,0 +1,144 @@ +/* +PROFILE_TRACE.H + +Expose bounded CPU trace recording and session controls. +*/ + +#ifndef __PROFILE_TRACE_H +#define __PROFILE_TRACE_H + +#ifdef HALO_PROFILE + +/* included by C89 game units: keep declarations compatible with them. */ + +/* ---------- constants */ + +enum +{ + _profile_track_game = 0, + _profile_track_p2p, + NUMBER_OF_PROFILE_TRACKS, + + MAXIMUM_PROFILE_TRACE_NAMES = 512, + MAXIMUM_PROFILE_TRACE_DEPTH = 64, + MAXIMUM_PROFILE_TRACE_AGGREGATES = 32, + /* debug.profile_memory: both arenas together, megabytes */ + DEFAULT_PROFILE_TRACE_MEMORY = 256, + MINIMUM_PROFILE_TRACE_MEMORY = 4, + MAXIMUM_PROFILE_TRACE_MEMORY = 1024, +}; + +enum profile_trace_state +{ + _profile_trace_idle = 0, + _profile_trace_armed, + _profile_trace_recording, + _profile_trace_finishing, +}; + +/* when an armed recording starts: at the next frame, or once a game that is +not the main menu is in progress (debug.profile_record_when = "game") */ +enum profile_trace_when +{ + _profile_trace_when_now = 0, + _profile_trace_when_game, +}; + +enum profile_trace_stop_reason +{ + _profile_trace_stop_none = 0, + _profile_trace_stop_command, + _profile_trace_stop_seconds, + _profile_trace_stop_map_load, + _profile_trace_stop_exit, +}; + +enum profile_trace_answer +{ + _profile_trace_answer_armed = 0, + _profile_trace_answer_already_recording, + _profile_trace_answer_still_writing, + _profile_trace_answer_stopping, + _profile_trace_answer_disarmed, + _profile_trace_answer_not_recording, +}; + +/* ---------- structures */ + +/* (the recording's identity at start; players_most grows while it runs) */ +struct profile_trace_session +{ + char folder[260]; + char platform[16]; + char build[48]; + /* host, client or local */ + char role[8]; + /* a client's own machine index, -1 otherwise */ + long own_machine; + char map[64]; + char map_name[64]; + char gametype[16]; + long players; + long players_most; +}; + +struct profile_trace_status +{ + int state; + char name[64]; + long part; +}; + +/* ---------- prototypes/PROFILE_TRACE.C */ + +/* the seams: set once at start-up by the game (profile_console.c), and by +tools/profile_check.c with fakes */ +void profile_trace_set_clock(unsigned long long (*now_ns)(void)); +void profile_trace_set_track_lock(void (*lock)(void), void (*unlock)(void)); +void profile_trace_set_allocator(void *(*allocate)(unsigned long size), void (*release)(void *block)); +void profile_trace_set_log(void (*log)(const char *text)); +void profile_trace_set_notify(void (*notify)(const char *text)); +/* starts the writer of a new recording (profile_json_writer_start): 0 when +it cannot */ +void profile_trace_set_writer(int (*start)(void)); +/* whether a game that is not the main menu is in progress */ +void profile_trace_set_ready(int (*ready)(void)); +void profile_trace_set_session(void (*describe)(struct profile_trace_session *session)); +void profile_trace_set_session_sampler(void (*sample)(struct profile_trace_session *session)); + +/* which track the calling thread records on */ +void profile_trace_thread_register(int track); + +/* a name's id (the same name, the same id, for the whole run); an +aggregate name's scopes are summed per frame rather than recorded */ +int profile_trace_name(const char *name); +void profile_trace_name_aggregate(const char *name); + +void profile_trace_begin(int name); +void profile_trace_end(int name); + +/* the game thread, with no scope open: where a recording starts, is cut +and stops */ +void profile_trace_frame_boundary(void); +/* the game thread, at each game tick's start */ +void profile_trace_tick(void); + +/* the console's requests (the game thread), applied at the next frame +boundary; seconds 0 records until stopped */ +int profile_trace_request_start(double seconds, int when, long memory_megabytes); +int profile_trace_request_stop(void); +/* a map starts loading: a recording stops, but for the first load after +launch (the main menu, or init.txt's map) */ +void profile_trace_map_loaded(void); +/* the process ends: stops a recording, cuts its last part and waits for +the writer (main_exit, and atexit for the other ways out) */ +void profile_trace_shutdown(void); + +/* whether the tracks record now: changed under the track lock, so a p2p +pass sees one value */ +int profile_trace_recording(void); +void profile_trace_status(struct profile_trace_status *status); + +#endif + +#endif diff --git a/port/linux/src/p2p.c b/port/linux/src/p2p.c index 73f18275..d2ed6d6d 100644 --- a/port/linux/src/p2p.c +++ b/port/linux/src/p2p.c @@ -58,6 +58,9 @@ only look up and create stand-ins. #include "port_config.h" #include "p2p_internal.h" #include "ikcp.h" +#ifdef HALO_PROFILE +#include "profile_trace.h" +#endif #include #include @@ -276,6 +279,32 @@ struct stun_server pthread_mutex_t p2p_lock = PTHREAD_MUTEX_INITIALIZER; +#ifdef HALO_PROFILE +/* the p2p thread's own track of the profiling build's recording +(profile_trace.c): profile.c's sections are the game thread's alone */ +static struct +{ + int pass; + int tunnel_receive; + int kcp_update; + int streams; +} p2p_profile_names; + +/* the recording's track lock: the game thread flips the recording and its +arenas under it, and this thread records under it, so each record lands in +one arena; a pass that drops the lock (DNS, key exchange) can straddle a +cut, and profile_trace_end drops that scope */ +void p2p_profile_lock(void) +{ + pthread_mutex_lock(&p2p_lock); +} + +void p2p_profile_unlock(void) +{ + pthread_mutex_unlock(&p2p_lock); +} +#endif + static struct { int running; @@ -2824,6 +2853,13 @@ static void *p2p_thread(void *unused) static int read_owners[MAXIMUM_SOCKETS], write_owners[MAXIMUM_SOCKETS]; (void)unused; +#ifdef HALO_PROFILE + profile_trace_thread_register(_profile_track_p2p); + p2p_profile_names.pass = profile_trace_name("p2p.pass"); + p2p_profile_names.tunnel_receive = profile_trace_name("p2p.tunnel_receive"); + p2p_profile_names.kcp_update = profile_trace_name("p2p.kcp_update"); + p2p_profile_names.streams = profile_trace_name("p2p.streams"); +#endif pthread_mutex_lock(&p2p_lock); #ifndef HALO_ANDROID /* (here: it may wait for a program) */ @@ -2903,6 +2939,9 @@ static void *p2p_thread(void *unused) read_count = write_count = 0; } pthread_mutex_lock(&p2p_lock); +#ifdef HALO_PROFILE + profile_trace_begin(p2p_profile_names.pass); +#endif /* what is ready (the lists now hold only ready sockets, in the order asked, so each one's owner is found going along both); a socket @@ -2921,7 +2960,13 @@ static void *p2p_thread(void *unused) switch (owner & 255) { case _owner_tunnel: +#ifdef HALO_PROFILE + profile_trace_begin(p2p_profile_names.tunnel_receive); +#endif tunnel_readable(); +#ifdef HALO_PROFILE + profile_trace_end(p2p_profile_names.tunnel_receive); +#endif break; case _owner_handoff: if (socket == p2p.handoff_socket) @@ -2941,6 +2986,9 @@ static void *p2p_thread(void *unused) break; } } +#ifdef HALO_PROFILE + profile_trace_begin(p2p_profile_names.streams); +#endif for (index = 0, asked = 0; index < write_count; index++) { int socket = write[index]; @@ -2965,13 +3013,22 @@ static void *p2p_thread(void *unused) if (stream->used && stream->state == _stream_connecting && elapsed(stream->created_time, 5000)) stream_local_closed(stream); } +#ifdef HALO_PROFILE + profile_trace_end(p2p_profile_names.streams); +#endif p2p_signal_update(read, read_count, write, write_count); +#ifdef HALO_PROFILE + profile_trace_begin(p2p_profile_names.kcp_update); +#endif for (index = 0; index < MAXIMUM_STREAMS; index++) { if (p2p.streams[index].used) stream_update(&p2p.streams[index]); } +#ifdef HALO_PROFILE + profile_trace_end(p2p_profile_names.kcp_update); +#endif update_peers(); expire_proxies(); stun_update(); @@ -2999,6 +3056,9 @@ static void *p2p_thread(void *unused) p2p_discord_update(); #ifdef HALO_ANDROID poll_invite_file(); +#endif +#ifdef HALO_PROFILE + profile_trace_end(p2p_profile_names.pass); #endif } return NULL; diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 2d0d5f58..82695219 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -451,6 +451,21 @@ static const struct config_setting config_settings[] = { "debug.sample_seconds", _config_real, "0.0", "HALO_SAMPLE", _environment_value, _platform_android, "Log where every game thread is this often, in seconds (read by the\n" "app, port/android/host/host_debug.c); 0 never." }, +#ifdef HALO_PROFILE + { "debug.profile_record", _config_boolean, "false", "HALO_PROFILE_RECORD", _environment_value, _platform_all, + "Record a profile with no command (configure.py --profile builds): from\n" + "when profile_record_when says until profile_stop, a map change or the\n" + "end, into numbered part files in the data folder's profiles folder\n" + "(tools/net_report.py reads it)." }, + { "debug.profile_record_when", _config_string, "\"start\"", "HALO_PROFILE_RECORD_WHEN", _environment_value, + _platform_all, + "\"start\": from the first frame until the first map change; \"game\":\n" + "each game that is not the main menu, a recording each, until its map\n" + "goes." }, + { "debug.profile_memory", _config_integer, "256", "HALO_PROFILE_MEMORY", _environment_value, _platform_all, + "Megabytes a recording keeps in memory, 4 to 1024: two halves, each\n" + "written out as a part when it fills." }, +#endif }; #define NUMBER_OF_CONFIG_SETTINGS (sizeof(config_settings) / sizeof(config_settings[0])) diff --git a/port/linux/src/profile_json.c b/port/linux/src/profile_json.c new file mode 100644 index 00000000..5942eb38 --- /dev/null +++ b/port/linux/src/profile_json.c @@ -0,0 +1,522 @@ +/* +PROFILE_JSON.C + +Write recording parts on the writer thread. +*/ + +#ifdef HALO_PROFILE + +#include "profile_part.h" + +#include +#include +#include + +/* ---------- constants */ + +enum +{ + PROFILE_JSON_WORST_FRAMES = 10, + PROFILE_JSON_LONGEST_CHILDREN = 3, +}; + +/* ---------- structures */ + +/* where the next item of a list goes: a comma and a line before all but +the first */ +struct profile_json_list +{ + FILE *file; + int items; +}; + +struct profile_json_name_totals +{ + unsigned long long count; + unsigned long long total; + unsigned long long maximum; +}; + +/* ---------- globals */ + +static const char *profile_json_stop_reasons[] = { "", "command", "seconds", "map_load", "exit" }; + +/* ---------- private code */ + +static void profile_json_item( + struct profile_json_list *list) +{ + if (list->items++) + fputs(",\n", list->file); +} + +static void profile_json_escaped( + FILE *file, + const char *text) +{ + for (; text && *text; text++) + { + unsigned char character = (unsigned char)*text; + + if (character == '"' || character == '\\') + fprintf(file, "\\%c", character); + else if (character < 32 || character > 126) + fprintf(file, "\\u%04x", character); + else + fputc(character, file); + } +} + +static void profile_json_string( + FILE *file, + const char *text) +{ + fputc('"', file); + profile_json_escaped(file, text); + fputc('"', file); +} + +/* microseconds with three decimals, as the trace event format has them */ +static void profile_json_time( + FILE *file, + unsigned long long nanoseconds) +{ + fprintf(file, "%llu.%03llu", nanoseconds / 1000ULL, nanoseconds % 1000ULL); +} + +static int profile_json_compare_records( + const void *a, + const void *b) +{ + const struct profile_trace_record *first = a; + const struct profile_trace_record *second = b; + + if (first->start != second->start) + return first->start < second->start ? -1 : 1; + return (int)first->depth - (int)second->depth; +} + +static int profile_json_name_of( + const struct profile_part *part, + const char *text) +{ + int name; + + for (name = 0; name < part->name_count; name++) + { + if (strcmp(profile_trace_name_text(name), text) == 0) + return name; + } + return -1; +} + +static void profile_json_header( + const struct profile_part *part, + FILE *file) +{ + const struct profile_trace_session *session = &part->session; + + fputs("\"header\": {\"format\": 1, \"build\": ", file); + profile_json_string(file, session->build); + fputs(", \"platform\": ", file); + profile_json_string(file, session->platform); + fputs(", \"role\": ", file); + profile_json_string(file, session->role); + fprintf(file, ", \"own_machine\": %ld, \"map\": ", session->own_machine); + profile_json_string(file, session->map); + fputs(", \"map_name\": ", file); + profile_json_string(file, session->map_name); + fputs(", \"gametype\": ", file); + profile_json_string(file, session->gametype); + fprintf(file, ", \"players\": %ld, \"players_most\": %ld, \"start_utc\": ", session->players, + session->players_most); + profile_json_string(file, part->start_utc); + fputs(", \"recording\": ", file); + profile_json_string(file, part->name); + fprintf(file, ", \"part\": %ld, \"last_part\": %s" + ", \"first_frame\": %ld, \"frames\": %ld, \"first_tick\": %ld, \"ticks\": %ld", + part->number, part->last ? "true" : "false", + part->first_frame, part->frames, part->first_tick, part->ticks); + fprintf(file, ", \"start_s\": %.6f, \"duration_s\": %.6f, \"stop_reason\": ", + (double)part->start_ns / 1e9, (double)(part->end_ns - part->start_ns) / 1e9); + profile_json_string(file, profile_json_stop_reasons[part->stop_reason]); + fprintf(file, ", \"clock_read_ns\": %.1f, \"memory_used\": %lu, \"memory_limit\": %lu, \"writer_wait_ms\": %.3f" + ", \"foreign_scopes\": %lu, \"deep_scopes\": %lu, \"unbalanced_scopes\": %lu, \"dropped_scopes\": %lu}", + part->clock_read_ns, part->memory_used, part->memory_limit, part->writer_wait_ms, + part->foreign_scopes, part->deep_scopes, part->unbalanced_scopes, part->dropped_scopes); +} + +/* a worst frame's scopes: the open ones (one a depth, so no more than the +depth limit) and the three with the most time of their own so far */ +struct profile_json_ranking +{ + unsigned long scope[MAXIMUM_PROFILE_TRACE_DEPTH]; + unsigned long long children[MAXIMUM_PROFILE_TRACE_DEPTH]; + int open; + unsigned long longest[PROFILE_JSON_LONGEST_CHILDREN]; + unsigned int own[PROFILE_JSON_LONGEST_CHILDREN]; + int count; +}; + +/* the innermost open scope ends: its time less its children's is ranked, and +all of it is its parent's child time */ +static void profile_json_close( + struct profile_json_ranking *ranking, + const struct profile_trace_record *game) +{ + int open = --ranking->open; + unsigned long scope = ranking->scope[open]; + unsigned long long duration = game[scope].duration; + unsigned int own = (unsigned int)(duration > ranking->children[open] ? duration - ranking->children[open] : 0); + int slot; + + for (slot = ranking->count; slot > 0 && ranking->own[slot - 1] < own; slot--) + { + if (slot < PROFILE_JSON_LONGEST_CHILDREN) + { + ranking->longest[slot] = ranking->longest[slot - 1]; + ranking->own[slot] = ranking->own[slot - 1]; + } + } + if (slot < PROFILE_JSON_LONGEST_CHILDREN) + { + ranking->longest[slot] = scope; + ranking->own[slot] = own; + if (ranking->count < PROFILE_JSON_LONGEST_CHILDREN) + ranking->count++; + } + if (open > 0) + ranking->children[open - 1] += duration; +} + +static void profile_json_cpu_summary( + const struct profile_part *part, + FILE *file) +{ + struct profile_json_name_totals totals[MAXIMUM_PROFILE_TRACE_NAMES]; + const struct profile_trace_record *game = part->records[_profile_track_game]; + unsigned long game_count = part->record_counts[_profile_track_game]; + unsigned long worst[PROFILE_JSON_WORST_FRAMES]; + long worst_frame_number[PROFILE_JSON_WORST_FRAMES]; + int worst_count = 0; + struct profile_json_ranking ranking; + int frame_name = profile_json_name_of(part, "frame"); + int tick_name = profile_json_name_of(part, "game_tick"); + struct profile_json_list list; + unsigned long index; + long frames = 0; + int track, name; + + memset(totals, 0, sizeof(totals)); + for (track = 0; track < NUMBER_OF_PROFILE_TRACKS; track++) + { + for (index = 0; index < part->record_counts[track]; index++) + { + const struct profile_trace_record *record = &part->records[track][index]; + struct profile_json_name_totals *name_totals; + + if (record->name >= MAXIMUM_PROFILE_TRACE_NAMES) + continue; + name_totals = &totals[record->name]; + if (record->depth < MAXIMUM_PROFILE_TRACE_DEPTH || record->depth == PROFILE_TRACE_DEPTH_AGGREGATE_TIME) + { + name_totals->total += record->duration; + if (record->duration > name_totals->maximum) + name_totals->maximum = record->duration; + } + if (record->depth < MAXIMUM_PROFILE_TRACE_DEPTH) + name_totals->count++; + else if (record->depth == PROFILE_TRACE_DEPTH_AGGREGATE_COUNT) + name_totals->count += record->duration; + } + } + /* the worst frames: the game track is sorted, so a frame's children + follow it */ + for (index = 0; index < game_count; index++) + { + int slot; + + if (game[index].depth >= MAXIMUM_PROFILE_TRACE_DEPTH || game[index].name != frame_name) + continue; + frames++; + for (slot = worst_count; slot > 0 && game[worst[slot - 1]].duration < game[index].duration; slot--) + { + if (slot < PROFILE_JSON_WORST_FRAMES) + { + worst[slot] = worst[slot - 1]; + worst_frame_number[slot] = worst_frame_number[slot - 1]; + } + } + if (slot < PROFILE_JSON_WORST_FRAMES) + { + worst[slot] = index; + worst_frame_number[slot] = part->first_frame + frames - 1; + if (worst_count < PROFILE_JSON_WORST_FRAMES) + worst_count++; + } + } + + fprintf(file, "{\"frames\": %ld, \"ticks\": %ld, \"columns\": [\"name\", \"count\", \"total_ms\", " + "\"mean_ms\", \"max_ms\", \"per_frame\", \"per_tick_ms\"], \"rows\": [\n", frames, part->ticks); + list.file = file; + list.items = 0; + for (name = 0; name < part->name_count && name < MAXIMUM_PROFILE_TRACE_NAMES; name++) + { + const char *text = profile_trace_name_text(name); + struct profile_json_name_totals const *name_totals = &totals[name]; + int per_tick = strncmp(text, "game_tick", 9) == 0 || strncmp(text, "network_distributed_tick", 24) == 0; + + if (!name_totals->count) + continue; + profile_json_item(&list); + fputc('[', file); + profile_json_string(file, text); + fprintf(file, ", %llu, %.3f, %.4f, %.3f, %.3f, ", name_totals->count, name_totals->total / 1e6, + name_totals->total / 1e6 / (double)name_totals->count, name_totals->maximum / 1e6, + frames ? (double)name_totals->count / (double)frames : 0.0); + if (per_tick && part->ticks) + fprintf(file, "%.4f]", name_totals->total / 1e6 / (double)part->ticks); + else + fputs("null]", file); + } + fputs(list.items ? "\n]" : "]", file); + + fputs(", \"worst_frames\": {\"columns\": [\"frame\", \"at_s\", \"frame_ms\", \"ticks\", \"longest\"], \"rows\": [\n", file); + list.items = 0; + for (index = 0; index < (unsigned long)worst_count; index++) + { + const struct profile_trace_record *frame = &game[worst[index]]; + unsigned long long end = frame->start + frame->duration; + int ticks = 0; + unsigned long child; + int slot; + + /* (a tick sits under whatever the main loop wraps it in, so every + depth of the frame counts; the scopes are ranked by the time they have + of their own, or the parents, which hold all of it, would always come + first. The open scopes are a stack, so nothing is allocated for it) */ + memset(&ranking, 0, sizeof(ranking)); + for (child = worst[index] + 1; child < game_count && game[child].start < end; child++) + { + if (game[child].depth >= MAXIMUM_PROFILE_TRACE_DEPTH || game[child].depth <= frame->depth) + continue; + if (game[child].name == tick_name) + ticks++; + while (ranking.open && game[ranking.scope[ranking.open - 1]].depth >= game[child].depth) + profile_json_close(&ranking, game); + ranking.scope[ranking.open] = child; + ranking.children[ranking.open++] = 0; + } + while (ranking.open) + profile_json_close(&ranking, game); + profile_json_item(&list); + fprintf(file, "[%ld, %.3f, %.3f, %d, [", worst_frame_number[index], frame->start / 1e9, frame->duration / 1e6, + ticks); + for (slot = 0; slot < ranking.count; slot++) + { + fputs(slot ? ", [" : "[", file); + profile_json_string(file, profile_trace_name_text(game[ranking.longest[slot]].name)); + fprintf(file, ", %.3f]", ranking.own[slot] / 1e6); + } + fputs("]]", file); + } + fputs(list.items ? "\n]}}" : "]}}", file); +} + +static void profile_json_event( + FILE *file, + struct profile_json_list *list, + const struct profile_trace_record *record) +{ + const char *name = profile_trace_name_text(record->name); + + profile_json_item(list); + switch (record->depth) + { + case PROFILE_TRACE_DEPTH_AGGREGATE_TIME: + case PROFILE_TRACE_DEPTH_AGGREGATE_COUNT: + fputs("{\"ph\":\"C\",\"name\":\"", file); + profile_json_escaped(file, name); + fputs(record->depth == PROFILE_TRACE_DEPTH_AGGREGATE_TIME ? "_ms\",\"pid\":1,\"tid\":1,\"ts\":" : + "_count\",\"pid\":1,\"tid\":1,\"ts\":", file); + profile_json_time(file, record->start); + if (record->depth == PROFILE_TRACE_DEPTH_AGGREGATE_TIME) + fprintf(file, ",\"args\":{\"value\":%.4f}}", record->duration / 1e6); + else + fprintf(file, ",\"args\":{\"value\":%u}}", record->duration); + break; + default: + fputs("{\"ph\":\"X\",\"name\":", file); + profile_json_string(file, name); + fprintf(file, ",\"pid\":1,\"tid\":%d,\"ts\":", record->track + 1); + profile_json_time(file, record->start); + fputs(",\"dur\":", file); + profile_json_time(file, record->duration); + fputc('}', file); + break; + } +} + +/* ---------- public code */ + +int profile_json_write( + struct profile_part *part, + FILE *file) +{ + struct profile_json_list list; + unsigned long next[NUMBER_OF_PROFILE_TRACKS] = { 0, 0 }; + int track; + + for (track = 0; track < NUMBER_OF_PROFILE_TRACKS; track++) + { + if (part->record_counts[track]) + { + qsort(part->records[track], part->record_counts[track], sizeof(struct profile_trace_record), + profile_json_compare_records); + } + } + + fputs("{\"halo\": {\n", file); + profile_json_header(part, file); + fputs("},\n\"cpu_summary\": ", file); + profile_json_cpu_summary(part, file); + fputs(",\n\"traceEvents\": [\n", file); + + list.file = file; + list.items = 0; + profile_json_item(&list); + fprintf(file, "{\"ph\":\"M\",\"name\":\"process_name\",\"pid\":1,\"args\":{\"name\":\"halo %s %s\"}}", + part->session.role, part->session.platform); + profile_json_item(&list); + fputs("{\"ph\":\"M\",\"name\":\"thread_name\",\"pid\":1,\"tid\":1,\"args\":{\"name\":\"game\"}}", file); + profile_json_item(&list); + fputs("{\"ph\":\"M\",\"name\":\"thread_name\",\"pid\":1,\"tid\":2,\"args\":{\"name\":\"p2p\"}}", file); + if (part->number == 1) + { + profile_json_item(&list); + fputs("{\"ph\":\"i\",\"name\":\"recording_start\",\"s\":\"g\",\"pid\":1,\"tid\":1,\"ts\":0.000}", file); + } + /* the two tracks merged, in order of start and depth: each scope after + its parents */ + for (;;) + { + const struct profile_trace_record *game = next[0] < part->record_counts[0] ? &part->records[0][next[0]] : NULL; + const struct profile_trace_record *p2p = next[1] < part->record_counts[1] ? &part->records[1][next[1]] : NULL; + + if (!game && !p2p) + break; + if (game && (!p2p || profile_json_compare_records(game, p2p) <= 0)) + { + profile_json_event(file, &list, game); + next[0]++; + } + else + { + profile_json_event(file, &list, p2p); + next[1]++; + } + } + if (part->last) + { + profile_json_item(&list); + fputs("{\"ph\":\"i\",\"name\":\"recording_stop\",\"s\":\"g\",\"pid\":1,\"tid\":1,\"ts\":", file); + profile_json_time(file, part->end_ns); + fprintf(file, ",\"args\":{\"reason\":\"%s\"}}", profile_json_stop_reasons[part->stop_reason]); + } + fputs("\n],\n\"displayTimeUnit\": \"ms\"}\n", file); + return !ferror(file); +} + +void profile_json_choose_name( + const char *folder, + const char *stamp, + const char *role, + char *name, + int size) +{ + char path[512]; + int suffix; + + snprintf(name, (size_t)size, "profile_%s_%s", stamp, role); + for (suffix = 2; suffix < 1000; suffix++) + { + FILE *file; + int occupied = 0; + + snprintf(path, sizeof(path), "%s/%s.part1.json", folder, name); + file = fopen(path, "rb"); + if (file) + { + fclose(file); + occupied = 1; + } + if (!occupied) + break; + snprintf(name, (size_t)size, "profile_%s_%s_%d", stamp, role, suffix); + } +} + +int profile_json_write_part( + struct profile_part *part) +{ + char temporary[520]; + char path[512]; + FILE *file; + int written; + + snprintf(path, sizeof(path), "%s/%s.part%ld.json", part->folder, part->name, part->number); + snprintf(temporary, sizeof(temporary), "%s.tmp", path); + file = fopen(temporary, "wb"); + if (!file) + { + profile_trace_log("cannot write %s", temporary); + return 0; + } + written = profile_json_write(part, file); + if (fclose(file) != 0) + written = 0; + /* (Windows' rename does not replace a file: the recording's name was + chosen not to be there) */ + if (written && rename(temporary, path) != 0) + written = 0; + if (!written) + { + remove(temporary); + profile_trace_log("cannot write %s", path); + return 0; + } + profile_trace_log("wrote %s", path); + return 1; +} + +static void *profile_json_writer( + void *unused) +{ + (void)unused; + for (;;) + { + struct profile_part *part = profile_trace_writer_next(); + int last = part->last; + profile_json_write_part(part); + profile_trace_writer_done(part); + if (last) + break; + } + return NULL; +} + +int profile_json_writer_start( + void) +{ + pthread_attr_t attributes; + pthread_t thread; + int started; + + pthread_attr_init(&attributes); + pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED); + started = pthread_create(&thread, &attributes, profile_json_writer, NULL) == 0; + pthread_attr_destroy(&attributes); + return started; +} + +#endif diff --git a/port/linux/src/profile_part.h b/port/linux/src/profile_part.h new file mode 100644 index 00000000..d85c412b --- /dev/null +++ b/port/linux/src/profile_part.h @@ -0,0 +1,97 @@ +/* +PROFILE_PART.H + +Define frozen recording parts shared by the recorder and writer. +*/ + +#ifndef __PROFILE_PART_H +#define __PROFILE_PART_H + +#ifdef HALO_PROFILE + +#include "profile_trace.h" + +#include + +/* ---------- constants */ + +/* records that are not scopes, by their depth (a scope's is under +MAXIMUM_PROFILE_TRACE_DEPTH): an aggregate name's time and count over a +frame (start: the frame's) */ +enum +{ + PROFILE_TRACE_DEPTH_AGGREGATE_TIME = 255, + PROFILE_TRACE_DEPTH_AGGREGATE_COUNT = 254, +}; + +/* ---------- structures */ + +/* a scope that ended: 16 bytes */ +struct profile_trace_record +{ + /* nanoseconds since the recording started */ + unsigned long long start; + /* nanoseconds, saturating (or the counter's value) */ + unsigned int duration; + unsigned short name; + unsigned char depth; + unsigned char track; +}; + +struct profile_part +{ + char name[64]; + char folder[260]; + long number; + int last; + int stop_reason; + int arena; + struct profile_trace_record *records[NUMBER_OF_PROFILE_TRACKS]; + unsigned long record_counts[NUMBER_OF_PROFILE_TRACKS]; + unsigned long long start_ns; + unsigned long long end_ns; + long first_frame; + long frames; + long first_tick; + long ticks; + long name_count; + struct profile_trace_session session; + char start_utc[32]; + double clock_read_ns; + unsigned long memory_used; + unsigned long memory_limit; + double writer_wait_ms; + unsigned long foreign_scopes; + unsigned long deep_scopes; + unsigned long unbalanced_scopes; + unsigned long dropped_scopes; +}; + +typedef char profile_trace_record_size_assert[sizeof(struct profile_trace_record) == 16 ? 1 : -1]; + +/* ---------- prototypes/PROFILE_TRACE.C */ + +const char *profile_trace_name_text(int name); +int profile_trace_name_is_aggregate(int name); +/* the writer: the next part (waits for one), and a part written (its +arena back; after the last, both arenas freed and the recording over) */ +struct profile_part *profile_trace_writer_next(void); +/* a line in the log (profile_trace_set_log) */ +void profile_trace_log(const char *format, ...); +void profile_trace_writer_done(struct profile_part *part); + +/* ---------- prototypes/PROFILE_JSON.C */ + +/* a part as a Chrome trace with the "halo" and "cpu_summary" keys; 0 when +the file could not be written */ +int profile_json_write(struct profile_part *part, FILE *file); +/* Part names keep profile__. */ +void profile_json_choose_name(const char *folder, const char *stamp, const char *role, char *name, int size); +/* the writer thread of a recording: 0 when it cannot start */ +int profile_json_writer_start(void); +/* a part's file: /.part.json, written as .tmp and renamed */ +int profile_json_write_part(struct profile_part *part); + +#endif + +#endif diff --git a/port/linux/src/profile_trace.c b/port/linux/src/profile_trace.c new file mode 100644 index 00000000..7af3c511 --- /dev/null +++ b/port/linux/src/profile_trace.c @@ -0,0 +1,1006 @@ +/* +PROFILE_TRACE.C + +Record bounded CPU traces and hand frozen parts to the writer. +*/ + +#ifdef HALO_PROFILE + +#include "profile_part.h" + +#include +#include +#include +#include +#include +#include + +/* state changes happen only at the game frame boundary. The recording flag +and track regions change under the p2p lock. two arenas let the writer own +one frozen part while the game and p2p threads fill the other. */ + +/* ---------- constants */ + +enum +{ + /* a region is cut when no more than this share of it is left (a + sixteenth: what a frame adds must still fit) */ + PROFILE_TRACE_HEADROOM_SHIFT = 4, + PROFILE_TRACE_CLOCK_SAMPLES = 1000, +}; + +/* ---------- structures */ + +struct profile_trace_open +{ + unsigned long long start; + unsigned short name; + /* begun while recording: only those are written */ + unsigned char recorded; +}; + +struct profile_trace_track +{ + int index; + struct profile_trace_record *records; + unsigned long count; + unsigned long capacity; + struct profile_trace_open stack[MAXIMUM_PROFILE_TRACE_DEPTH]; + int depth; + int open_aggregates; + /* scopes begun past the stack's depth and not yet ended */ + int overflow; + /* the track's own counts (no other thread writes them) */ + unsigned long deep; + unsigned long unbalanced; + unsigned long dropped; +}; + +struct profile_trace_aggregate +{ + int name; + unsigned long long time; + unsigned long count; +}; + +/* ---------- prototypes */ + +static unsigned long long profile_trace_default_clock(void); +static void profile_trace_no_lock(void); +static void *profile_trace_default_allocate(unsigned long size); +static void profile_trace_default_log(const char *text); +static void profile_trace_no_notify(const char *text); +static int profile_trace_always_ready(void); +static void profile_trace_default_session(struct profile_trace_session *session); +static void profile_trace_default_session_sampler(struct profile_trace_session *session); +static void profile_trace_sample_session(void); + +/* ---------- globals */ + +static struct +{ + unsigned long long (*now)(void); + void (*lock)(void); + void (*unlock)(void); + void *(*allocate)(unsigned long size); + void (*release)(void *block); + void (*emit)(const char *text); + void (*notify)(const char *text); + int (*start_writer)(void); + int (*ready)(void); + void (*describe)(struct profile_trace_session *session); + void (*sample)(struct profile_trace_session *session); +} profile_trace_seams = +{ + profile_trace_default_clock, + profile_trace_no_lock, + profile_trace_no_lock, + profile_trace_default_allocate, + free, + profile_trace_default_log, + profile_trace_no_notify, + profile_json_writer_start, + profile_trace_always_ready, + profile_trace_default_session, + profile_trace_default_session_sampler, +}; + +/* the names, for the whole run (profile.c keeps their ids) */ +static pthread_mutex_t profile_trace_names_lock = PTHREAD_MUTEX_INITIALIZER; +static const char *profile_trace_names[MAXIMUM_PROFILE_TRACE_NAMES]; +static unsigned char profile_trace_aggregate_names[MAXIMUM_PROFILE_TRACE_NAMES]; +static int profile_trace_name_count; +static struct profile_trace_aggregate profile_trace_aggregates[MAXIMUM_PROFILE_TRACE_AGGREGATES]; +static int profile_trace_aggregate_count; +static int profile_trace_wait_name = -1; + +static struct profile_trace_track profile_trace_tracks[NUMBER_OF_PROFILE_TRACKS]; +static __thread struct profile_trace_track *profile_trace_thread_track; +/* scopes from threads with no track (any thread: atomically) */ +static unsigned long profile_trace_foreign_scopes; + +static struct +{ + int state; + /* stored by the writer alone, once it has freed the arenas */ + int finished; + int rearm; + int when; + double seconds; + long memory_megabytes; + int stop_reason; + int map_loaded; + int name_chosen; + /* the tracks append; changed under the track lock */ + int recording; + unsigned long long origin; + unsigned long long frame_start; + void *arenas[2]; + unsigned long arena_size; + unsigned long game_bytes; + unsigned long p2p_bytes; + struct profile_part parts[2]; + int current; + long part_number; + long frame; + long tick; + unsigned long long next_session_sample; + char name[64]; + struct profile_trace_session session; + char start_utc[32]; + double clock_read_ns; + double writer_wait_ms; + /* the hand-off with the writer: which arenas it holds, and the parts + queued for it */ + pthread_mutex_t writer_lock; + pthread_cond_t writer_changed; + int writer_holds[2]; + struct profile_part *queue[2]; + int queue_count; +} profile_trace_globals = +{ + .state = _profile_trace_idle, + .memory_megabytes = DEFAULT_PROFILE_TRACE_MEMORY, + .writer_lock = PTHREAD_MUTEX_INITIALIZER, + .writer_changed = PTHREAD_COND_INITIALIZER, +}; + +/* ---------- private code */ + +static unsigned long long profile_trace_default_clock(void) +{ + struct timespec now; + + clock_gettime(CLOCK_MONOTONIC, &now); + return (unsigned long long)now.tv_sec * 1000000000ULL + (unsigned long long)now.tv_nsec; +} + +static void profile_trace_no_lock(void) +{ +} + +static void *profile_trace_default_allocate(unsigned long size) +{ + return malloc(size); +} + +static void profile_trace_default_log(const char *text) +{ + fprintf(stderr, "profile: %s\n", text); +} + +static void profile_trace_no_notify(const char *text) +{ + (void)text; +} + +static int profile_trace_always_ready(void) +{ + return 1; +} + +static void profile_trace_default_session(struct profile_trace_session *session) +{ + memset(session, 0, sizeof(*session)); + strcpy(session->folder, "."); + strcpy(session->role, "local"); + session->own_machine = -1; + strcpy(session->gametype, "none"); +} + +static void profile_trace_default_session_sampler(struct profile_trace_session *session) +{ + (void)session; +} + +static void profile_trace_sample_session(void) +{ + struct profile_trace_session current; + unsigned long long now = profile_trace_seams.now(); + + if (now < profile_trace_globals.next_session_sample) + return; + profile_trace_globals.next_session_sample = now + 1000000000ULL; + memset(¤t, 0, sizeof(current)); + profile_trace_seams.sample(¤t); + if (!profile_trace_globals.session.map_name[0] && current.map_name[0]) + { + strcpy(profile_trace_globals.session.map, current.map); + strcpy(profile_trace_globals.session.map_name, current.map_name); + strcpy(profile_trace_globals.session.gametype, current.gametype); + } + if (current.players > profile_trace_globals.session.players_most) + profile_trace_globals.session.players_most = current.players; +} + +static const char *profile_trace_platform(void) +{ +#if defined(HALO_ANDROID) + return "android"; +#elif defined(_WIN32) + return "windows"; +#else + return "linux"; +#endif +} + +static int profile_trace_is_recording(void) +{ + return __atomic_load_n(&profile_trace_globals.recording, __ATOMIC_RELAXED); +} + +static void profile_trace_append( + struct profile_trace_track *track, + unsigned long long start, + unsigned long long duration, + int name, + int depth) +{ + struct profile_trace_record *record; + + if (!track->records || track->count >= track->capacity) + { + track->dropped++; + return; + } + record = &track->records[track->count]; + /* (the game thread reads the p2p track's count at the frame boundary, + without its lock) */ + __atomic_store_n(&track->count, track->count + 1, __ATOMIC_RELAXED); + record->start = start; + record->duration = duration > 0xFFFFFFFFULL ? 0xFFFFFFFFU : (unsigned int)duration; + record->name = (unsigned short)name; + record->depth = (unsigned char)depth; + record->track = (unsigned char)track->index; +} + +static int profile_trace_track_full( + struct profile_trace_track const *track) +{ + unsigned long count = __atomic_load_n(&track->count, __ATOMIC_RELAXED); + + return count + (track->capacity >> PROFILE_TRACE_HEADROOM_SHIFT) >= track->capacity; +} + +/* the arena's part n: the tracks write to its regions (under the track +lock: the p2p thread reads them) */ +static void profile_trace_open_part( + int arena, + unsigned long long now) +{ + struct profile_part *part = &profile_trace_globals.parts[arena]; + char *base = profile_trace_globals.arenas[arena]; + struct profile_trace_track *game = &profile_trace_tracks[_profile_track_game]; + struct profile_trace_track *p2p = &profile_trace_tracks[_profile_track_p2p]; + + memset(part, 0, sizeof(*part)); + part->arena = arena; + part->number = profile_trace_globals.part_number; + part->start_ns = now - profile_trace_globals.origin; + part->first_frame = profile_trace_globals.frame; + part->first_tick = profile_trace_globals.tick; + game->records = (struct profile_trace_record *)base; + game->capacity = profile_trace_globals.game_bytes / sizeof(struct profile_trace_record); + game->count = 0; + p2p->records = (struct profile_trace_record *)(base + profile_trace_globals.game_bytes); + p2p->capacity = profile_trace_globals.p2p_bytes / sizeof(struct profile_trace_record); + p2p->count = 0; + part->records[_profile_track_game] = game->records; + part->records[_profile_track_p2p] = p2p->records; + profile_trace_globals.current = arena; +} + +static void profile_trace_emit_aggregates( + void) +{ + struct profile_trace_track *game = &profile_trace_tracks[_profile_track_game]; + unsigned long long start = profile_trace_globals.frame_start - profile_trace_globals.origin; + int index; + + for (index = 0; index < profile_trace_aggregate_count; index++) + { + struct profile_trace_aggregate *aggregate = &profile_trace_aggregates[index]; + + if (!aggregate->count) + continue; + profile_trace_append(game, start, aggregate->time, aggregate->name, PROFILE_TRACE_DEPTH_AGGREGATE_TIME); + profile_trace_append(game, start, aggregate->count, aggregate->name, PROFILE_TRACE_DEPTH_AGGREGATE_COUNT); + aggregate->time = 0; + aggregate->count = 0; + } +} + +/* the current part frozen and queued for the writer: the last of the +recording, or the tracks go on in the other arena (once the writer has +given it back: the game thread waits for it here, and the wait is a +profile_wait scope in the new part) */ +static void profile_trace_cut( + int last, + int reason) +{ + struct profile_part *part = &profile_trace_globals.parts[profile_trace_globals.current]; + int next = 1 - profile_trace_globals.current; + unsigned long long now; + unsigned long long wait_start = 0; + unsigned long long waited = 0; + int track; + + profile_trace_emit_aggregates(); + /* (a map load that stops the recording already names the next map: a + recording that never learnt its map stays without one rather than take + that one) */ + if (!(last && reason == _profile_trace_stop_map_load)) + { + profile_trace_globals.next_session_sample = 0; + profile_trace_sample_session(); + } + if (!last) + { + pthread_mutex_lock(&profile_trace_globals.writer_lock); + if (profile_trace_globals.writer_holds[next]) + { + wait_start = profile_trace_seams.now(); + while (profile_trace_globals.writer_holds[next]) + pthread_cond_wait(&profile_trace_globals.writer_changed, &profile_trace_globals.writer_lock); + waited = profile_trace_seams.now() - wait_start; + } + pthread_mutex_unlock(&profile_trace_globals.writer_lock); + profile_trace_globals.writer_wait_ms += (double)waited / 1000000.0; + } + + profile_trace_seams.lock(); + /* (read under the lock, after the wait for the writer: the p2p thread appends to this + arena until here, and every record it appended ended before this time) */ + now = profile_trace_seams.now(); + for (track = 0; track < NUMBER_OF_PROFILE_TRACKS; track++) + { + struct profile_trace_track *source = &profile_trace_tracks[track]; + + part->record_counts[track] = source->count; + part->deep_scopes += source->deep; + part->unbalanced_scopes += source->unbalanced; + part->dropped_scopes += source->dropped; + source->deep = source->unbalanced = source->dropped = 0; + } + if (last) + { + __atomic_store_n(&profile_trace_globals.recording, 0, __ATOMIC_RELAXED); + for (track = 0; track < NUMBER_OF_PROFILE_TRACKS; track++) + { + profile_trace_tracks[track].records = NULL; + profile_trace_tracks[track].capacity = 0; + profile_trace_tracks[track].count = 0; + } + } + else + { + profile_trace_globals.part_number++; + profile_trace_open_part(next, now); + } + profile_trace_seams.unlock(); + + part->last = last; + part->stop_reason = last ? reason : _profile_trace_stop_none; + part->end_ns = now - profile_trace_globals.origin; + part->frames = profile_trace_globals.frame - part->first_frame; + part->ticks = profile_trace_globals.tick - part->first_tick; + strcpy(part->name, profile_trace_globals.name); + strcpy(part->folder, profile_trace_globals.session.folder); + part->session = profile_trace_globals.session; + strcpy(part->session.platform, profile_trace_platform()); + strcpy(part->start_utc, profile_trace_globals.start_utc); + part->clock_read_ns = profile_trace_globals.clock_read_ns; + part->memory_used = (unsigned long)(part->record_counts[0] + part->record_counts[1]) * + sizeof(struct profile_trace_record); + part->memory_limit = profile_trace_globals.arena_size; + part->writer_wait_ms = profile_trace_globals.writer_wait_ms; + part->foreign_scopes = __atomic_exchange_n(&profile_trace_foreign_scopes, 0, __ATOMIC_RELAXED); + pthread_mutex_lock(&profile_trace_names_lock); + part->name_count = profile_trace_name_count; + pthread_mutex_unlock(&profile_trace_names_lock); + + if (!last && waited) + { + profile_trace_append(&profile_trace_tracks[_profile_track_game], wait_start - profile_trace_globals.origin, + now - wait_start, profile_trace_wait_name, 0); + } + + pthread_mutex_lock(&profile_trace_globals.writer_lock); + profile_trace_globals.writer_holds[part->arena] = 1; + profile_trace_globals.queue[profile_trace_globals.queue_count++] = part; + pthread_cond_broadcast(&profile_trace_globals.writer_changed); + pthread_mutex_unlock(&profile_trace_globals.writer_lock); + + if (last) + { + profile_trace_globals.state = _profile_trace_finishing; + profile_trace_log("writing %s, %ld parts", profile_trace_globals.name, profile_trace_globals.part_number); + } +} + +/* the recording's name, from the time and the machine's role: when it is +asked for (profile_record answers with it), or when a game starts */ +static void profile_trace_choose_name( + void) +{ + time_t wall = time(NULL); + struct tm *utc = gmtime(&wall); + char stamp[32]; + + profile_trace_seams.describe(&profile_trace_globals.session); + if (utc) + { + strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", utc); + strftime(profile_trace_globals.start_utc, sizeof(profile_trace_globals.start_utc), "%Y-%m-%d %H:%M:%S", utc); + } + else + { + strcpy(stamp, "00000000-000000"); + strcpy(profile_trace_globals.start_utc, ""); + } + profile_json_choose_name(profile_trace_globals.session.folder, stamp, profile_trace_globals.session.role, + profile_trace_globals.name, sizeof(profile_trace_globals.name)); + profile_trace_globals.session.players_most = profile_trace_globals.session.players; + profile_trace_globals.name_chosen = 1; +} + +static void profile_trace_start_recording( + void) +{ + unsigned long long memory = (unsigned long long)profile_trace_globals.memory_megabytes * 1024 * 1024; + unsigned long size = (unsigned long)(memory / 2) & ~63UL; + unsigned long scope_bytes = size; + unsigned long long now; + int track, sample; + + if (!profile_trace_globals.name_chosen) + profile_trace_choose_name(); + profile_trace_globals.name_chosen = 0; + profile_trace_globals.arenas[0] = profile_trace_seams.allocate(size); + profile_trace_globals.arenas[1] = profile_trace_globals.arenas[0] ? profile_trace_seams.allocate(size) : NULL; + if (!profile_trace_globals.arenas[1]) + { + char text[96]; + + if (profile_trace_globals.arenas[0]) + profile_trace_seams.release(profile_trace_globals.arenas[0]); + profile_trace_globals.arenas[0] = NULL; + snprintf(text, sizeof(text), "profile: no memory for %ld MB", profile_trace_globals.memory_megabytes); + profile_trace_log("%s", text + 9); + profile_trace_seams.notify(text); + profile_trace_globals.state = _profile_trace_idle; + profile_trace_globals.rearm = 0; + return; + } + profile_trace_globals.arena_size = size; + /* the scopes: seven eighths the game track's, an eighth + the p2p track's (whole records each) */ + profile_trace_globals.game_bytes = scope_bytes / 8 * 7 / sizeof(struct profile_trace_record) * + sizeof(struct profile_trace_record); + profile_trace_globals.p2p_bytes = (scope_bytes - profile_trace_globals.game_bytes) / + sizeof(struct profile_trace_record) * sizeof(struct profile_trace_record); + + __atomic_store_n(&profile_trace_foreign_scopes, 0, __ATOMIC_RELAXED); + for (track = 0; track < profile_trace_aggregate_count; track++) + { + profile_trace_aggregates[track].time = 0; + profile_trace_aggregates[track].count = 0; + } + profile_trace_globals.frame = 0; + profile_trace_globals.tick = 0; + profile_trace_globals.part_number = 1; + profile_trace_globals.writer_wait_ms = 0.0; + profile_trace_globals.stop_reason = _profile_trace_stop_none; + profile_trace_globals.writer_holds[0] = profile_trace_globals.writer_holds[1] = 0; + profile_trace_globals.queue_count = 0; + __atomic_store_n(&profile_trace_globals.finished, 0, __ATOMIC_RELAXED); + + now = profile_trace_seams.now(); + for (sample = 0; sample < PROFILE_TRACE_CLOCK_SAMPLES; sample++) + profile_trace_seams.now(); + profile_trace_globals.clock_read_ns = (double)(profile_trace_seams.now() - now) / PROFILE_TRACE_CLOCK_SAMPLES; + + now = profile_trace_seams.now(); + profile_trace_globals.origin = now; + profile_trace_globals.frame_start = now; + profile_trace_seams.lock(); + /* (under the lock: a p2p pass of the last recording may end, and count, meanwhile) */ + for (track = 0; track < NUMBER_OF_PROFILE_TRACKS; track++) + { + profile_trace_tracks[track].deep = 0; + profile_trace_tracks[track].unbalanced = 0; + profile_trace_tracks[track].dropped = 0; + } + profile_trace_open_part(0, now); + profile_trace_seams.unlock(); + if (!profile_trace_seams.start_writer()) + { + profile_trace_seams.lock(); + for (track = 0; track < NUMBER_OF_PROFILE_TRACKS; track++) + profile_trace_tracks[track].records = NULL; + profile_trace_seams.unlock(); + profile_trace_seams.release(profile_trace_globals.arenas[0]); + profile_trace_seams.release(profile_trace_globals.arenas[1]); + profile_trace_globals.arenas[0] = profile_trace_globals.arenas[1] = NULL; + profile_trace_log("the writer thread cannot start: not recording"); + profile_trace_seams.notify("profile: the writer thread cannot start"); + profile_trace_globals.state = _profile_trace_idle; + profile_trace_globals.rearm = 0; + return; + } + profile_trace_seams.lock(); + __atomic_store_n(&profile_trace_globals.recording, 1, __ATOMIC_RELAXED); + profile_trace_seams.unlock(); + profile_trace_globals.state = _profile_trace_recording; + profile_trace_log("recording %s", profile_trace_globals.name); +} + +/* ---------- public code */ + +void profile_trace_set_clock( + unsigned long long (*now_ns)(void)) +{ + profile_trace_seams.now = now_ns ? now_ns : profile_trace_default_clock; +} + +void profile_trace_set_track_lock( + void (*lock)(void), + void (*unlock)(void)) +{ + profile_trace_seams.lock = lock && unlock ? lock : profile_trace_no_lock; + profile_trace_seams.unlock = lock && unlock ? unlock : profile_trace_no_lock; +} + +void profile_trace_set_allocator( + void *(*allocate)(unsigned long size), + void (*release)(void *block)) +{ + profile_trace_seams.allocate = allocate && release ? allocate : profile_trace_default_allocate; + profile_trace_seams.release = allocate && release ? release : free; +} + +void profile_trace_set_log( + void (*log)(const char *text)) +{ + profile_trace_seams.emit = log ? log : profile_trace_default_log; +} + +void profile_trace_set_notify( + void (*notify)(const char *text)) +{ + profile_trace_seams.notify = notify ? notify : profile_trace_no_notify; +} + +void profile_trace_set_writer( + int (*start)(void)) +{ + profile_trace_seams.start_writer = start ? start : profile_json_writer_start; +} + +void profile_trace_set_ready( + int (*ready)(void)) +{ + profile_trace_seams.ready = ready ? ready : profile_trace_always_ready; +} + +void profile_trace_set_session( + void (*describe)(struct profile_trace_session *session)) +{ + profile_trace_seams.describe = describe ? describe : profile_trace_default_session; +} + +void profile_trace_set_session_sampler( + void (*sample)(struct profile_trace_session *session)) +{ + profile_trace_seams.sample = sample ? sample : profile_trace_default_session_sampler; +} + +void profile_trace_log( + const char *format, + ...) +{ + char text[512]; + va_list arguments; + + va_start(arguments, format); + vsnprintf(text, sizeof(text), format, arguments); + va_end(arguments); + profile_trace_seams.emit(text); +} + +void profile_trace_thread_register( + int track) +{ + if (track < 0 || track >= NUMBER_OF_PROFILE_TRACKS) + return; + profile_trace_tracks[track].index = track; + profile_trace_thread_track = &profile_trace_tracks[track]; +} + +int profile_trace_name( + const char *name) +{ + int index; + + if (!name) + return -1; + pthread_mutex_lock(&profile_trace_names_lock); + for (index = 0; index < profile_trace_name_count; index++) + { + if (strcmp(profile_trace_names[index], name) == 0) + break; + } + if (index == profile_trace_name_count) + { + if (index < MAXIMUM_PROFILE_TRACE_NAMES) + profile_trace_names[profile_trace_name_count++] = name; + else + index = -1; + } + pthread_mutex_unlock(&profile_trace_names_lock); + return index; +} + +void profile_trace_name_aggregate( + const char *name) +{ + int id = profile_trace_name(name); + + if (id < 0 || profile_trace_aggregate_names[id] || + profile_trace_aggregate_count >= MAXIMUM_PROFILE_TRACE_AGGREGATES) + { + return; + } + profile_trace_aggregates[profile_trace_aggregate_count].name = id; + profile_trace_aggregates[profile_trace_aggregate_count].time = 0; + profile_trace_aggregates[profile_trace_aggregate_count].count = 0; + profile_trace_aggregate_count++; + profile_trace_aggregate_names[id] = 1; +} + +const char *profile_trace_name_text( + int name) +{ + const char *text = "unknown"; + + pthread_mutex_lock(&profile_trace_names_lock); + if (name >= 0 && name < profile_trace_name_count) + text = profile_trace_names[name]; + pthread_mutex_unlock(&profile_trace_names_lock); + return text; +} + +int profile_trace_name_is_aggregate( + int name) +{ + return name >= 0 && name < MAXIMUM_PROFILE_TRACE_NAMES && profile_trace_aggregate_names[name]; +} + +void profile_trace_begin( + int name) +{ + struct profile_trace_track *track = profile_trace_thread_track; + struct profile_trace_open *open; + + if (!track) + { + if (profile_trace_is_recording()) + __atomic_fetch_add(&profile_trace_foreign_scopes, 1, __ATOMIC_RELAXED); + return; + } + if (name < 0) + return; + if (track->depth >= MAXIMUM_PROFILE_TRACE_DEPTH) + { + track->overflow++; + if (profile_trace_is_recording()) + track->deep++; + return; + } + open = &track->stack[track->depth++]; + open->name = (unsigned short)name; + open->recorded = (unsigned char)profile_trace_is_recording(); + open->start = open->recorded ? profile_trace_seams.now() : 0; + if (profile_trace_aggregate_names[name]) + track->open_aggregates++; +} + +void profile_trace_end( + int name) +{ + struct profile_trace_track *track = profile_trace_thread_track; + struct profile_trace_open *open; + unsigned long long duration; + + if (!track || name < 0) + return; + if (track->overflow) + { + track->overflow--; + return; + } + if (!track->depth || track->stack[track->depth - 1].name != name) + { + if (profile_trace_is_recording()) + track->unbalanced++; + return; + } + open = &track->stack[--track->depth]; + if (profile_trace_aggregate_names[name] && track->open_aggregates > 0) + track->open_aggregates--; + if (!open->recorded) + return; + /* (a scope that outlived its recording: the p2p thread lets go of its + lock for slow work, and a stop can come meanwhile) */ + if (!profile_trace_is_recording() || !track->records || open->start < profile_trace_globals.origin) + { + track->dropped++; + return; + } + duration = profile_trace_seams.now() - open->start; + if (profile_trace_aggregate_names[name]) + { + int index; + + if (track->index != _profile_track_game) + return; + for (index = 0; index < profile_trace_aggregate_count; index++) + { + if (profile_trace_aggregates[index].name == name) + { + profile_trace_aggregates[index].time += duration; + profile_trace_aggregates[index].count++; + break; + } + } + return; + } + { + /* (an aggregated parent leaves no record, so depth counts only the + recorded ancestors) */ + int depth = track->depth - track->open_aggregates; + profile_trace_append(track, open->start - profile_trace_globals.origin, duration, name, depth); + } +} + +void profile_trace_frame_boundary( + void) +{ + struct profile_trace_track *game = &profile_trace_tracks[_profile_track_game]; + unsigned long long now; + + /* (a scope the frame left open: its end is lost, and so is it) */ + if (game->depth || game->overflow) + { + if (profile_trace_is_recording()) + game->unbalanced += (unsigned long)(game->depth + game->overflow); + game->depth = 0; + game->open_aggregates = 0; + game->overflow = 0; + } + if (profile_trace_wait_name < 0) + profile_trace_wait_name = profile_trace_name("profile_wait"); + switch (profile_trace_globals.state) + { + case _profile_trace_armed: + if (profile_trace_globals.when == _profile_trace_when_now || profile_trace_seams.ready()) + profile_trace_start_recording(); + break; + case _profile_trace_recording: + now = profile_trace_seams.now(); + if (now >= profile_trace_globals.next_session_sample) + profile_trace_sample_session(); + profile_trace_emit_aggregates(); + profile_trace_globals.frame++; + profile_trace_globals.frame_start = now; + if (!profile_trace_globals.stop_reason && profile_trace_globals.seconds > 0.0 && + (double)(now - profile_trace_globals.origin) >= profile_trace_globals.seconds * 1000000000.0) + { + profile_trace_globals.stop_reason = _profile_trace_stop_seconds; + } + if (profile_trace_globals.stop_reason) + { + profile_trace_cut(1, profile_trace_globals.stop_reason); + } + else if (profile_trace_track_full(&profile_trace_tracks[_profile_track_game]) || + profile_trace_track_full(&profile_trace_tracks[_profile_track_p2p])) + { + profile_trace_cut(0, _profile_trace_stop_none); + } + break; + case _profile_trace_finishing: + if (__atomic_load_n(&profile_trace_globals.finished, __ATOMIC_ACQUIRE)) + { + __atomic_store_n(&profile_trace_globals.finished, 0, __ATOMIC_RELAXED); + profile_trace_globals.state = profile_trace_globals.rearm ? _profile_trace_armed : _profile_trace_idle; + } + break; + default: + break; + } +} + +void profile_trace_tick( + void) +{ + if (profile_trace_globals.state != _profile_trace_recording) + return; + profile_trace_globals.tick++; +} + +int profile_trace_request_start( + double seconds, + int when, + long memory_megabytes) +{ + switch (profile_trace_globals.state) + { + case _profile_trace_armed: + case _profile_trace_recording: + return _profile_trace_answer_already_recording; + case _profile_trace_finishing: + return _profile_trace_answer_still_writing; + default: + break; + } + if (memory_megabytes < MINIMUM_PROFILE_TRACE_MEMORY) + memory_megabytes = MINIMUM_PROFILE_TRACE_MEMORY; + if (memory_megabytes > MAXIMUM_PROFILE_TRACE_MEMORY) + memory_megabytes = MAXIMUM_PROFILE_TRACE_MEMORY; + profile_trace_globals.seconds = seconds > 0.0 ? seconds : 0.0; + profile_trace_globals.when = when; + profile_trace_globals.memory_megabytes = memory_megabytes; + profile_trace_globals.rearm = when == _profile_trace_when_game; + profile_trace_globals.stop_reason = _profile_trace_stop_none; + profile_trace_globals.state = _profile_trace_armed; + profile_trace_globals.name_chosen = 0; + if (when == _profile_trace_when_now) + profile_trace_choose_name(); + return _profile_trace_answer_armed; +} + +int profile_trace_request_stop( + void) +{ + switch (profile_trace_globals.state) + { + case _profile_trace_armed: + profile_trace_globals.state = _profile_trace_idle; + profile_trace_globals.rearm = 0; + return profile_trace_globals.when == _profile_trace_when_game ? _profile_trace_answer_disarmed : + _profile_trace_answer_not_recording; + case _profile_trace_recording: + profile_trace_globals.rearm = 0; + if (!profile_trace_globals.stop_reason) + profile_trace_globals.stop_reason = _profile_trace_stop_command; + return _profile_trace_answer_stopping; + case _profile_trace_finishing: + if (profile_trace_globals.rearm) + { + profile_trace_globals.rearm = 0; + return _profile_trace_answer_disarmed; + } + return _profile_trace_answer_not_recording; + default: + return _profile_trace_answer_not_recording; + } +} + +void profile_trace_map_loaded( + void) +{ + if (!profile_trace_globals.map_loaded) + { + profile_trace_globals.map_loaded = 1; + return; + } + if (profile_trace_globals.state == _profile_trace_recording && !profile_trace_globals.stop_reason) + profile_trace_globals.stop_reason = _profile_trace_stop_map_load; +} + +void profile_trace_shutdown( + void) +{ + struct profile_trace_track *game = &profile_trace_tracks[_profile_track_game]; + + /* (exit called on another thread: the game thread may be mid-frame, + writing to the arena; the part being recorded is lost, as when the + process is killed) */ + if (profile_trace_thread_track != game) + return; + profile_trace_globals.rearm = 0; + if (profile_trace_globals.state == _profile_trace_armed) + profile_trace_globals.state = _profile_trace_idle; + if (profile_trace_globals.state == _profile_trace_recording) + { + game->depth = 0; + game->open_aggregates = 0; + game->overflow = 0; + profile_trace_cut(1, _profile_trace_stop_exit); + } + if (profile_trace_globals.state == _profile_trace_finishing) + { + pthread_mutex_lock(&profile_trace_globals.writer_lock); + while (!__atomic_load_n(&profile_trace_globals.finished, __ATOMIC_ACQUIRE)) + pthread_cond_wait(&profile_trace_globals.writer_changed, &profile_trace_globals.writer_lock); + pthread_mutex_unlock(&profile_trace_globals.writer_lock); + __atomic_store_n(&profile_trace_globals.finished, 0, __ATOMIC_RELAXED); + profile_trace_globals.state = _profile_trace_idle; + } +} + +int profile_trace_recording( + void) +{ + return profile_trace_is_recording(); +} + +void profile_trace_status( + struct profile_trace_status *status) +{ + memset(status, 0, sizeof(*status)); + status->state = profile_trace_globals.state; + strcpy(status->name, profile_trace_globals.name); + if (profile_trace_globals.state != _profile_trace_recording) + return; + status->part = profile_trace_globals.part_number; +} + +struct profile_part *profile_trace_writer_next( + void) +{ + struct profile_part *part; + + pthread_mutex_lock(&profile_trace_globals.writer_lock); + while (!profile_trace_globals.queue_count) + pthread_cond_wait(&profile_trace_globals.writer_changed, &profile_trace_globals.writer_lock); + part = profile_trace_globals.queue[0]; + profile_trace_globals.queue[0] = profile_trace_globals.queue[1]; + profile_trace_globals.queue_count--; + pthread_mutex_unlock(&profile_trace_globals.writer_lock); + return part; +} + +void profile_trace_writer_done( + struct profile_part *part) +{ + int last = part->last; + + pthread_mutex_lock(&profile_trace_globals.writer_lock); + profile_trace_globals.writer_holds[part->arena] = 0; + pthread_cond_broadcast(&profile_trace_globals.writer_changed); + pthread_mutex_unlock(&profile_trace_globals.writer_lock); + if (!last) + return; + /* (the last part: no track writes to either arena any more, and the + writer has written the other part before this one) */ + profile_trace_seams.release(profile_trace_globals.arenas[0]); + profile_trace_seams.release(profile_trace_globals.arenas[1]); + profile_trace_globals.arenas[0] = profile_trace_globals.arenas[1] = NULL; + pthread_mutex_lock(&profile_trace_globals.writer_lock); + __atomic_store_n(&profile_trace_globals.finished, 1, __ATOMIC_RELEASE); + pthread_cond_broadcast(&profile_trace_globals.writer_changed); + pthread_mutex_unlock(&profile_trace_globals.writer_lock); +} + +#endif diff --git a/source/cseries/profile.c b/source/cseries/profile.c index 0ef43da2..6f536996 100644 --- a/source/cseries/profile.c +++ b/source/cseries/profile.c @@ -278,6 +278,11 @@ symbols in this file: #include +#ifdef HALO_PROFILE +#include "profile_console.h" +#include "profile_trace.h" +#endif + /* ---------- constants */ enum @@ -448,6 +453,24 @@ boolean profile_global_enable = FALSE; boolean profile_dump_frames = FALSE; boolean profile_dump_lost_frames = FALSE; +#ifdef HALO_PROFILE +/* port: the profiling build: each section's name in the trace +(port/linux/src/profile_trace.c) by section_index, as struct +profile_section's size is fixed (profile.h's assertions); the frame +timers' names; and whether the first frame has set the recording up */ +static short profile_trace_section_names[MAXIMUM_PROFILE_SECTIONS]; +static struct +{ + int frame; + int game_tick; + int render; + int render_window; + int idle; + int texture; +} profile_trace_timer_names; +static boolean profile_trace_launched; +#endif + /* ---------- public code */ void profile_dump_to_file( @@ -1075,16 +1098,60 @@ void profile_initialize( profile_globals.lost_frame_count = 999; profile_globals.framedump_file = NULL; +#ifdef HALO_PROFILE + profile_trace_timer_names.frame = profile_trace_name("frame"); + profile_trace_timer_names.game_tick = profile_trace_name("game_tick"); + profile_trace_timer_names.render = profile_trace_name("render"); + profile_trace_timer_names.render_window = profile_trace_name("render_window"); + profile_trace_timer_names.idle = profile_trace_name("idle"); + profile_trace_timer_names.texture = profile_trace_name("texture"); + /* (many times a frame: a record each would cost more than the work it + times, so a count and a total a frame) */ + profile_trace_name_aggregate("texture"); + profile_trace_name_aggregate("render_model"); + profile_trace_name_aggregate("memory_dynamic_array_resize"); + profile_trace_name_aggregate("memory_dynamic_array_add_element"); + profile_trace_name_aggregate("memory_dynamic_array_delete_element"); + profile_trace_name_aggregate("item_update"); + profile_trace_name_aggregate("weapon_update"); + profile_trace_name_aggregate("unit_update"); + profile_trace_name_aggregate("biped_update"); + profile_trace_name_aggregate("vehicle_update"); + profile_trace_name_aggregate("projectile_update"); + profile_trace_name_aggregate("render_structure_shadows"); + profile_trace_name_aggregate("render_structure_shadows_draw"); + profile_trace_name_aggregate("render_structure_diffuse_lights"); + profile_trace_name_aggregate("render_structure_diffuse_lights_draw"); + profile_trace_name_aggregate("render_structure_specular_lights"); + profile_trace_name_aggregate("render_structure_specular_lights_draw"); +#endif + return; } void profile_frame_start( void) { +#ifdef HALO_PROFILE + if (!profile_trace_launched) + { + profile_trace_launched = TRUE; + profile_console_launch(); + } + /* (no section is open here: where a recording starts, is cut and + stops) */ + profile_trace_frame_boundary(); + profile_console_frame(); +#endif if (!profile_timebase_ticks) { profile_internal_step(); } +#ifdef HALO_PROFILE + /* (after the step, which rolls up what the sections timed last frame: + the console no longer turns it off mid-frame, console.c) */ + profile_global_enable = profile_trace_recording(); +#endif csmemset(&profile_globals.current_frame, 0, sizeof(profile_globals.current_frame)); @@ -1092,6 +1159,9 @@ void profile_frame_start( profile_globals.current_frame.vertical_blank_index = rasterizer_globals.vertical_blank_index; profile_globals.current_frame.game_tick_count = 0; profile_timesection_begin_now(&profile_globals.current_frame.frame); +#ifdef HALO_PROFILE + profile_trace_begin(profile_trace_timer_names.frame); +#endif return; } @@ -1118,6 +1188,10 @@ void profile_tick_start( timer = &profile_globals.current_frame.game_ticks[profile_globals.current_frame.game_tick_count-1]; profile_timesection_begin_now(timer); +#ifdef HALO_PROFILE + profile_trace_tick(); + profile_trace_begin(profile_trace_timer_names.game_tick); +#endif return; } @@ -1132,6 +1206,9 @@ void profile_tick_end( timer = &profile_globals.current_frame.game_ticks[profile_globals.current_frame.game_tick_count-1]; profile_timesection_end_now(timer); +#ifdef HALO_PROFILE + profile_trace_end(profile_trace_timer_names.game_tick); +#endif return; } @@ -1154,6 +1231,9 @@ void profile_render_window_start( timer = &profile_globals.current_frame.windows[profile_globals.current_frame.window_count-1]; profile_timesection_begin_now(timer); +#ifdef HALO_PROFILE + profile_trace_begin(profile_trace_timer_names.render_window); +#endif return; } @@ -1168,6 +1248,9 @@ void profile_render_window_end( timer = &profile_globals.current_frame.windows[profile_globals.current_frame.window_count-1]; profile_timesection_end_now(timer); +#ifdef HALO_PROFILE + profile_trace_end(profile_trace_timer_names.render_window); +#endif return; } @@ -1177,6 +1260,9 @@ void profile_render_start( { profile_globals.current_frame.window_count = 0; profile_timesection_begin_now(&profile_globals.current_frame.render); +#ifdef HALO_PROFILE + profile_trace_begin(profile_trace_timer_names.render); +#endif return; } @@ -1185,6 +1271,9 @@ void profile_render_end( void) { profile_timesection_end_now(&profile_globals.current_frame.render); +#ifdef HALO_PROFILE + profile_trace_end(profile_trace_timer_names.render); +#endif return; } @@ -1200,6 +1289,9 @@ void profile_texture_start( if (PROFILE_TEXTURE_TIMED()) { profile_timesection_begin_now(&profile_globals.current_frame.texture); +#ifdef HALO_PROFILE + profile_trace_begin(profile_trace_timer_names.texture); +#endif } return; @@ -1211,6 +1303,9 @@ void profile_texture_end( if (PROFILE_TEXTURE_TIMED()) { profile_timesection_end_now(&profile_globals.current_frame.texture); +#ifdef HALO_PROFILE + profile_trace_end(profile_trace_timer_names.texture); +#endif } return; @@ -1220,6 +1315,9 @@ void profile_idle_start( void) { profile_timesection_begin_now(&profile_globals.current_frame.idle); +#ifdef HALO_PROFILE + profile_trace_begin(profile_trace_timer_names.idle); +#endif return; } @@ -1228,6 +1326,9 @@ void profile_idle_end( void) { profile_timesection_end_now(&profile_globals.current_frame.idle); +#ifdef HALO_PROFILE + profile_trace_end(profile_trace_timer_names.idle); +#endif return; } @@ -1249,6 +1350,9 @@ void profile_enter_private( QUERY_TIMEBASE(timebase); section->entry_timebase = timebase; section->frame_call_count++; +#ifdef HALO_PROFILE + profile_trace_begin(profile_trace_section_names[section->section_index]); +#endif return; } @@ -1271,6 +1375,9 @@ void profile_exit_private( QUERY_TIMEBASE(timebase); section->frame_elapsed_timebase += timebase-section->entry_timebase; section->stack_depth = NONE; +#ifdef HALO_PROFILE + profile_trace_end(profile_trace_section_names[section->section_index]); +#endif } else { @@ -1476,6 +1583,9 @@ void profile_frame_end( short frame_index; profile_timesection_end_now(&profile_globals.current_frame.frame); +#ifdef HALO_PROFILE + profile_trace_end(profile_trace_timer_names.frame); +#endif match_assert("c:\\halo\\SOURCE\\cseries\\profile.c", 448, (profile_globals.current_frame.game_tick_count >= 0) && (profile_globals.current_frame.game_tick_count <= MAXIMUM_GAME_TICKS_PER_FRAME)); @@ -1740,6 +1850,9 @@ void find_profile_section( section->section_index = profile_globals.section_count; profile_globals.section_count++; profile_globals.sections[section->section_index] = section; +#ifdef HALO_PROFILE + profile_trace_section_names[section->section_index] = (short)profile_trace_name(section->name); +#endif csmemset(section->frame_elapsed_timebase_history, 0, sizeof(section->frame_elapsed_timebase_history)); csmemset(section->frame_call_count_history, 0, sizeof(section->frame_call_count_history)); diff --git a/source/cseries/profile_sections.h b/source/cseries/profile_sections.h new file mode 100644 index 00000000..627017c7 --- /dev/null +++ b/source/cseries/profile_sections.h @@ -0,0 +1,31 @@ +/* +PROFILE_SECTIONS.H + +Wrap game CPU sections without changing normal builds. +*/ + +#ifndef __PROFILE_SECTIONS_H +#define __PROFILE_SECTIONS_H + +#ifdef HALO_PROFILE + +#include "cseries/profile.h" + +/* these macros leave no tokens in a normal build: call sites have no trailing +semicolon. a call site may be an unbraced if-branch: in a normal build +profile_scope is the statement alone. */ +#define PROFILE_SECTION(variable, name) static struct profile_section variable = { name, NONE, TRUE }; +#define profile_scope_enter(variable) profile_enter(variable) +#define profile_scope_exit(variable) profile_exit(variable) +#define profile_scope(variable, statement) { profile_enter(variable) statement profile_exit(variable) } + +#else + +#define PROFILE_SECTION(variable, name) +#define profile_scope_enter(variable) +#define profile_scope_exit(variable) +#define profile_scope(variable, statement) statement + +#endif + +#endif diff --git a/source/game/game.c b/source/game/game.c index 83742035..7c6cfdc3 100644 --- a/source/game/game.c +++ b/source/game/game.c @@ -182,6 +182,10 @@ struct game_options; #include "units/units.h" #include "units/vehicles.h" #include "network_coop.h" /* port: port/linux/game/network_coop.c */ +#ifdef HALO_PROFILE +#include "profile_console.h" +#endif +#include "cseries/profile_sections.h" /* network_game_globals.c's */ boolean network_game_distributed_client(void); @@ -248,6 +252,25 @@ char const *global_game_difficulty_level_names[NUMBER_OF_GAME_DIFFICULTY_LEVELS] }; static struct profile_section game_update_section = {"game_update", NONE, TRUE}; +/* port: game_tick's steps, timed in the profiling build */ +PROFILE_SECTION(game_tick_cheats_enforce_section, "game_tick.cheats_enforce") +PROFILE_SECTION(game_tick_remove_quitting_players_section, "game_tick.remove_quitting_players") +PROFILE_SECTION(game_tick_allegiance_section, "game_tick.allegiance") +PROFILE_SECTION(game_tick_units_section, "game_tick.units") +PROFILE_SECTION(game_tick_ai_section, "game_tick.ai") +PROFILE_SECTION(game_tick_actors_drive_section, "game_tick.actors_drive") +PROFILE_SECTION(game_tick_players_before_section, "game_tick.players_before") +PROFILE_SECTION(game_tick_effects_section, "game_tick.effects") +PROFILE_SECTION(game_tick_antennas_section, "game_tick.antennas") +PROFILE_SECTION(game_tick_first_person_section, "game_tick.first_person") +PROFILE_SECTION(game_tick_game_engine_section, "game_tick.game_engine") +PROFILE_SECTION(game_tick_editor_section, "game_tick.editor") +PROFILE_SECTION(game_tick_hs_section, "game_tick.hs") +PROFILE_SECTION(game_tick_recorded_animations_section, "game_tick.recorded_animations") +PROFILE_SECTION(game_tick_objects_section, "game_tick.objects") +PROFILE_SECTION(game_tick_players_after_section, "game_tick.players_after") +PROFILE_SECTION(game_tick_hud_section, "game_tick.hud") +PROFILE_SECTION(game_tick_player_effect_section, "game_tick.player_effect") /* ---------- public code */ @@ -328,45 +351,46 @@ void game_tick( /* port: a client of another's game, the host's rules (its own cheats and game speed, set before it joined too, put back) */ - cheats_network_client_enforce(); - remove_quitting_players_from_game(); - game_allegiance_update(); - units_update(); + profile_scope(game_tick_cheats_enforce_section, cheats_network_client_enforce();) + profile_scope(game_tick_remove_quitting_players_section, remove_quitting_players_from_game();) + profile_scope(game_tick_allegiance_section, game_allegiance_update();) + profile_scope(game_tick_units_section, units_update();) /* (the host's actors drive the host's units, which a client of the distributed netcode has from the host: its own would fight the host's positions, and could place objects of their own) */ if (!network_game_distributed_client()) - ai_update(); + profile_scope(game_tick_ai_section, ai_update();) else - network_actors_drive(); - players_update_before_game(); + profile_scope(game_tick_actors_drive_section, network_actors_drive();) + profile_scope(game_tick_players_before_section, players_update_before_game();) seconds_per_tick = game_globals->players_are_double_speed ? 1.0f / (2 * TICKS_PER_SECOND) : 1.0f / TICKS_PER_SECOND; - effects_update(seconds_per_tick); + profile_scope(game_tick_effects_section, effects_update(seconds_per_tick);) /* An antenna's chain is a simulation of the Xbox's own step, an update a tick: stepped a frame at a time, its springs, its carry and its points' physics act against the frames' own movement of it, and a vehicle at speed swings it far wider than a tick's step does. The frames between the ticks are drawn from what each tick leaves (antenna.c). */ - antennas_update(seconds_per_tick); - lock_global_random_seed(); - rumble_update(); - first_person_weapons_update(); - unlock_global_random_seed(); - game_engine_update(); - editor_update(); + profile_scope(game_tick_antennas_section, antennas_update(seconds_per_tick);) + profile_scope(game_tick_first_person_section, + lock_global_random_seed(); + rumble_update(); + first_person_weapons_update(); + unlock_global_random_seed();) + profile_scope(game_tick_game_engine_section, game_engine_update();) + profile_scope(game_tick_editor_section, editor_update();) /* port: in network co-op only the host runs the scripts. A client running them would place the map's actors and objects a second time and make decisions that belong to the host. */ if (!(network_game_distributed_client() && network_coop_active())) - hs_update(); - recorded_animations_update(); - objects_update(); - players_update_after_game(); - hud_update(); - player_effect_update(); + profile_scope(game_tick_hs_section, hs_update();) + profile_scope(game_tick_recorded_animations_section, recorded_animations_update();) + profile_scope(game_tick_objects_section, objects_update();) + profile_scope(game_tick_players_after_section, players_update_after_game();) + profile_scope(game_tick_hud_section, hud_update();) + profile_scope(game_tick_player_effect_section, player_effect_update();) /* port: and at its end, before the frame draws the lights */ game_state_check_data_arrays(); lights_port_recover(); @@ -584,6 +608,11 @@ boolean game_is_cooperative( boolean game_load( struct game_options *options) { +#ifdef HALO_PROFILE + /* (every map load passes here: main_new_map's and a network game's, + network_game_manager.c) */ + profile_console_map_loaded(options->map_name); +#endif match_assert( "c:\\halo\\SOURCE\\game\\game.c", 0x192, diff --git a/source/game/game_time.c b/source/game/game_time.c index 739418be..8e40cc8d 100644 --- a/source/game/game_time.c +++ b/source/game/game_time.c @@ -75,9 +75,15 @@ symbols in this file: #include "networking/network_game_globals.h" #include "networking/network_server_manager.h" #include "saved games/game_state.h" +#include "cseries/profile_sections.h" /* port/linux/game/network_distributed.c's */ void network_distributed_tick(void); +/* port: the tick loop's steps, timed in the profiling build */ +PROFILE_SECTION(game_time_network_distributed_tick_section, "network_distributed_tick") +PROFILE_SECTION(game_time_render_interpolation_tick_section, "render_interpolation_tick") +PROFILE_SECTION(game_time_game_frame_section, "game_frame") + /* ---------- constants */ enum @@ -468,11 +474,11 @@ void game_time_update( for (update_index = 0; update_index < server_updates; update_index++) { game_tick(); - render_interpolation_tick(); + profile_scope(game_time_render_interpolation_tick_section, render_interpolation_tick();) game_time_globals->server_time++; game_time_globals->local_time++; /* the distributed netcode's per-tick state */ - network_distributed_tick(); + profile_scope(game_time_network_distributed_tick_section, network_distributed_tick();) } } @@ -488,7 +494,7 @@ void game_time_update( (port/linux/game/render_interpolation.c). The frames of one tick share the fraction, so what they hang off is drawn where they put it. */ render_interpolation_frame_begin(); - game_frame(game_time_get_speed()*time_delta_sec); + profile_scope(game_time_game_frame_section, game_frame(game_time_get_speed()*time_delta_sec);) render_interpolation_frame_end(); } else diff --git a/source/hs/hs.c b/source/hs/hs.c index e1f6fde6..a3f6f469 100644 --- a/source/hs/hs.c +++ b/source/hs/hs.c @@ -2840,6 +2840,9 @@ symbols in this file: #include "units/units.h" #include "units/vehicles.h" #include "coop_scripts.h" /* port: port/linux/game/coop_scripts.c */ +#ifdef HALO_PROFILE +#include "profile_console.h" +#endif /* ---------- constants */ @@ -15357,6 +15360,12 @@ static boolean hs_compile_and_evaluate_command( char buffer[1024]; char expanded[1024]; +#ifdef HALO_PROFILE + /* port: the profiling build's commands change nothing of the game: a client + may give them, and they are no scripts */ + if (profile_console_command(expression)) + return TRUE; +#endif /* port: the co-op host's bringto, which brings every player to the host (players.c; a client is told it is the host's) */ if (hs_host_player_command(expression, "bringto")) diff --git a/source/main/console.c b/source/main/console.c index 4b8a7103..d67d4e4a 100644 --- a/source/main/console.c +++ b/source/main/console.c @@ -129,7 +129,9 @@ void console_open( { console_globals.input_state.result[0] = '\0'; console_globals.active = terminal_gets_begin(&console_globals.input_state); +#ifndef HALO_PROFILE profile_global_enable = FALSE; +#endif } return; @@ -498,7 +500,12 @@ boolean console_update( { console_globals.input_state.result[0] = '\0'; console_globals.active = terminal_gets_begin(&console_globals.input_state); + /* port: the profiling build: cleared mid-frame, a section already entered + would never exit, and its next enter assert; and opening the console + to type profile_stop would end the recording's timing */ +#ifndef HALO_PROFILE profile_global_enable = FALSE; +#endif } return console_globals.active; diff --git a/source/main/main.c b/source/main/main.c index 31a23036..1f2510a6 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -380,6 +380,10 @@ symbols in this file: #include "networking/network_game_manager.h" #include "networking/network_server_manager.h" /* port: a co-op game's level won */ #include "network_coop.h" /* port: port/linux/game/network_coop.c */ +#include "cseries/profile_sections.h" +#ifdef HALO_PROFILE +#include "profile_trace.h" +#endif #include "camera/director.h" #include "camera/observer.h" #include "cutscene/cinematics.h" @@ -1440,6 +1444,16 @@ short main_get_window_count( return single_window ? 1 : PIN(local_player_count(), 1, MAXIMUM_WINDOWS); } +#ifdef HALO_PROFILE +/* port: whether the main menu's map is the one loaded (profile_console.c: a +recording of debug.profile_record_when = "game" waits for another) */ +boolean main_menu_is_loaded( + void) +{ + return main_globals.main_menu_scenario_loaded; +} +#endif + static void main_new_map( struct game_options *options) { @@ -2364,6 +2378,9 @@ void main_set_map_name( static void main_exit( void) { +#ifdef HALO_PROFILE + profile_trace_shutdown(); +#endif switch (main_globals.connection) { case _game_connection_network_server: @@ -3311,6 +3328,18 @@ static void main_game_render( return; } +/* port: the main loop's steps, timed in the profiling build */ +PROFILE_SECTION(main_input_section, "input") +PROFILE_SECTION(main_platform_events_section, "platform_events") +PROFILE_SECTION(main_network_start_frame_section, "network_start_frame") +PROFILE_SECTION(main_update_time_section, "main_update_time") +PROFILE_SECTION(main_ui_update_section, "ui_update") +PROFILE_SECTION(main_network_end_frame_section, "network_end_frame") +PROFILE_SECTION(main_game_time_update_section, "game_time_update") +PROFILE_SECTION(main_non_deterministic_update_section, "non_deterministic_update") +PROFILE_SECTION(main_throttle_section, "throttle") +PROFILE_SECTION(main_present_section, "present") + void main_loop( void) { @@ -3424,12 +3453,16 @@ void main_loop( } profile_frame_start(); + profile_scope_enter(main_input_section) input_frame_begin(); input_update(); input_abstraction_update(); + profile_scope_exit(main_input_section) + profile_scope_enter(main_platform_events_section) shell_idle(); event_manager_update(); telnet_console_process(); + profile_scope_exit(main_platform_events_section) if (!shell_application_is_paused()) { @@ -3437,6 +3470,7 @@ void main_loop( /* automated system link tests (port/linux/game/network_test.c) */ network_test_update(main_globals.main_menu_scenario_loaded, main_globals.seconds_elapsed); + profile_scope_enter(main_network_start_frame_section) connection = main_globals.connection; if (connection==_game_connection_network_client) { @@ -3464,12 +3498,16 @@ void main_loop( } else if (connection==_game_connection_film_playback) { + profile_scope_exit(main_network_start_frame_section) break; } + profile_scope_exit(main_network_start_frame_section) - main_update_time(); + profile_scope(main_update_time_section, main_update_time();) + profile_scope_enter(main_ui_update_section) process_ui_widgets(); bink_playback_update(); + profile_scope_exit(main_ui_update_section) if ((!game_in_editor() && (input_key_is_down(_key_end) || input_key_is_down(_key_escape))) || editor_should_exit()) { @@ -3492,13 +3530,16 @@ void main_loop( player_control_update((real)main_globals.halt_time_scale*main_globals.seconds_elapsed); connection = main_globals.connection; + profile_scope_enter(main_network_end_frame_section) if (connection>_game_connection_local && connection<=_game_connection_network_server && !network_game_client_end_frame()) { display_error_when_main_menu_loaded(1); network_game_abort(); } + profile_scope_exit(main_network_end_frame_section) - game_time_update((real)main_globals.halt_time_scale*main_globals.seconds_elapsed); + profile_scope(main_game_time_update_section, + game_time_update((real)main_globals.halt_time_scale*main_globals.seconds_elapsed);) render_frame = main_globals.main_menu_scenario_loaded || (main_globals.halt_time_scale && @@ -3508,11 +3549,13 @@ void main_loop( render_frame = main_globals.main_menu_scenario_loaded || main_globals.halt_time_scale; render_frame &= !game_engine_running() || game_time_get()>=3; + profile_scope_enter(main_non_deterministic_update_section) collision_log_continue_period(1); director_update((real)main_globals.halt_time_scale*main_globals.seconds_elapsed); observer_update((real)main_globals.halt_time_scale*main_globals.seconds_elapsed); collision_log_end_period(); game_engine_update_non_deterministic((real)main_globals.halt_time_scale*main_globals.seconds_elapsed); + profile_scope_exit(main_non_deterministic_update_section) } if (main_globals.saving_map) @@ -3536,11 +3579,11 @@ void main_loop( profile_render_end(); } - main_rasterizer_throttle(); + profile_scope(main_throttle_section, main_rasterizer_throttle();) if (render_frame && !debug_no_drawing) { - main_present_frame(); + profile_scope(main_present_section, main_present_frame();) } } diff --git a/tools/android_build.py b/tools/android_build.py index fcce7c58..e81b8f1e 100644 --- a/tools/android_build.py +++ b/tools/android_build.py @@ -26,7 +26,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional from .linux_build import (LINUX_PROFILE, MINIUPNPC_DEFINES, MINIUPNPC_DIR, MUSL_MATH_DIR, XDK_INCLUDE, - compile_launcher, game_defines_and_includes, game_sources, miniupnpc_sources, + compile_launcher, configuration_defines, game_defines_and_includes, game_sources, miniupnpc_sources, musl_math_sources, pgo_mode, pgo_profile, profile_use_flags, updater_defines, xdk_headers) from .embed_assets import hud_assets_build, hud_configure_inputs @@ -378,7 +378,7 @@ def generate_android_build(n: Writer, sln: Any) -> None: f"-isystem {libc_include}", f"-isystem {arch}", f"-isystem {MUSL_DIR}/arch/generic", f"-isystem {MUSL_DIR}/include", ] - guest_abi = " ".join(GUEST_ABI_FLAGS + (["-DHALO_RELEASE"] if getattr(sln, "port_release", False) else [])) + guest_abi = " ".join(GUEST_ABI_FLAGS + configuration_defines(sln)) guest_code = " ".join(GUEST_CODE_FLAGS) tool_implicit = [Path("tools/android_asm_convert.py"), *generated_headers] # profile-guided optimisation with the Linux build's profile (committed, diff --git a/tools/linux_build.py b/tools/linux_build.py index 5569daec..c40564c1 100644 --- a/tools/linux_build.py +++ b/tools/linux_build.py @@ -151,6 +151,25 @@ def updater_defines(release: bool) -> str: flavor = "release" if release else "debug" return f'-DHALO_BUILD_NUMBER={number} -DHALO_BUILD_FLAVOR=\\"{flavor}\\"' +def configuration_defines(sln: Any) -> List[str]: + """what configure.py's options define for every unit of a native build: + --release (no assertions), --profile (the profiling build's recording, + port/linux/src/profile_trace.c)""" + defines = [] + if getattr(sln, "port_release", False): + defines.append("-DHALO_RELEASE") + if getattr(sln, "port_profile", False): + defines.append("-DHALO_PROFILE") + return defines + + +def check_profile_options(profile: bool, pgo: str) -> None: + """A profiling build is optimised with the committed profiles or none: + trained, a profile would record the profiling code's own paths.""" + if profile and pgo == "train": + raise ValueError("--profile cannot be used with --pgo=train: train profiles with a normal build") + + PLATFORM_FLAGS = [ "-std=gnu11", "-D_GNU_SOURCE", @@ -385,8 +404,8 @@ def generate_linux_build(n: Writer, sln: Any) -> None: # (a debug build checks its stack frames, and stops at the first one # overrun, as it stops at the first failed assertion; a release build # does not, so that an overrun nobody has met cannot end a game) - abi = " ".join(LINUX_ABI_FLAGS + [march_flag(sln)] + (["-DHALO_RELEASE"] if getattr(sln, "port_release", False) - else ["-fstack-protector-strong"])) + abi = " ".join(LINUX_ABI_FLAGS + [march_flag(sln)] + configuration_defines(sln) + + ([] if getattr(sln, "port_release", False) else ["-fstack-protector-strong"])) port_include = PORT_DIR / "include" sdk_flags = f"-idirafter {XDK_INCLUDE}" libs = " ".join(f"-l{lib}" for lib in config.get("libraries", [])) diff --git a/tools/net_report.py b/tools/net_report.py new file mode 100644 index 00000000..5e947a1c --- /dev/null +++ b/tools/net_report.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +"""Summarize CPU profiling recordings. +Usage: python tools/net_report.py RECORDING [RECORDING2 ...] [options].""" + +import argparse +import csv +import json +import re +import sys +from pathlib import Path + +FORMAT = 1 +SUMMARY_LINE_LIMIT = 300 +# (the summary file always uses this, so its length does not depend on --top) +SUMMARY_TOP = 40 +REQUIRED_HEADER = ("part", "last_part", "role", "own_machine", "build", "platform", "map", + "players", "start_utc", "start_s", "duration_s", + "stop_reason") +PER_TICK_PREFIXES = ("game_tick", "network_distributed_tick") + +COLUMNS_GLOSSARY = """\ +scope CPU scope name (a profile section or frame timer); nested names use "." +count times the scope ran in the recording +total_ms summed inclusive time (children included), milliseconds +mean_ms total_ms / count +max_ms longest single run; aggregate sections: largest summed time in one frame +per_frame count / frames recorded +per_tick_ms total_ms / ticks recorded (game_tick.* and network_distributed_tick* only) +frame frame number in the recording +at_s seconds from the start of the recording to that frame +frame_ms that frame's duration +ticks game ticks that frame ran +longest scopes (ms) the frame's three scopes, at any depth, with the most time of their own (children not counted), name and that time""" + + +class ReportError(Exception): + pass + + +# ---------- reading a recording + + +def part_paths(recording): + """the recording's name, its part files by number, and the warnings""" + path = Path(recording) + match = re.match(r"^(.*)\.part(\d+)\.json$", path.name) + base = path.parent / (match.group(1) if match else re.sub(r"\.json$", "", path.name)) + parts = {} + for candidate in base.parent.glob(base.name + ".part*.json"): + number = re.match(r"^.*\.part(\d+)\.json$", candidate.name) + if number and candidate.name.startswith(base.name + ".part"): + parts[int(number.group(1))] = candidate + if not parts: + raise ReportError(f"no parts of {base}") + warnings = [f"part {number} of {base.name} is missing" for number in range(1, max(parts) + 1) + if number not in parts] + return base, [parts[number] for number in sorted(parts)], warnings + + +def read_head(path): + """the header and cpu tables, read before the event array""" + with open(path, "r", encoding="utf-8") as file: + return read_head_stream(file, path) + + +def read_head_stream(file, path): + text = "" + marker = '"traceEvents"' + while marker not in text: + chunk = file.read(65536) + if not chunk: + raise ReportError(f"{path}: has no traceEvents") + text += chunk + text = text.split(marker, 1)[0].rstrip().rstrip(",") + "}" + try: + head = json.loads(text) + except json.JSONDecodeError as error: + raise ReportError(f"{path}: not a recording part ({error})") from None + if not isinstance(head, dict): + raise ReportError(f"{path}: not a recording part") + for key in ("halo", "cpu_summary"): + if not isinstance(head.get(key), dict): + raise ReportError(f'{path}: has no "{key}"') + _validate_head_part(head, path) + return [head] + + +def _validate_head_part(head, path): + where = str(path) + for key in ("halo", "cpu_summary"): + if not isinstance(head.get(key), dict): + raise ReportError(f'{where}: has no "{key}"') + header = head["halo"].get("header") + if not isinstance(header, dict): + raise ReportError(f'{where}: has no "header" in "halo"') + for key in REQUIRED_HEADER: + if key not in header: + raise ReportError(f'{where}: the header has no "{key}"') + + +def table_dicts(table): + columns = table["columns"] + return [dict(zip(columns, row)) for row in table["rows"]] + + +class Recording: + """a recording's parts with their CPU summaries merged""" + + def __init__(self, recording): + self.base, paths, self.warnings = part_paths(recording) + heads = [head for part in paths for head in read_head(part)] + self.name = self.base.name + self.headers = [head["halo"]["header"] for head in heads] + self.parts = [header["part"] for header in self.headers] + self.complete = any(header["last_part"] for header in self.headers) + if not self.complete: + self.warnings.append(f"{self.name} has no last part: the recording is incomplete") + self.header = self.headers[0] + self.cpu = self.merge_cpu([head["cpu_summary"] for head in heads]) + + @staticmethod + def merge_cpu(summaries): + names = {} + frames = sum(summary["frames"] for summary in summaries) + ticks = sum(summary["ticks"] for summary in summaries) + worst = [] + for summary in summaries: + for row in table_dicts(summary): + totals = names.setdefault(row["name"], {"count": 0, "total_ms": 0.0, "max_ms": 0.0}) + totals["count"] += row["count"] + totals["total_ms"] += row["total_ms"] + totals["max_ms"] = max(totals["max_ms"], row["max_ms"]) + worst.extend(table_dicts(summary["worst_frames"])) + for name, totals in names.items(): + totals["mean_ms"] = totals["total_ms"] / totals["count"] if totals["count"] else 0.0 + totals["per_frame"] = totals["count"] / frames if frames else 0.0 + totals["per_tick_ms"] = (totals["total_ms"] / ticks if ticks and name.startswith(PER_TICK_PREFIXES) + else None) + worst.sort(key=lambda row: -row["frame_ms"]) + return {"frames": frames, "ticks": ticks, "names": names, "worst": worst[:10]} + + def select(self, start=None, end=None): + """Elapsed seconds from the recorded part headers, clipped to a window.""" + seconds = 0.0 + for header in self.headers: + begin = header["start_s"] + finish = begin + header["duration_s"] + seconds += max(0.0, min(finish, end if end is not None else finish) - + max(begin, start if start is not None else begin)) + return seconds + + +# ---------- the tables + + +def cpu_table(recording): + rows = [[name, totals["count"], totals["total_ms"], totals["mean_ms"], totals["max_ms"], totals["per_frame"], + totals["per_tick_ms"]] for name, totals in recording.cpu["names"].items()] + rows.sort(key=lambda row: (-row[2], row[0])) + return ["scope", "count", "total_ms", "mean_ms", "max_ms", "per_frame", "per_tick_ms"], rows + + +def worst_frame_table(recording): + rows = [[row["frame"], row["at_s"], row["frame_ms"], row["ticks"], + ", ".join(f"{name} {ms:.1f}" for name, ms in row["longest"])] for row in recording.cpu["worst"]] + return ["frame", "at_s", "frame_ms", "ticks", "longest scopes (ms)"], rows + + +# ---------- text + + +NUMBER_FORMATS = { + "count": "{:.0f}", "total_ms": "{:.1f}", "mean_ms": "{:.3f}", "max_ms": "{:.3f}", "per_frame": "{:.2f}", + "per_tick_ms": "{:.3f}", "frame": "{:.0f}", "at_s": "{:.2f}", "frame_ms": "{:.2f}", "ticks": "{:.0f}", +} +TEXT_WIDTH = {"scope": 40, "longest scopes (ms)": 72} + + +def cell(column, value): + if value is None: + return "-" + if isinstance(value, (int, float)) and not isinstance(value, bool) and column in NUMBER_FORMATS: + return NUMBER_FORMATS[column].format(value) + text = str(value) + width = TEXT_WIDTH.get(column) + if width and len(text) > width: + text = text[:width - 1] + "~" + return text + + +def format_table(columns, rows, limit=None): + """aligned lines: names left, numbers right; past the limit a line of + how many more""" + shown = rows if limit is None else rows[:limit] + if not shown: + return ["(none)"] + cells = [[cell(column, value) for column, value in zip(columns, row)] for row in shown] + numeric = [column in NUMBER_FORMATS for column in columns] + widths = [max([len(column)] + [len(line[index]) for line in cells]) for index, column in enumerate(columns)] + + def line(values): + parts = [value.rjust(width) if is_number else value.ljust(width) + for value, width, is_number in zip(values, widths, numeric)] + return " ".join(parts).rstrip() + + lines = [line(columns)] + [line(values) for values in cells] + if limit is not None and len(rows) > limit: + lines.append(f"... {len(rows) - limit} more rows (net_report.py --top N)") + return lines + + +def problems(recording): + found = list(recording.warnings) + counters = (("unbalanced_scopes", "unbalanced scopes"), ("deep_scopes", "deep scopes"), + ("foreign_scopes", "foreign scopes"), ("dropped_scopes", "dropped scopes")) + for key, label in counters: + total = sum(header.get(key, 0) for header in recording.headers) + if total: + found.append(f"{label} {total}") + wait = sum(header.get("writer_wait_ms", 0.0) for header in recording.headers[-1:]) + if wait: + found.append(f"writer waited {wait:.0f} ms") + return found + + +def summary_lines(recording, start, end, top): + seconds = recording.select(start, end) + windowed = start is not None or end is not None + whole = recording.select() + header, last = recording.header, recording.headers[-1] + parts = f"parts {min(recording.parts)}-{max(recording.parts)}" if len(recording.parts) > 1 else "part 1" + state = "complete" if recording.complete and not any( + "missing" in warning for warning in recording.warnings) else "incomplete" + found = problems(recording) + own = header["own_machine"] if header["own_machine"] >= 0 else "-" + lines = [ + f"# halo profile summary, format {FORMAT}", + f"# recording: {recording.name}, {parts} ({state})", + f"# build: {header['build']}, platform: {header['platform']}, role: {header['role']}, own_machine: {own}", + f"# map: {header['map']}, players: {header['players']} at start, " + f"{max(item.get('players_most', item['players']) for item in recording.headers)} most", + f"# recorded: {header['start_utc']} UTC, {whole:.1f} s, {recording.cpu['ticks']} ticks, " + f"{recording.cpu['frames']} frames, stopped: {last['stop_reason'] or '-'}", + *([f"# window: {seconds:.1f} s selected (seconds {start if start is not None else 0:g} to " + f"{f'{end:g}' if end is not None else 'end'}); the cpu tables, ticks and frames cover the whole recording"] + if windowed else []), + f"# problems: {'; '.join(found) if found else 'none'}", + "# units: ms = milliseconds", "", "## columns", + ] + note = ", whole recording" if windowed else "" + sections = [(f"cpu scopes (top {min(top, 40)} by total_ms{note})", cpu_table(recording), min(top, 40)), + (f"worst frames (10{note})", worst_frame_table(recording), 10)] + printed = {column for _, (columns, rows), _ in sections if rows for column in columns} + lines.extend(line for line in COLUMNS_GLOSSARY.splitlines() if re.split(r"\s{2,}", line, maxsplit=1)[0] in printed) + for title, table, limit in sections: + lines.extend(["", f"## {title}", *format_table(*table, limit)]) + return lines + + +def write_csv(recording, directory): + directory.mkdir(parents=True, exist_ok=True) + for name, (columns, rows) in {"cpu": cpu_table(recording), "worst_frames": worst_frame_table(recording)}.items(): + with open(directory / f"{recording.name}.{name}.csv", "w", newline="", encoding="ascii", errors="replace") as file: + writer = csv.writer(file) + writer.writerow(columns) + writer.writerows([[NUMBER_FORMATS[column].format(value) + if column in NUMBER_FORMATS and isinstance(value, (int, float)) else + ("-" if value is None else value) for column, value in zip(columns, row)] for row in rows]) + + +def main(arguments=None): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("recordings", nargs="+", metavar="RECORDING") + parser.add_argument("--from", dest="start", type=float, help="from this second of the recording") + parser.add_argument("--to", dest="end", type=float, help="to this second") + parser.add_argument("--top", type=int, default=40, help="CPU rows (default 40)") + parser.add_argument("--csv", type=Path, metavar="DIR", help="every table as CSV in DIR") + parser.add_argument("--no-summary", action="store_true", help="do not write .summary.txt") + options = parser.parse_args(arguments) + try: + recordings = [Recording(name) for name in options.recordings] + except (ReportError, OSError) as error: + print(f"net_report: {error}", file=sys.stderr) + return 1 + for recording in recordings: + try: + lines = summary_lines(recording, options.start, options.end, options.top) + file_lines = lines if options.top == SUMMARY_TOP else summary_lines( + recording, options.start, options.end, SUMMARY_TOP) + except (KeyError, TypeError, IndexError, ValueError) as error: + print(f"net_report: {recording.name}: a table is not as the writer wrote it ({error!r})", file=sys.stderr) + return 1 + for warning in recording.warnings: + print(f"warning: {warning}", file=sys.stderr) + print("\n".join(lines)) + print() + if not options.no_summary: + path = recording.base.parent / f"{recording.name}.summary.txt" + path.write_text("\n".join(file_lines) + "\n", encoding="ascii", errors="replace") + if options.csv: + write_csv(recording, options.csv) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/profile_check.c b/tools/profile_check.c new file mode 100644 index 00000000..27ed1538 --- /dev/null +++ b/tools/profile_check.c @@ -0,0 +1,1059 @@ +/* +PROFILE_CHECK.C + +Exercise the CPU recorder and writer with standalone fakes. +*/ + +#include "profile_part.h" +#include "profile_console_gametype.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---------- fakes */ + +static int failures; +static int session_sample_calls; +static unsigned long long fake_now = 1000000000ULL; +static pthread_mutex_t fake_track_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t fake_allocator_lock = PTHREAD_MUTEX_INITIALIZER; +static long allocations_outstanding; +static long allocations_made; +static int allocations_refused; +/* allocations that succeed before the rest are refused (-1: no such limit) */ +static int allocations_allowed = -1; +static int fake_ready; +static const char *fake_role = "host"; +static long fake_players = 2; +static long fake_own_machine = -1; +static const char *fake_map = "levels\\a30\\a30"; +static const char *fake_map_name = "a30"; +static const char *fake_folder = "."; +static char last_notice[128]; +static char last_log[600]; +static int name_aggregate_child; + +static void check(int good, const char *what) +{ + if (!good) + { + failures++; + printf("FAIL: %s\n", what); + } +} + +static void gametype_checks(void) +{ + static const char *const expected[] = { "none", "ctf", "slayer", "oddball", "king", "race" }; + long engine; + for (engine = 0; engine <= 5; engine++) + check(strcmp(profile_console_engine_gametype(1, engine), expected[engine]) == 0, + "engine index maps to its engine gametype"); + check(strcmp(profile_console_engine_gametype(0, 2), "campaign") == 0, + "campaign does not reuse a stale slayer variant"); + check(strcmp(profile_console_engine_gametype(1, 6), "none") == 0 && + strcmp(profile_console_engine_gametype(1, 7), "none") == 0, "an engine past race is none"); + check(strcmp(profile_console_scenario_map("levels\\b30\\b30", "ui"), "levels\\b30\\b30") == 0, + "the pending scenario takes precedence over the previous map"); +} + +static unsigned long long fake_clock(void) +{ + return __atomic_load_n(&fake_now, __ATOMIC_RELAXED); +} + +static void advance(unsigned long long nanoseconds) +{ + __atomic_fetch_add(&fake_now, nanoseconds, __ATOMIC_RELAXED); +} + +static void fake_lock(void) +{ + pthread_mutex_lock(&fake_track_lock); +} + +static void fake_unlock(void) +{ + pthread_mutex_unlock(&fake_track_lock); +} + +static void *fake_allocate(unsigned long size) +{ + void *block; + + pthread_mutex_lock(&fake_allocator_lock); + block = allocations_refused || allocations_allowed == 0 ? NULL : malloc(size); + if (allocations_allowed > 0) + allocations_allowed--; + if (block) + { + allocations_outstanding++; + allocations_made++; + } + pthread_mutex_unlock(&fake_allocator_lock); + return block; +} + +static void fake_release(void *block) +{ + if (!block) + return; + pthread_mutex_lock(&fake_allocator_lock); + allocations_outstanding--; + pthread_mutex_unlock(&fake_allocator_lock); + free(block); +} + +static long outstanding(void) +{ + long count; + + pthread_mutex_lock(&fake_allocator_lock); + count = allocations_outstanding; + pthread_mutex_unlock(&fake_allocator_lock); + return count; +} + +static void quiet_log(const char *text) +{ + if (strstr(text, "cannot write")) + snprintf(last_log, sizeof(last_log), "%s", text); +} + +static void fake_notify(const char *text) +{ + snprintf(last_notice, sizeof(last_notice), "%s", text); +} + +static int fake_ready_test(void) +{ + return fake_ready; +} + +static void fake_session(struct profile_trace_session *session) +{ + memset(session, 0, sizeof(*session)); + snprintf(session->folder, sizeof(session->folder), "%s", fake_folder); + strcpy(session->build, "check"); + strncpy(session->role, fake_role, sizeof(session->role) - 1); + session->own_machine = fake_own_machine; + strncpy(session->map, fake_map, sizeof(session->map) - 1); + strncpy(session->map_name, fake_map_name, sizeof(session->map_name) - 1); + strcpy(session->gametype, "campaign"); + session->players = fake_players; + session->players_most = fake_players; +} + +static void fake_session_sample(struct profile_trace_session *session) +{ + session_sample_calls++; + fake_session(session); +} + +/* ---------- the fake writer: what each part held */ + +enum +{ + MAXIMUM_PARTS = 64, +}; + +static struct +{ + pthread_mutex_t lock; + int parts; + int finished; + unsigned long counts[MAXIMUM_PROFILE_TRACE_NAMES]; + unsigned long counter_records; + unsigned long long aggregate_time[MAXIMUM_PROFILE_TRACE_NAMES]; + unsigned long aggregate_count[MAXIMUM_PROFILE_TRACE_NAMES]; + long first_frame[MAXIMUM_PARTS]; + long frames[MAXIMUM_PARTS]; + unsigned long long start_ns[MAXIMUM_PARTS]; + unsigned long long end_ns[MAXIMUM_PARTS]; + int last[MAXIMUM_PARTS]; + int stop_reason[MAXIMUM_PARTS]; + char map_name[MAXIMUM_PARTS][64]; + double writer_wait_ms; + unsigned long unbalanced; + unsigned long deep; + unsigned long foreign; + unsigned long dropped; + int records_in_span; + int aggregate_child_depth; + int delay_milliseconds; +} written = { .lock = PTHREAD_MUTEX_INITIALIZER }; + +static void *fake_writer(void *unused) +{ + (void)unused; + for (;;) + { + struct profile_part *part = profile_trace_writer_next(); + int last = part->last; + unsigned long index; + int track; + + /* (a slow first part: the game thread's wait for it is on the fake + clock too) */ + if (written.delay_milliseconds && part->number == 1) + { + usleep((useconds_t)written.delay_milliseconds * 1000); + advance((unsigned long long)written.delay_milliseconds * 1000000ULL); + } + pthread_mutex_lock(&written.lock); + if (written.parts < MAXIMUM_PARTS) + { + written.first_frame[written.parts] = part->first_frame; + written.frames[written.parts] = part->frames; + written.start_ns[written.parts] = part->start_ns; + written.end_ns[written.parts] = part->end_ns; + written.last[written.parts] = part->last; + written.stop_reason[written.parts] = part->stop_reason; + strcpy(written.map_name[written.parts], part->session.map_name); + } + written.parts++; + written.writer_wait_ms = part->writer_wait_ms; + written.unbalanced += part->unbalanced_scopes; + written.deep += part->deep_scopes; + written.foreign += part->foreign_scopes; + written.dropped += part->dropped_scopes; + for (track = 0; track < NUMBER_OF_PROFILE_TRACKS; track++) + { + for (index = 0; index < part->record_counts[track]; index++) + { + const struct profile_trace_record *record = &part->records[track][index]; + + if (record->depth < MAXIMUM_PROFILE_TRACE_DEPTH) + { + written.counts[record->name]++; + if (record->name == name_aggregate_child) + written.aggregate_child_depth = record->depth; + /* (a scope that began before the part, in the p2p pass a + cut came in, is in the part it ended in) */ + if (record->start + record->duration > part->end_ns) + written.records_in_span = 0; + } + else + { + written.counter_records++; + if (record->depth == PROFILE_TRACE_DEPTH_AGGREGATE_TIME) + written.aggregate_time[record->name] += record->duration; + else if (record->depth == PROFILE_TRACE_DEPTH_AGGREGATE_COUNT) + written.aggregate_count[record->name] += record->duration; + } + } + } + pthread_mutex_unlock(&written.lock); + if (last) + { + pthread_mutex_lock(&written.lock); + written.finished++; + pthread_mutex_unlock(&written.lock); + } + /* Publish the fake writer's last observation before done makes the + recorder idle; finish() may read the observations as soon as it does. */ + profile_trace_writer_done(part); + if (last) + break; + } + return NULL; +} + +static int fake_writer_start(void) +{ + pthread_t thread; + + return pthread_create(&thread, NULL, fake_writer, NULL) == 0 && pthread_detach(thread) == 0; +} + +static void forget_written(void) +{ + pthread_mutex_lock(&written.lock); + written.parts = 0; + written.finished = 0; + memset(written.counts, 0, sizeof(written.counts)); + written.counter_records = 0; + written.unbalanced = written.deep = written.foreign = written.dropped = 0; + written.records_in_span = 1; + pthread_mutex_unlock(&written.lock); +} + +/* whether the writer has finished a recording, read through its lock */ +static int writer_finished(void) +{ + int finished; + + pthread_mutex_lock(&written.lock); + finished = written.finished; + pthread_mutex_unlock(&written.lock); + return finished; +} + +/* frame boundaries until the recording is written and the state idle */ +static void finish(void) +{ + struct profile_trace_status status; + int frames; + + for (frames = 0; frames < 100000; frames++) + { + profile_trace_frame_boundary(); + profile_trace_status(&status); + if (status.state == _profile_trace_idle || status.state == _profile_trace_armed) + { + /* (what the writer noted, seen through its lock) */ + pthread_mutex_lock(&written.lock); + pthread_mutex_unlock(&written.lock); + return; + } + usleep(100); + } + check(0, "the recording finishes"); +} + +/* ---------- checks */ + +static int name_frame, name_tick, name_a, name_b, name_c, name_texture, name_pass, name_inner_aggregate; + +static void frame(int scopes) +{ + int index; + + profile_trace_frame_boundary(); + profile_trace_begin(name_frame); + for (index = 0; index < scopes; index++) + { + profile_trace_begin(name_a); + advance(1000); + profile_trace_begin(name_b); + advance(500); + profile_trace_end(name_b); + profile_trace_end(name_a); + } + advance(1000); + profile_trace_end(name_frame); +} + +static void scope_checks(void) +{ + struct profile_trace_status status; + int index; + + forget_written(); + check(profile_trace_request_start(0.0, _profile_trace_when_now, 4) == _profile_trace_answer_armed, + "profile_record arms"); + profile_trace_status(&status); + check(status.state == _profile_trace_armed, "armed until the next frame boundary"); + check(strncmp(status.name, "profile_", 8) == 0 && strstr(status.name, "_host"), "named when asked for"); + check(!profile_trace_recording(), "not recording before the boundary"); + frame(3); + check(profile_trace_recording(), "recording from the boundary"); + /* the first map load after launch (the main menu) does not stop it */ + profile_trace_map_loaded(); + profile_trace_frame_boundary(); + check(profile_trace_recording(), "the first map load does not stop a recording"); + check(profile_trace_request_start(0.0, _profile_trace_when_now, 4) == _profile_trace_answer_already_recording, + "a second profile_record is refused"); + + /* nesting deeper than the stack: not recorded, counted, and the rest + still balances */ + profile_trace_frame_boundary(); + for (index = 0; index < MAXIMUM_PROFILE_TRACE_DEPTH + 3; index++) + profile_trace_begin(name_c); + for (index = 0; index < MAXIMUM_PROFILE_TRACE_DEPTH + 3; index++) + profile_trace_end(name_c); + /* an end that is not the top's, and one with nothing open */ + profile_trace_begin(name_a); + profile_trace_end(name_b); + profile_trace_end(name_a); + profile_trace_end(name_b); + /* aggregate scopes: summed for the frame, no records */ + profile_trace_begin(name_texture); + profile_trace_begin(name_inner_aggregate); + advance(500); + profile_trace_begin(name_aggregate_child); + advance(1000); + profile_trace_end(name_aggregate_child); + profile_trace_end(name_inner_aggregate); + advance(500); + profile_trace_end(name_texture); + for (index = 0; index < 5; index++) + { + profile_trace_begin(name_texture); + advance(2000); + profile_trace_end(name_texture); + } + /* a scope left open is dropped at the boundary, and counted */ + profile_trace_begin(name_a); + frame(1); + check(profile_trace_request_stop() == _profile_trace_answer_stopping, "profile_stop stops"); + check(profile_trace_recording(), "a stop applies at the next boundary"); + profile_trace_frame_boundary(); + check(!profile_trace_recording(), "stopped at the boundary"); + check(profile_trace_request_start(0.0, _profile_trace_when_now, 4) == _profile_trace_answer_still_writing || + writer_finished(), "profile_record while writing is refused"); + finish(); + check(written.finished == 1 && written.parts == 1, "one part written"); + check(written.counts[name_frame] == 2, "two frames recorded"); + check(written.counts[name_a] == 5 && written.counts[name_b] == 4, "every scope recorded once"); + check(written.counts[name_c] == MAXIMUM_PROFILE_TRACE_DEPTH, "scopes within the stack recorded"); + check(written.deep == 3, "scopes past the stack counted"); + check(written.unbalanced == 3, "unbalanced ends and the open scope counted"); + check(written.counts[name_texture] == 0 && written.counter_records >= 2, "aggregate names give counters"); + check(written.aggregate_child_depth == 0, "ordinary scopes inside an aggregate keep visible depth"); + check(written.aggregate_count[name_texture] == 6 && written.aggregate_time[name_texture] == 12000, + "outer aggregate totals include calls nested with another aggregate"); + check(written.aggregate_count[name_inner_aggregate] == 1 && written.aggregate_time[name_inner_aggregate] == 1500, + "nested aggregate keeps its own cpu summary count and time"); + check(written.counts[name_aggregate_child] == 1, "ordinary nested scope contributes one cpu summary count"); + check(written.stop_reason[0] == _profile_trace_stop_command, "stopped by the command"); + check(outstanding() == 0, "both arenas freed after the recording"); +} + +static void state_checks(void) +{ + struct profile_trace_status status; + + /* game: armed until a game is in progress, stopped by a map load, then + armed again */ + forget_written(); + fake_ready = 0; + fake_map = ""; + fake_map_name = ""; + check(profile_trace_request_start(0.0, _profile_trace_when_game, 4) == _profile_trace_answer_armed, "armed for a game"); + frame(1); + frame(1); + profile_trace_status(&status); + check(status.state == _profile_trace_armed, "waits for a game"); + fake_ready = 1; + frame(1); + check(profile_trace_recording(), "records once a game is in progress"); + frame(1); + fake_map = "levels\\next\\next"; + fake_map_name = "next"; + profile_trace_map_loaded(); + frame(1); + check(!profile_trace_recording(), "a map load stops it"); + finish(); + fake_map = "levels\\a30\\a30"; + fake_map_name = "a30"; + profile_trace_status(&status); + check(status.state == _profile_trace_armed, "armed again for the next game"); + check(written.stop_reason[0] == _profile_trace_stop_map_load, "stopped by the map load"); + check(strcmp(written.map_name[0], "next") != 0, "the map that stops a recording is not sampled into it"); + frame(1); + check(profile_trace_recording(), "the next game is recorded"); + check(profile_trace_request_stop() == _profile_trace_answer_stopping, "stopped by hand"); + frame(1); + finish(); + profile_trace_status(&status); + check(status.state == _profile_trace_idle, "a stop by hand does not arm again"); + check(outstanding() == 0, "nothing left allocated"); + + /* the seconds of profile_record */ + forget_written(); + profile_trace_request_start(0.5, _profile_trace_when_now, 4); + frame(1); + advance(400000000ULL); + frame(1); + check(profile_trace_recording(), "still recording before its seconds"); + advance(200000000ULL); + frame(1); + check(!profile_trace_recording(), "stopped after its seconds"); + finish(); + check(written.stop_reason[0] == _profile_trace_stop_seconds, "stopped by its seconds"); + + /* no memory: not recording, said so, nothing kept */ + allocations_refused = 1; + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + frame(1); + allocations_refused = 0; + profile_trace_status(&status); + check(status.state == _profile_trace_idle && strstr(last_notice, "no memory for 4 MB"), "no memory is reported"); + check(outstanding() == 0, "a failed allocation keeps nothing"); +} + +static volatile int p2p_running; +static unsigned long p2p_recorded; + +static void *p2p_thread(void *unused) +{ + (void)unused; + profile_trace_thread_register(_profile_track_p2p); + while (__atomic_load_n(&p2p_running, __ATOMIC_RELAXED)) + { + fake_lock(); + if (profile_trace_recording()) + p2p_recorded++; + profile_trace_begin(name_pass); + profile_trace_end(name_pass); + fake_unlock(); + /* (a pass every 50 microseconds: the p2p region holds an eighth of + the scopes, as the game's p2p thread has far fewer) */ + usleep(50); + } + return NULL; +} + +/* what profile_stop answers while a recording is only armed: a game is waited for +only by "game" mode, which stop ends too */ +static void stop_answer_checks(void) +{ + struct profile_trace_status status; + + check(profile_trace_request_start(0.0, _profile_trace_when_now, 4) == _profile_trace_answer_armed, "armed for the next frame"); + check(profile_trace_request_stop() == _profile_trace_answer_not_recording, + "stopped before it began, a recording armed for the next frame was waiting for no game"); + profile_trace_status(&status); + check(status.state == _profile_trace_idle, "stopped while armed, it does not start"); + check(profile_trace_request_start(0.0, _profile_trace_when_game, 4) == _profile_trace_answer_armed, "armed for a game"); + check(profile_trace_request_stop() == _profile_trace_answer_disarmed, "a recording armed for a game is not waited for any more"); + profile_trace_status(&status); + check(status.state == _profile_trace_idle, "stopped while armed for a game, it does not start"); +} + +static void part_checks(void) +{ + pthread_t thread; + unsigned long frames_recorded = 0; + int frame_index, part; + int continuous = 1; + + forget_written(); + p2p_recorded = 0; + p2p_running = 1; + pthread_create(&thread, NULL, p2p_thread, NULL); + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + /* (4 MB, CPU-only: about 114 000 game records a part) */ + for (frame_index = 0; frame_index < 3000; frame_index++) + { + frame(50); + if (profile_trace_recording()) + frames_recorded++; + if (frame_index % 30 == 0) + profile_trace_tick(); + advance(1000000); + } + profile_trace_request_stop(); + profile_trace_frame_boundary(); + __atomic_store_n(&p2p_running, 0, __ATOMIC_RELAXED); + pthread_join(thread, NULL); + finish(); + fake_role = "host"; + fake_players = 2; + fake_own_machine = -1; + check(written.parts >= 3, "a long recording is cut into parts"); + check(written.counts[name_frame] == frames_recorded, "every frame in exactly one part"); + check(written.counts[name_a] == frames_recorded * 50, "every scope in exactly one part"); + check(written.counts[name_pass] == p2p_recorded, "every recorded p2p pass in exactly one part"); + check(written.dropped == 0, "nothing dropped"); + for (part = 1; part < written.parts && part < MAXIMUM_PARTS; part++) + { + continuous &= written.first_frame[part] == written.first_frame[part - 1] + written.frames[part - 1]; + continuous &= written.start_ns[part] == written.end_ns[part - 1]; + continuous &= !written.last[part - 1]; + } + check(continuous, "frames and time continue across parts"); + check(written.last[written.parts - 1], "only the last part is the last"); + check(written.records_in_span, "each record within its part"); + check(outstanding() == 0, "both arenas freed after a long recording"); + + /* a cut while the writer holds the other arena waits for it */ + forget_written(); + written.delay_milliseconds = 1000; + p2p_recorded = 0; + p2p_running = 1; + pthread_create(&thread, NULL, p2p_thread, NULL); + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + frames_recorded = 0; + for (frame_index = 0; frame_index < 3000; frame_index++) + { + frame(50); + if (profile_trace_recording()) + frames_recorded++; + } + profile_trace_request_stop(); + profile_trace_frame_boundary(); + __atomic_store_n(&p2p_running, 0, __ATOMIC_RELAXED); + pthread_join(thread, NULL); + finish(); + written.delay_milliseconds = 0; + check(written.records_in_span, "each record within its part while the writer is slow"); + check(written.counts[name_pass] == p2p_recorded, "a slow writer loses no p2p pass"); + check(written.parts >= 3, "parts while the writer is slow"); + check(written.writer_wait_ms > 0.0, "the wait for the writer is counted"); + check(written.counts[name_a] == frames_recorded * 50, "a slow writer loses nothing"); + check(written.counts[profile_trace_name("profile_wait")] >= 1, "the wait is a scope"); +} + +static int straddle_begun; +static int straddle_released; + +/* a p2p pass that lets go of its lock for slow work (p2p_resolve's DNS) */ +static void *straddling_pass(void *unused) +{ + (void)unused; + profile_trace_thread_register(_profile_track_p2p); + fake_lock(); + profile_trace_begin(name_pass); + fake_unlock(); + __atomic_store_n(&straddle_begun, 1, __ATOMIC_RELEASE); + while (!__atomic_load_n(&straddle_released, __ATOMIC_ACQUIRE)) + usleep(100); + fake_lock(); + profile_trace_end(name_pass); + fake_unlock(); + return NULL; +} + +static void straddle(int cut) +{ + pthread_t thread; + struct profile_trace_status status; + + forget_written(); + straddle_begun = straddle_released = 0; + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + frame(1); + pthread_create(&thread, NULL, straddling_pass, NULL); + while (!__atomic_load_n(&straddle_begun, __ATOMIC_ACQUIRE)) + usleep(100); + if (cut) + { + /* frames until a cut comes, while the pass is out of its lock */ + do + { + frame(50); + profile_trace_status(&status); + } + while (status.part < 2); + } + else + { + profile_trace_request_stop(); + profile_trace_frame_boundary(); + } + __atomic_store_n(&straddle_released, 1, __ATOMIC_RELEASE); + pthread_join(thread, NULL); + if (cut) + { + profile_trace_request_stop(); + profile_trace_frame_boundary(); + } + finish(); +} + +/* a pass begun in one recording and ended in the next (a stop, a re-arm and +a start between): its start is before the new origin, so it is dropped, and +counted in the new recording. concurrent: it ends while the start is made */ +static void straddle_restart(int concurrent) +{ + pthread_t thread; + + forget_written(); + straddle_begun = straddle_released = 0; + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + frame(1); + pthread_create(&thread, NULL, straddling_pass, NULL); + while (!__atomic_load_n(&straddle_begun, __ATOMIC_ACQUIRE)) + usleep(100); + profile_trace_request_stop(); + profile_trace_frame_boundary(); + finish(); + forget_written(); + advance(1000000); + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + if (concurrent) + { + __atomic_store_n(&straddle_released, 1, __ATOMIC_RELEASE); + frame(1); + } + else + { + frame(1); + __atomic_store_n(&straddle_released, 1, __ATOMIC_RELEASE); + } + pthread_join(thread, NULL); + frame(1); + profile_trace_request_stop(); + profile_trace_frame_boundary(); + finish(); +} + +static void straddle_checks(void) +{ + straddle(1); + check(written.counts[name_pass] == 1, "a pass across a cut is in the part it ended in, once"); + straddle(0); + check(written.counts[name_pass] == 0, "a pass that outlives its recording is not written"); + check(outstanding() == 0, "nothing left after a pass across a stop"); + straddle_restart(0); + check(written.counts[name_pass] == 0, "a pass across a restart is dropped, not given a wrapped start"); + check(written.dropped == 1, "and counted in the new recording"); + straddle_restart(1); + check(written.counts[name_pass] == 0, "a pass ending as the next recording starts is dropped"); + check(outstanding() == 0, "nothing left after a pass across a restart"); +} + +static void lifetime_checks(void) +{ + int cycle; + + /* many recordings, cut and not, stopped every way, with nothing left */ + for (cycle = 0; cycle < 40; cycle++) + { + int frames = cycle % 4 == 0 ? 2500 : 3; + int index; + + forget_written(); + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + for (index = 0; index < frames; index++) + frame(50); + if (cycle % 3 == 0) + { + profile_trace_map_loaded(); + profile_trace_frame_boundary(); + } + else if (cycle % 3 == 1) + { + profile_trace_request_stop(); + profile_trace_frame_boundary(); + } + else + { + profile_trace_shutdown(); + } + finish(); + if (outstanding() != 0) + { + check(0, "a recording leaves nothing allocated"); + break; + } + } + check(allocations_made > 80, "the cycles allocated"); + /* exit while armed, while writing, and twice */ + profile_trace_request_start(0.0, _profile_trace_when_game, 4); + fake_ready = 0; + profile_trace_shutdown(); + profile_trace_shutdown(); + check(outstanding() == 0, "shutdown while armed keeps nothing"); + fake_ready = 1; +} + +/* a real recording through the real writer, into the folder */ +static void file_checks(const char *folder) +{ + struct profile_trace_status status; + int frame_index; + int quoted; + + fake_folder = folder; + profile_trace_set_writer(NULL); + profile_trace_set_session_sampler(fake_session_sample); + quoted = profile_trace_name("quote\"back\\slash"); + fake_map = ""; + fake_map_name = ""; + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + fake_role = "client"; + fake_players = 5; + fake_own_machine = 1; + fake_map = "levels\\a30\\a30"; + fake_map_name = "a30"; + for (frame_index = 0; frame_index < 2400; frame_index++) + { + if (frame_index == 2) + { + fake_map = "levels\\b30\\b30"; + fake_map_name = "b30"; + } + frame(40); + profile_trace_begin(name_texture); + profile_trace_begin(name_inner_aggregate); + advance(100); + profile_trace_end(name_inner_aggregate); + profile_trace_begin(name_aggregate_child); + advance(100); + profile_trace_end(name_aggregate_child); + profile_trace_end(name_texture); + profile_trace_begin(quoted); + advance(10); + profile_trace_end(quoted); + if (frame_index % 2 == 0) + { + profile_trace_begin(name_tick); + profile_trace_tick(); + advance(100); + profile_trace_end(name_tick); + } + advance(16000000); + } + profile_trace_shutdown(); + check(session_sample_calls > 0 && session_sample_calls < 2400, + "session sampling is periodic and not per frame"); + fake_role = "host"; + fake_players = 2; + fake_own_machine = -1; + fake_map = "levels\\a30\\a30"; + fake_map_name = "a30"; + check(outstanding() == 0, "the real writer frees both arenas"); + profile_trace_status(&status); + { + char name[64]; + char path[512]; + FILE *file; + + profile_json_choose_name(folder, "20261005-142233", "host", name, sizeof(name)); + check(strcmp(name, "profile_20261005-142233_host") == 0, "a free name is taken as it is"); + snprintf(path, sizeof(path), "%s/%s.part1.json", folder, name); + file = fopen(path, "wb"); + if (file) + fclose(file); + profile_json_choose_name(folder, "20261005-142233", "host", name, sizeof(name)); + check(strcmp(name, "profile_20261005-142233_host_2") == 0, "a name taken gets _2"); + remove(path); + } +} + +/* a folder that cannot be written: each part is logged and given up, its +.tmp gone, the arenas freed, and the recording still finishes */ +static void unwritable_checks(const char *folder) +{ + char missing[512]; + int frame_index; + + snprintf(missing, sizeof(missing), "%s/missing/deeper", folder); + fake_folder = missing; + last_log[0] = 0; + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + for (frame_index = 0; frame_index < 2400; frame_index++) + frame(40); + profile_trace_request_stop(); + profile_trace_frame_boundary(); + finish(); + check(strstr(last_log, "cannot write") && strstr(last_log, "missing/deeper"), "a part that cannot be written is logged"); + check(outstanding() == 0, "a part that cannot be written frees its arena"); + fake_folder = folder; +} + +static int refusing_writer_start(void) +{ + return 0; +} + +/* a start that fails after it has allocated: nothing is kept, it says so, and +a recording of "game" mode is not armed again to fail at every game */ +static void release_path_checks(void) +{ + static const char *const what[] = { "the second arena refused", "the writer thread cannot start" }; + int fail, game; + + for (fail = 0; fail < 2; fail++) + { + for (game = 0; game < 2; game++) + { + struct profile_trace_status status; + char text[96]; + + snprintf(text, sizeof(text), "%s, %s mode", what[fail], game ? "game" : "now"); + forget_written(); + fake_ready = 1; + last_notice[0] = 0; + if (fail == 0) + allocations_allowed = 1; + else + profile_trace_set_writer(refusing_writer_start); + profile_trace_request_start(0.0, game ? _profile_trace_when_game : _profile_trace_when_now, 4); + frame(1); + allocations_allowed = -1; + profile_trace_set_writer(fake_writer_start); + profile_trace_status(&status); + check(outstanding() == 0, text); + check(status.state == _profile_trace_idle, text); + check(strstr(last_notice, fail == 0 ? "no memory for 4 MB" : "the writer thread cannot start") != NULL, text); + check(!profile_trace_recording(), text); + frame(1); + profile_trace_status(&status); + check(status.state == _profile_trace_idle && outstanding() == 0, text); + } + } +} + +/* a worst frame's ticks and longest scopes come from any depth of the frame: +game_tick sits under game_time_update, and the time a scope has of its own is +what ranks it (its parents would always come first otherwise) */ +static void worst_frame_checks(void) +{ + struct profile_trace_record game[16]; + struct profile_part *part = calloc(1, sizeof(*part)); + int frame_id = profile_trace_name("frame"); + int update = profile_trace_name("game_time_update"); + int tick = profile_trace_name("game_tick"); + int objects = profile_trace_name("game_tick.objects"); + int ai = profile_trace_name("game_tick.ai"); + int render = profile_trace_name("render"); + int bsp = profile_trace_name("render.bsp"); + int texture = profile_trace_name("texture"); + char text[65536]; + unsigned long count = 0, length; + FILE *file = tmpfile(); + const char *row; + + /* (milliseconds, as nanoseconds below) */ +#define WORST_RECORD(start_ms, duration_ms, id, level) \ + do { game[count].start = (start_ms) * 1000000ULL; game[count].duration = (duration_ms) * 1000000U; \ + game[count].name = (unsigned short)(id); game[count].depth = (level); game[count].track = 0; count++; } while (0) + WORST_RECORD(0, 100, frame_id, 0); + WORST_RECORD(0, 60, update, 1); + WORST_RECORD(0, 30, tick, 2); + WORST_RECORD(0, 20, objects, 3); + WORST_RECORD(30, 25, tick, 2); + WORST_RECORD(30, 22, ai, 3); + WORST_RECORD(60, 35, render, 1); + WORST_RECORD(60, 10, bsp, 2); + /* (an aggregate's time is a counter record, not a scope) */ + WORST_RECORD(5, 99, texture, PROFILE_TRACE_DEPTH_AGGREGATE_TIME); + WORST_RECORD(100, 5, frame_id, 0); +#undef WORST_RECORD + part->records[_profile_track_game] = game; + part->record_counts[_profile_track_game] = count; + part->name_count = profile_trace_name("worst_frame_last") + 1; + part->ticks = 2; + check(file != NULL, "a scratch file for the worst frames"); + if (!file) + { + free(part); + return; + } + check(profile_json_write(part, file), "a part with nested scopes is written"); + rewind(file); + length = fread(text, 1, sizeof(text) - 1, file); + text[length] = 0; + fclose(file); + row = strstr(text, "\"worst_frames\""); + check(row && strstr(row, "[0, 0.000, 100.000, 2, [[\"render\", 25.000], [\"game_tick.ai\", 22.000], " + "[\"game_tick.objects\", 20.000]]]"), + "the worst frame runs its two ticks under game_time_update and names the scopes with the most time of their own"); + check(row && strstr(row, "[1, 0.100, 5.000, 0, []]"), "a frame with no scopes in it has no ticks and no longest"); + free(part); +} + +/* the reset race: a p2p pass begun in one recording ends, from inside +the next recording's allocator seam, before that start resets the tracks' +counts. Only the track lock orders its bump before the reset, which +ThreadSanitizer checks (the reset moved out of the lock is a race) */ +static int race_armed; +static int race_ended; + +static void *race_pass(void *unused) +{ + (void)unused; + profile_trace_thread_register(_profile_track_p2p); + fake_lock(); + profile_trace_begin(name_pass); + fake_unlock(); + __atomic_store_n(&straddle_begun, 1, __ATOMIC_RELEASE); + /* (relaxed: no happens-before from the game thread's release) */ + while (!__atomic_load_n(&straddle_released, __ATOMIC_RELAXED)) + usleep(100); + fake_lock(); + profile_trace_end(name_pass); + fake_unlock(); + __atomic_store_n(&race_ended, 1, __ATOMIC_RELAXED); + return NULL; +} + +static void *race_allocate(unsigned long size) +{ + if (__atomic_load_n(&race_armed, __ATOMIC_RELAXED)) + { + __atomic_store_n(&race_armed, 0, __ATOMIC_RELAXED); + __atomic_store_n(&straddle_released, 1, __ATOMIC_RELAXED); + while (!__atomic_load_n(&race_ended, __ATOMIC_RELAXED)) + usleep(100); + } + return fake_allocate(size); +} + +static void reset_race_checks(void) +{ + pthread_t thread; + + profile_trace_set_allocator(race_allocate, fake_release); + forget_written(); + straddle_begun = straddle_released = 0; + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + frame(1); + pthread_create(&thread, NULL, race_pass, NULL); + while (!__atomic_load_n(&straddle_begun, __ATOMIC_ACQUIRE)) + usleep(100); + profile_trace_request_stop(); + profile_trace_frame_boundary(); + finish(); + + forget_written(); + advance(1000000); + __atomic_store_n(&race_armed, 1, __ATOMIC_RELAXED); + profile_trace_request_start(0.0, _profile_trace_when_now, 4); + frame(1); + pthread_join(thread, NULL); + frame(1); + profile_trace_request_stop(); + profile_trace_frame_boundary(); + finish(); + check(written.counts[name_pass] == 0, "the pass of the last recording is not written"); + check(written.dropped == 0, "a pass ended before the start is not counted in the new recording"); + check(outstanding() == 0, "the race recordings leave nothing allocated"); + profile_trace_set_allocator(fake_allocate, fake_release); +} + +int main(int argc, char **argv) +{ + profile_trace_set_clock(fake_clock); + profile_trace_set_track_lock(fake_lock, fake_unlock); + profile_trace_set_allocator(fake_allocate, fake_release); + profile_trace_set_log(quiet_log); + profile_trace_set_notify(fake_notify); + profile_trace_set_writer(fake_writer_start); + profile_trace_set_ready(fake_ready_test); + profile_trace_set_session(fake_session); + profile_trace_set_session_sampler(fake_session_sample); + gametype_checks(); + profile_trace_thread_register(_profile_track_game); + name_frame = profile_trace_name("frame"); + name_tick = profile_trace_name("game_tick"); + name_a = profile_trace_name("game_tick.objects"); + name_b = profile_trace_name("objects_update"); + name_c = profile_trace_name("deep"); + name_pass = profile_trace_name("p2p.pass"); + name_aggregate_child = profile_trace_name("aggregate_child"); + profile_trace_name_aggregate("texture"); + profile_trace_name_aggregate("render_model"); + name_texture = profile_trace_name("texture"); + name_inner_aggregate = profile_trace_name("render_model"); + fake_ready = 1; + written.records_in_span = 1; + + scope_checks(); + state_checks(); + stop_answer_checks(); + part_checks(); + straddle_checks(); + lifetime_checks(); + worst_frame_checks(); + release_path_checks(); + reset_race_checks(); + if (argc > 1) + { + file_checks(argv[1]); + unwritable_checks(argv[1]); + } + printf("%s (%d failures)\n", failures ? "FAIL" : "PASS", failures); + return failures != 0; +} diff --git a/tools/profile_fixture.py b/tools/profile_fixture.py new file mode 100644 index 00000000..79ecc683 --- /dev/null +++ b/tools/profile_fixture.py @@ -0,0 +1,66 @@ +"""Hand-written CPU recording parts for the report tests.""" + +import json +from pathlib import Path + + +def part(number, intervals, last, frames_before, ticks_before): + frames = 60 * len(intervals) + ticks = 30 * len(intervals) + header = { + "format": 1, "build": "0 debug profile", "platform": "windows", "role": "host", "own_machine": -1, + "map": "levels\\a30\\a30", "map_name": "a30", "gametype": "campaign", + "players": 3 if number == 1 else 4, "players_most": 4, "start_utc": "2026-10-05 14:22:33", + "recording": "profile_20261005-142233_host", "part": number, "last_part": last, + "first_frame": frames_before, "frames": frames, + "first_tick": ticks_before, "ticks": ticks, "start_s": float(intervals[0]), "duration_s": float(len(intervals)), + "stop_reason": "command" if last else "", "clock_read_ns": 21.5, "memory_used": 1000, "memory_limit": 16777216, + "writer_wait_ms": 0.0, "foreign_scopes": 0, "deep_scopes": 0, "unbalanced_scopes": 0, "dropped_scopes": 0, + "dropped_scopes": 0, + } + halo = {"header": header} + cpu_rows = [ + ["frame", frames, 8.3 * frames, 8.3, 41.2, 1.0, None], + ["game_tick", ticks, 3.2 * ticks, 3.2, 6.012, 0.5, 3.2], + ["game_tick.objects", ticks, 1.4 * ticks, 1.4, 12.1, 0.5, 1.4], + ["network_distributed_tick", ticks, 0.6 * ticks, 0.6, 4.0, 0.5, 0.6], + ["render", frames, 4.0 * frames, 4.0, 9.8, 1.0, None], + ] + worst = [[frames_before + 12, frames_before / 60.0 + 0.2, 41.2 - number, 2, + [["game_tick.objects", 12.1], ["render", 9.8], ["network_distributed_tick", 4.0]]]] + cpu = {"frames": frames, "ticks": ticks, + "columns": ["name", "count", "total_ms", "mean_ms", "max_ms", "per_frame", "per_tick_ms"], + "rows": cpu_rows, "worst_frames": {"columns": ["frame", "at_s", "frame_ms", "ticks", "longest"], + "rows": worst}} + return halo, cpu + + +def write_part(path, halo, cpu): + """the writer's layout: one item per line, the events last""" + lines = ['{"halo": {', '"header": ' + json.dumps(halo["header"])] + for name, table in halo.items(): + if name == "header": + continue + lines[-1] += "," + lines.append(f'"{name}": {{"columns": {json.dumps(table["columns"])}, "rows": [') + lines.extend(json.dumps(row) + ("," if index < len(table["rows"]) - 1 else "") + for index, row in enumerate(table["rows"])) + lines.append("]}") + lines[-1] += "}," + lines.append('"cpu_summary": ' + json.dumps(cpu) + ",") + lines.append('"traceEvents": [') + lines.append('{"ph":"M","name":"process_name","pid":1,"args":{"name":"halo host windows"}}') + lines.append("],") + lines.append('"displayTimeUnit": "ms"}') + Path(path).write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def write_recording(folder, name="profile_20261005-142233_host", split=True): + """three parts of two intervals each, or the same six in one part""" + folder = Path(folder) + folder.mkdir(parents=True, exist_ok=True) + spans = [[0, 1], [2, 3], [4, 5]] if split else [[0, 1, 2, 3, 4, 5]] + for index, intervals in enumerate(spans): + halo, cpu = part(index + 1, intervals, index == len(spans) - 1, intervals[0] * 60, intervals[0] * 30) + write_part(folder / f"{name}.part{index + 1}.json", halo, cpu) + return folder / name diff --git a/tools/profile_fixture.summary.txt b/tools/profile_fixture.summary.txt new file mode 100644 index 00000000..1ba3af2a --- /dev/null +++ b/tools/profile_fixture.summary.txt @@ -0,0 +1,35 @@ +# halo profile summary, format 1 +# recording: profile_20261005-142233_host, parts 1-3 (complete) +# build: 0 debug profile, platform: windows, role: host, own_machine: - +# map: levels\a30\a30, players: 3 at start, 4 most +# recorded: 2026-10-05 14:22:33 UTC, 6.0 s, 180 ticks, 360 frames, stopped: command +# problems: none +# units: ms = milliseconds + +## columns +scope CPU scope name (a profile section or frame timer); nested names use "." +count times the scope ran in the recording +total_ms summed inclusive time (children included), milliseconds +mean_ms total_ms / count +max_ms longest single run; aggregate sections: largest summed time in one frame +per_frame count / frames recorded +per_tick_ms total_ms / ticks recorded (game_tick.* and network_distributed_tick* only) +frame frame number in the recording +at_s seconds from the start of the recording to that frame +frame_ms that frame's duration +ticks game ticks that frame ran +longest scopes (ms) the frame's three scopes, at any depth, with the most time of their own (children not counted), name and that time + +## cpu scopes (top 40 by total_ms) +scope count total_ms mean_ms max_ms per_frame per_tick_ms +frame 360 2988.0 8.300 41.200 1.00 - +render 360 1440.0 4.000 9.800 1.00 - +game_tick 180 576.0 3.200 6.012 0.50 3.200 +game_tick.objects 180 252.0 1.400 12.100 0.50 1.400 +network_distributed_tick 180 108.0 0.600 4.000 0.50 0.600 + +## worst frames (10) +frame at_s frame_ms ticks longest scopes (ms) + 12 0.20 40.20 2 game_tick.objects 12.1, render 9.8, network_distributed_tick 4.0 + 132 2.20 39.20 2 game_tick.objects 12.1, render 9.8, network_distributed_tick 4.0 + 252 4.20 38.20 2 game_tick.objects 12.1, render 9.8, network_distributed_tick 4.0 diff --git a/tools/test_profile.py b/tools/test_profile.py new file mode 100644 index 00000000..d3533ecf --- /dev/null +++ b/tools/test_profile.py @@ -0,0 +1,569 @@ +"""Profiling build, CPU recorder, written parts and CPU report tests. +For local WSL runs in a Windows worktree, set HALO_GIT=git.exe. +""" + +import io +import json +import os +import re +import shlex +import shutil +import subprocess +import tarfile +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from tools import linux_build, net_report, ninja_syntax, profile_fixture + +ROOT = Path(__file__).resolve().parent.parent +GIT = os.environ.get("HALO_GIT", "git") + + +# ---------- the build switch + + +def test_configuration_defines(): + assert linux_build.configuration_defines(SimpleNamespace()) == [] + assert linux_build.configuration_defines(SimpleNamespace(port_release=True)) == ["-DHALO_RELEASE"] + assert linux_build.configuration_defines(SimpleNamespace(port_release=True, port_profile=True)) == [ + "-DHALO_RELEASE", "-DHALO_PROFILE"] + + +def test_every_native_build_takes_the_configuration_defines(): + # (the guest, the game and the platform layer of all three: one place + # decides what --release and --profile define) + for name in ("linux_build.py", "windows_build.py", "android_build.py"): + text = (ROOT / "tools" / name).read_text(encoding="utf-8") + assert "configuration_defines(sln)" in text, name + assert '["-DHALO_RELEASE"] if' not in text, name + + +def test_profile_refuses_pgo_training(): + with pytest.raises(ValueError, match="--pgo=train"): + linux_build.check_profile_options(True, "train") + linux_build.check_profile_options(True, "use") + linux_build.check_profile_options(True, "off") + linux_build.check_profile_options(False, "train") + + +def test_linux_build_compiles_with_the_profile_define(monkeypatch): + monkeypatch.chdir(ROOT) + out = io.StringIO() + linux_build.generate_linux_build(ninja_syntax.Writer(out), SimpleNamespace( + build_dir=Path("build"), linux_cc="clang", compiler_launcher=None, port_release=False, port_lto="off", + port_portable=True, port_pgo="off", port_pgo_profile=None, port_profile=True)) + # (ninja_syntax wraps long lines with " $"; Windows paths have backslashes) + text = re.sub(r" \$\n *", " ", out.getvalue()).replace("\\", "/") + for obj in ("build/linux/obj/source/game/game.o", "build/linux/obj/port/linux/src/p2p.o"): + block = text[text.index(f"build {obj}:"):] + cflags = next(line for line in block.splitlines() if line.strip().startswith("cflags = ")) + assert "-DHALO_PROFILE" in cflags, obj + + +# ---------- the recording's C units (tools/profile_check.c) + +PROFILE_SOURCES = ["port/linux/src/profile_trace.c", "port/linux/src/profile_json.c"] + + +def check_compiler(): + """a C compiler of this machine with POSIX threads, or None (Windows has + them only through the port's own layer, so the check runs on Linux and + in WSL)""" + if os.name == "nt": + return None + for name in ("clang", "cc", "gcc"): + if shutil.which(name): + return name + return None + + +def build_check(tmp_path, sanitizers): + compiler = check_compiler() + if not compiler: + pytest.skip("needs a C compiler with POSIX threads (Linux, WSL)") + program = tmp_path / ("profile_check" + "".join(f"_{name}" for name in sanitizers)) + flags = [f"-fsanitize={','.join(sanitizers)}", "-fno-omit-frame-pointer"] if sanitizers else [] + built = subprocess.run([compiler, "-std=gnu11", "-Wall", "-Werror", "-DHALO_PROFILE", "-pthread", "-g", "-O1", + *flags, "-idirafter", "port/linux/include", "-Iport/linux/src", + "-o", str(program), + "tools/profile_check.c", *PROFILE_SOURCES], + cwd=ROOT, capture_output=True, text=True) + if built.returncode != 0 and sanitizers and "sanitize" in built.stderr: + pytest.skip(f"{compiler} cannot build with {sanitizers}") + assert built.returncode == 0, built.stderr[-4000:] + return program + + +def run_check(program, folder): + folder.mkdir(exist_ok=True) + result = subprocess.run([str(program), str(folder)], capture_output=True, text=True, timeout=300, + env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1", "TSAN_OPTIONS": "halt_on_error=1"}) + assert result.returncode == 0, result.stdout + result.stderr[-4000:] + assert "PASS" in result.stdout + + +def test_check_program_passes(tmp_path): + run_check(build_check(tmp_path, []), tmp_path / "out") + + +def test_check_program_has_no_leaks_or_memory_errors(tmp_path): + """AddressSanitizer and LeakSanitizer over every recording the check + makes: arenas, the writer's files, the parts""" + run_check(build_check(tmp_path, ["address", "undefined"]), tmp_path / "out") + + +def test_check_program_has_no_data_races(tmp_path): + """ThreadSanitizer: the p2p track, the writer hand-off, the recording + flag""" + run_check(build_check(tmp_path, ["thread"]), tmp_path / "out") + + +@pytest.fixture(scope="module") +def written_parts(tmp_path_factory): + folder = tmp_path_factory.mktemp("written") + run_check(build_check(tmp_path_factory.mktemp("build"), []), folder) + recordings = {} + for path in folder.glob("*.part*.json"): + try: + header = json.loads(path.read_text(encoding="utf-8"))["halo"]["header"] + except (OSError, json.JSONDecodeError, KeyError): + continue + recordings.setdefault(header["recording"], []).append((header["part"], path)) + parts = max(recordings.values(), key=len) + assert len(parts) >= 2, "the real C recording is split into parts" + return [path for number, path in sorted(parts)] + + +def test_written_parts_are_traces(written_parts): + for path in written_parts: + text = path.read_text(encoding="utf-8") + trace = json.loads(text) + assert list(trace) == ["halo", "cpu_summary", "traceEvents", "displayTimeUnit"] + assert text.startswith('{"halo": {\n"header": {') + assert trace["halo"]["header"]["last_part"] == (path == written_parts[-1]) + header = trace["halo"]["header"] + assert (header["role"], header["own_machine"], header["players"], header["players_most"]) == ( + "host", -1, 2, 5) + assert (header["map_name"], header["gametype"]) == ("b30", "campaign") + lines = text.splitlines() + events = lines.index('"traceEvents": [') + # one event a line, the scopes in order of their start + starts = [json.loads(line.rstrip(","))["ts"] for line in lines[events + 1:-2] if '"ph":"X"' in line] + assert starts == sorted(starts) + assert '"quote\\"back\\\\slash"' in text or path != written_parts[0] + + +def test_written_parts_nest_on_tracks(written_parts): + trace = json.loads(written_parts[0].read_text(encoding="utf-8")) + names = {event["args"]["name"] for event in trace["traceEvents"] if event["ph"] == "M"} + assert {"game", "p2p"} <= names + scopes = [event for event in trace["traceEvents"] if event["ph"] == "X" and event["tid"] == 1] + frame = next(event for event in scopes if event["name"] == "frame") + inside = [event for event in scopes if frame["ts"] <= event["ts"] < frame["ts"] + frame["dur"] and event is not frame] + assert inside and all(event["ts"] + event["dur"] <= frame["ts"] + frame["dur"] + 0.001 for event in inside) + + +def test_written_parts_import_in_perfetto(written_parts): + """trace_processor's import, when the perfetto package is installed (run + by hand on a real recording otherwise)""" + trace_processor = pytest.importorskip("perfetto.trace_processor") + with trace_processor.TraceProcessor(trace=str(written_parts[0])) as processor: + errors = processor.query("select name, value from stats where severity = 'error' and value > 0") + rows = list(errors) + assert not rows, [(row.name, row.value) for row in rows] + + +def test_written_parts_read_by_the_report(written_parts): + recording = net_report.Recording(written_parts[0]) + assert recording.complete and not recording.warnings + cpu_rows = [dict(zip(summary["columns"], row)) for summary in + [json.loads(path.read_text(encoding="utf-8"))["cpu_summary"] for path in written_parts] + for row in summary["rows"]] + totals = {} + for row in cpu_rows: + totals[row["name"]] = totals.get(row["name"], 0) + row["count"] + assert totals["texture"] == 2400 + assert totals["render_model"] == 2400 + assert totals["aggregate_child"] == 2400 + frames = sum(1 for path in written_parts for event in json.loads(path.read_text(encoding="utf-8"))["traceEvents"] + if event["ph"] == "X" and event["name"] == "frame") + assert recording.cpu["frames"] == frames + + +# ---------- tools/net_report.py + +EXPECTED_SUMMARY = ROOT / "tools" / "profile_fixture.summary.txt" + + +def cpu_recording(tmp_path, role="host"): + base = tmp_path / f"compat_{role}" + for number, start, duration in ((1, 0.0, 1.25), (2, 1.25, 2.75)): + halo, cpu = profile_fixture.part(number, [number - 1], number == 2, 0, 0) + halo["header"].update(role=role, start_s=start, duration_s=duration) + profile_fixture.write_part(Path(f"{base}.part{number}.json"), {"header": halo["header"]}, cpu) + return net_report.Recording(base) + + +def test_cpu_only_duration_comes_from_each_part_header(tmp_path): + recording = cpu_recording(tmp_path) + assert recording.select() == 4.0 + assert recording.select(0.5, 3.0) == 2.5 + assert recording.select(5.0, 6.0) == 0.0 + + + +def test_cpu_report_prints_and_exports_only_cpu_measurements(tmp_path, capsys): + base = profile_fixture.write_recording(tmp_path) + directory = tmp_path / "csv" + assert net_report.main([str(base), "--no-summary", "--csv", str(directory)]) == 0 + output = capsys.readouterr().out + assert "## cpu scopes" in output and "## worst frames" in output + assert "## traffic" not in output and "## message types" not in output + assert "wire_rtt_ms" not in output and "msgs/s" not in output + assert {path.name.rsplit(".", 2)[1] for path in directory.glob("*.csv")} == {"cpu", "worst_frames"} + + +@pytest.fixture +def host(tmp_path): + return profile_fixture.write_recording(tmp_path) + + +def test_report_summary_is_the_expected_one(host): + assert net_report.main([str(host) + ".part2.json"]) == 0 + written = Path(str(host) + ".summary.txt").read_text(encoding="ascii") + assert written == EXPECTED_SUMMARY.read_text(encoding="ascii") + assert len(written.splitlines()) <= net_report.SUMMARY_LINE_LIMIT + + +def test_report_uses_players_most_and_accepts_old_headers(tmp_path): + base = profile_fixture.write_recording(tmp_path / "legacy") + paths = [Path(f"{base}.part{number}.json") for number in (1, 2, 3)] + for index, path in enumerate(paths): + data = json.loads(path.read_text(encoding="utf-8")) + header = data["halo"]["header"] + for key in ("map_name", "gametype", "players_most"): + header.pop(key, None) + header["players"] = 1 + index + profile_fixture.write_part(path, data["halo"], data["cpu_summary"]) + old_parts_report = net_report.Recording(paths[0]) + old_summary = net_report.summary_lines(old_parts_report, None, None, 40) + assert any("players: 1 at start, 3 most" in line for line in old_summary) + assert net_report.main([str(paths[0]), "--no-summary"]) == 0 + + +def test_report_summary_uses_current_players_most(tmp_path): + base = profile_fixture.write_recording(tmp_path / "current") + last = Path(f"{base}.part3.json") + data = json.loads(last.read_text(encoding="utf-8")) + data["halo"]["header"]["players_most"] = 9 + profile_fixture.write_part(last, data["halo"], data["cpu_summary"]) + recording = net_report.Recording(Path(f"{base}.part1.json")) + summary = net_report.summary_lines(recording, None, None, 40) + assert any("players: 3 at start, 9 most" in line for line in summary) + + +def test_report_merges_parts_as_one(tmp_path): + split = profile_fixture.write_recording(tmp_path / "split") + whole = profile_fixture.write_recording(tmp_path / "whole", split=False) + assert net_report.main([str(split), "--no-summary", "--csv", str(tmp_path / "split_csv")]) == 0 + assert net_report.main([str(whole), "--no-summary", "--csv", str(tmp_path / "whole_csv")]) == 0 + for table in sorted((tmp_path / "split_csv").glob("*.csv")): + if table.name.endswith(".worst_frames.csv"): + continue + assert table.read_text() == (tmp_path / "whole_csv" / table.name).read_text(), table.name + + +def test_report_warns_about_a_missing_part(host, capsys): + Path(str(host) + ".part2.json").unlink() + net_report.main([str(host), "--no-summary"]) + captured = capsys.readouterr() + assert "part 2 of profile_20261005-142233_host is missing" in captured.err + assert "(incomplete)" in captured.out + + +def test_report_warns_about_no_last_part(host, capsys): + Path(str(host) + ".part3.json").unlink() + net_report.main([str(host), "--no-summary"]) + assert "has no last part: the recording is incomplete" in capsys.readouterr().err + + +def test_summary_glossary_has_one_line_per_printed_column(host): + net_report.main([str(host)]) + lines = Path(str(host) + ".summary.txt").read_text(encoding="ascii").splitlines() + start = lines.index("## columns") + 1 + end = lines.index("", start) + glossary = [re.split(r"\s{2,}", line, maxsplit=1)[0] for line in lines[start:end]] + printed = set() + for index, line in enumerate(lines[end:], end): + if line.startswith("## ") and index + 1 < len(lines) and lines[index + 1] not in ("", "(none)"): + printed.update(re.split(r"\s{2,}", lines[index + 1].strip())) + assert printed and len(glossary) == len(set(glossary)) + assert set(glossary) == printed + + +def test_summary_file_ignores_top(host): + assert net_report.main([str(host), "--top", "1"]) == 0 + written = Path(str(host) + ".summary.txt").read_text(encoding="ascii") + assert written == EXPECTED_SUMMARY.read_text(encoding="ascii") + assert len(written.splitlines()) <= net_report.SUMMARY_LINE_LIMIT + + +def test_report_top_limits_the_printed_tables(host, capsys): + net_report.main([str(host), "--no-summary", "--top", "1"]) + assert "... 4 more rows (net_report.py --top N)" in capsys.readouterr().out + + +def test_report_says_cpu_tables_cover_the_whole_recording(host, capsys): + net_report.main([str(host), "--no-summary", "--from", "2", "--to", "4"]) + out = capsys.readouterr().out + assert "# window: 2.0 s selected (seconds 2 to 4)" in out + assert "cpu tables, ticks and frames cover the whole recording" in out + assert "## cpu scopes (top 40 by total_ms, whole recording)" in out + assert "## worst frames (10, whole recording)" in out + + +def test_report_names_the_broken_part(host, capsys): + path = Path(str(host) + ".part2.json") + path.write_text(path.read_text().replace('"cpu_summary"', '"cpu_summery"'), encoding="utf-8") + assert net_report.main([str(host), "--no-summary"]) == 1 + err = capsys.readouterr().err + assert err.startswith("net_report: ") and "part2" in err and "cpu_summary" in err + path.write_text(Path(str(host) + ".part1.json").read_text().replace('"last_part"', '"last_prt"'), encoding="utf-8") + assert net_report.main([str(host), "--no-summary"]) == 1 + assert "last_part" in capsys.readouterr().err + + +def test_windows_compile_command_parser_preserves_quoted_define_and_paths(): + command = ('clang -I"source/saved films" -include port\\windows\\include\\prefix.h ' + '-I"C:\\Program Files\\LLVM\\include" ' + '-DHALO_BUILD_FLAVOR=\\"release\\" source\\game.c -o build\\game.o') + assert parse_compile_command(command, windows=True) == [ + "clang", "-Isource/saved films", "-include", "port/windows/include/prefix.h", + "-IC:/Program Files/LLVM/include", '-DHALO_BUILD_FLAVOR="release"', + "source/game.c", "-o", "build/game.o"] + + +def test_console_words_and_record_seconds_use_the_real_parser(tmp_path): + compiler = check_compiler() + if not compiler: + pytest.skip("needs a C compiler (Linux, WSL)") + text = (ROOT / "port/linux/game/profile_console.c").read_text() + word = text[text.index("static char const *profile_console_word("):text.index("/* ---------- public code */")] + command = text[text.index("boolean profile_console_command("):text.rindex("\n#endif")] + program = tmp_path / "console_check" + code = tmp_path / "console_check.c" + # Compile the game's actual parsing functions; only their external game effects are stubbed. + code.write_text(r''' +#include +#include +#include +#include "profile_trace.h" +typedef int boolean; +#define TRUE 1 +#define FALSE 0 +static int requests; +static double accepted_seconds; +static void profile_console_install(void) {} +static long config_integer(const char *name) { (void)name; return 32; } +static void console_printf(boolean flag, const char *format, ...) { (void)flag; (void)format; } +int profile_trace_request_start(double seconds, int when, long memory) { + assert(when == _profile_trace_when_now && memory == 32); + requests++; accepted_seconds = seconds; return _profile_trace_answer_armed; +} +int profile_trace_request_stop(void) { return _profile_trace_answer_not_recording; } +void profile_trace_status(struct profile_trace_status *status) { memset(status, 0, sizeof(*status)); } +''' + word + command + r''' +int main(void) { + assert(strcmp(profile_console_word(" (PROFILE_RECORD 1.5)", "profile_record"), "1.5)") == 0); + assert(profile_console_word("profile_record_extra", "profile_record") == NULL); + assert(profile_console_command("PROFILE_RECORD 1.5")); + assert(requests == 1 && accepted_seconds == 1.5); + assert(profile_console_command("profile_record x")); + assert(requests == 1); + assert(profile_console_command("profile_record -1")); + assert(requests == 1); + assert(profile_console_command("profile_record")); + assert(requests == 2 && accepted_seconds == 0.0); + assert(!profile_console_command("profile_record_extra 1")); + return 0; +} +''') + subprocess.run([compiler, "-std=gnu11", "-Wall", "-Werror", "-DHALO_PROFILE", + "-I", str(ROOT / "port/linux/include"), str(code), "-o", str(program)], check=True) + subprocess.run([str(program)], check=True) + + +# ---------- a normal build is unchanged + +PROFILE_PREFIXES = ("profile_trace_", "profile_net_", "profile_json_", "profile_overlay_") + + +def parse_compile_command(command, windows=False): + """Split a Ninja C command, normalizing Windows path separators without changing escaped quotes.""" + if windows: + command = re.sub(r'\\(?!")', "/", command) + return shlex.split(command, posix=True) + + +def unavailable(reason): + """a run that asks for the check (HALO_PROFILE_BASE or HALO_PROFILE_OBJECTS) must not skip it silently""" + if os.environ.get("HALO_PROFILE_BASE") or os.environ.get("HALO_PROFILE_OBJECTS"): + pytest.fail(reason) + pytest.skip(reason) + + +def preprocess_flags(): + """the game's or the platform layer's compile flags for this computer's + build, as ninja has them, without HALO_PROFILE and without what makes + an object; absolute where they name a generated file""" + missing = [name for name in ("clang", "ninja") if not shutil.which(name)] + if not (ROOT / "build.ninja").is_file(): + missing.append("a configured build (build.ninja)") + if missing: + unavailable("needs " + ", ".join(missing)) + # (HALO_PROFILE_OBJECTS names another configured flavour's object folder, such as build/android/guest/obj, + # where this computer has no build of its own) + objects = os.environ.get("HALO_PROFILE_OBJECTS") or f"build/{'windows' if os.name == 'nt' else 'linux'}/obj" + + def flags(object_path): + listing = subprocess.run(["ninja", "-t", "commands", object_path], cwd=ROOT, capture_output=True, text=True) + if listing.returncode != 0: + unavailable(f"needs a build.ninja that has {object_path}") + command = listing.stdout.strip().splitlines()[-1] + words = parse_compile_command(command, windows=os.name == "nt") + while words and not words[0].startswith("-"): + words = words[1:] + # (the Android guest's command is a pipeline whose first compile makes assembly) + words = words[:words.index("&&")] if "&&" in words else words + kept, skip = [], False + for word in words: + if skip: + skip = False + elif word in ("-MF", "-o", "-c"): + skip = True + elif word in ("-MMD", "-S") or word == "-DHALO_PROFILE" or word.startswith(("-flto", "-fprofile-use")): + continue + elif word.endswith(".c") and not word.startswith("-"): + continue + elif word.startswith(("build/", "build\\")): + kept.append(str(ROOT / word)) + elif word.startswith(("-Ibuild/", "-Ibuild\\")): + kept.append("-I" + str(ROOT / word[2:])) + else: + kept.append(word) + return kept + + flag_sets = { + "game": flags(f"{objects}/source/game/game.o"), + "platform": flags(f"{objects}/port/linux/src/p2p.o"), + "port_game": flags(f"{objects}/port/linux/game/network_coop.o"), + } + # (only a missing 32-bit C library is "no build here"; any other error is a wrong command, which must not hide) + probe = subprocess.run(["clang", *flag_sets["game"], "-E", "-P", "-x", "c", "-"], cwd=ROOT, + input="#include \n#include \n", + capture_output=True, text=True) + if probe.returncode != 0: + reason = "the game's flags do not preprocess: " + next((line for line in probe.stderr.splitlines() if "error" in line), "")[:200] + if "bits/libc-header-start.h" in probe.stderr: + pytest.skip("needs a 32-bit C library (gcc-multilib); " + reason) + pytest.fail(reason) + return flag_sets + + +def flags_for(path, flag_sets): + if path.startswith("port/linux/src/"): + return flag_sets["platform"] + if path.startswith("port/linux/game/"): + return flag_sets["port_game"] + return flag_sets["game"] + + +def preprocessed(tree, path, flags): + # (path None: an empty file, which is all that the forced includes leave of a file that is empty) + arguments = ["-x", "c", "-"] if path is None else [path] + # (__DATE__ and __TIME__ pinned: main.c prints them, and the two trees can be preprocessed a second apart) + pinned = ["-Wno-builtin-macro-redefined", '-D__DATE__="Jan 1 2000"', '-D__TIME__="00:00:00"'] + result = subprocess.run(["clang", *flags, *pinned, "-E", "-P", *arguments], cwd=tree, + input="" if path is None else None, + capture_output=True, text=True) + assert result.returncode == 0, f"{path}: {result.stderr[-2000:]}" + # (only the tokens: blank lines and layout differ where #ifdef blocks were) + return " ".join(result.stdout.split()) + + +def includers_of_changed_headers(base, already): + """the C files that include a header that changed since the base: a header's #ifdef can change them too""" + def names(*arguments): + return subprocess.run([GIT, *arguments], cwd=ROOT, capture_output=True, text=True, check=True).stdout.split() + + added = set(names("diff", "--name-only", "--diff-filter=A", base, "HEAD", "--", "*.c")) + found = set() + for header in names("diff", "--name-only", "--diff-filter=M", base, "HEAD", "--", "*.h"): + pattern = r'#[ ]*include[ ]*[<"]([^">]*/)?' + re.escape(header.rsplit("/", 1)[-1]) + '[">]' + for path in names("grep", "-l", "-E", pattern, "HEAD", "--", "*.c"): + path = path.split(":", 1)[1] + if path.startswith(("source/", "port/linux/src/", "port/linux/game/")) and path not in added: + found.add(path) + return sorted(found - set(already)) + + +@pytest.mark.skipif(not os.environ.get("HALO_PROFILE_BASE"), reason="HALO_PROFILE_BASE names the base commit") +def test_normal_build_preprocesses_as_before(tmp_path): + base = os.environ["HALO_PROFILE_BASE"] + flag_sets = preprocess_flags() + changed = subprocess.run([GIT, "diff", "--name-only", "--diff-filter=M", base, "HEAD", "--", "*.c"], cwd=ROOT, + capture_output=True, text=True, check=True).stdout.split() + assert changed, "no C file changed since the base" + changed += includers_of_changed_headers(base, changed) + tree = tmp_path / "base" + tree.mkdir() + archive = subprocess.run([GIT, "archive", "--format=tar", base], cwd=ROOT, check=True, capture_output=True).stdout + with tarfile.open(fileobj=io.BytesIO(archive)) as reference: + reference.extractall(tree) + for path in changed: + flags = flags_for(path, flag_sets) + assert preprocessed(tree, path, flags) == preprocessed(ROOT, path, flags), path + + +def test_new_files_are_empty_in_a_normal_build(): + flag_sets = preprocess_flags() + # (found, not listed: a new profile_*.c that forgets its #ifdef shows up here) + paths = sorted(str(path.relative_to(ROOT)).replace("\\", "/") + for directory in ("port/linux/src", "port/linux/game") + for path in (ROOT / directory).glob("profile_*.c")) + assert set(paths) == {*PROFILE_SOURCES, "port/linux/game/profile_console.c"}, paths + for path in paths: + flags = flags_for(path, flag_sets) + assert preprocessed(ROOT, path, flags) == preprocessed(ROOT, None, flags), path + + +def test_normal_build_has_no_profiling_symbols(): + ninja_file = ROOT / "build.ninja" + if not ninja_file.is_file(): + pytest.skip("needs a configured build (build.ninja)") + if "-DHALO_PROFILE" in ninja_file.read_text(encoding="utf-8"): + pytest.skip("a profiling build is configured") + nm = shutil.which("llvm-nm") or shutil.which("nm") + if os.name == "nt" or os.environ.get("HALO_PROFILE_OBJECTS"): + folder = ROOT / (os.environ.get("HALO_PROFILE_OBJECTS") or "build/windows/obj") + objects = sorted(str(path) for path in folder.rglob("*.o")) + else: + objects = [str(ROOT / "build/linux/halo")] if (ROOT / "build/linux/halo").is_file() else [] + if not nm: + unavailable("needs nm") + if not objects: + unavailable("needs a built normal build (build/windows/obj/**/*.o, build/linux/halo, or the objects in HALO_PROFILE_OBJECTS)") + found, words, defined = set(), set(), 0 + for start in range(0, len(objects), 200): + listing = subprocess.run([nm, "--defined-only", *objects[start:start + 200]], capture_output=True, text=True) + # (an nm that cannot read the objects, such as bitcode from --lto, lists nothing and would pass) + assert listing.returncode == 0, listing.stderr[-1000:] + words |= {word.lstrip("_") for word in listing.stdout.split()} + defined += sum(len(line.split()) == 3 for line in listing.stdout.splitlines()) + found = {word for word in words if word.startswith(PROFILE_PREFIXES)} + # (a count, not a named symbol: link-time optimization inlines the functions that have one caller. The game's + # objects list thousands; an unreadable or a near-empty set lists none) + assert defined >= 300, f"nm lists {defined} defined symbols: it cannot read these objects" + assert not found, sorted(found)[:20] diff --git a/tools/windows_build.py b/tools/windows_build.py index f410e935..55489cb1 100644 --- a/tools/windows_build.py +++ b/tools/windows_build.py @@ -19,7 +19,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional from .linux_build import (LINUX_PROFILE, MINIUPNPC_DIR, OPTIMISATION, WINDOWS_PROFILE, XDK_INCLUDE, lto_mode, - march_flag, miniupnpc_sources, pgo_mode, compile_launcher, game_defines_and_includes, + march_flag, miniupnpc_sources, pgo_mode, compile_launcher, configuration_defines, game_defines_and_includes, game_sources, musl_math_cflags, musl_math_sources, pgo_profile, profile_use_flags, xdk_headers) from .embed_assets import hud_assets_build, hud_configure_inputs @@ -304,8 +304,8 @@ def generate_windows_build(n: Writer, sln: Any) -> None: # (a debug build checks its stack frames (/GS), and stops at the first # one overrun, as at the first failed assertion; a release build does # not, so that an overrun nobody has met cannot end a game) - abi = " ".join(WINDOWS_ABI_FLAGS + [march_flag(sln)] + (["-DHALO_RELEASE"] if getattr(sln, "port_release", False) - else ["-fstack-protector-strong"])) + abi = " ".join(WINDOWS_ABI_FLAGS + [march_flag(sln)] + configuration_defines(sln) + + ([] if getattr(sln, "port_release", False) else ["-fstack-protector-strong"])) sdl_include = SDL_DIR / "include" libs = " ".join( [_quote(SDL_DIR / "lib" / "x86" / "SDL3.lib")] From a27c205c32652ec1462d2f5c19506880d1171afb Mon Sep 17 00:00:00 2001 From: TwistedFlog <138166350+TwistedFlog@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:02:39 +1300 Subject: [PATCH 093/187] Visibility test results are copied a frame later, all at once (#150) A copy of each lens flare's result right after its test made the GPU wait for that test, hundreds of times a frame in The Library (51-61 fps with drops to 20 on an RX 6700 XT). Each frame's results are now copied together at the next present, when they are long done, so the GPU never waits (160-213 fps). Two batches of query objects alternate so a query is not reused before its count is copied. Flares react a frame later. Co-Authored-By: Claude Opus 5.5 (1M context) --- port/linux/src/d3d8_gl.c | 88 +++++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 14954cb9..bf7d2bbe 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -549,11 +549,21 @@ struct gl_device GLuint visibility_latest[VISIBILITY_TEST_SLOTS]; #else /* each test's latest result, which the GPU writes (as a query buffer) - when the test's draws are done: the game waits for results at the start - of the next frame, and a query would stop the CPU there until the GPU - had caught up */ + a frame after the test (visibility_copy_batch): the game waits for + results at the start of the next frame, and a query would stop the CPU + there until the GPU had caught up */ GLuint visibility_results_buffer; volatile GLuint *visibility_results; + /* the tests of this frame and of the one before, whose counts the GPU + is yet to copy into their slots; each batch keeps its own query objects, + which are not reused until their counts are copied */ + struct + { + GLuint queries[VISIBILITY_TEST_SLOTS]; + unsigned short slots[VISIBILITY_TEST_SLOTS]; + unsigned long count; + } visibility_batches[2]; + unsigned long visibility_batch; /* a pipeline flush every flush_every draws (draw_flush), 0 never */ unsigned long flush_every; unsigned long flush_draws; @@ -1542,7 +1552,12 @@ static void gl_initialize(void) GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); device.visibility_results = glMapBufferRange(GL_QUERY_BUFFER, 0, VISIBILITY_TEST_SLOTS * sizeof(GLuint), GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - if (!device.visibility_results) + if (device.visibility_results) + { + glGenQueries(VISIBILITY_TEST_SLOTS, device.visibility_batches[0].queries); + glGenQueries(VISIBILITY_TEST_SLOTS, device.visibility_batches[1].queries); + } + else platform_log("cannot map the visibility test results; tests wait for the GPU"); { long every = config_integer("debug.gpu_flush_draws"); @@ -2071,6 +2086,18 @@ void WINAPI D3DDevice_BeginVisibilityTest(void) glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0); return; } +#else + if (device.visibility_results) + { + unsigned long count = device.visibility_batches[device.visibility_batch].count; + + /* the batch's next query, or (with the batch full) a scratch one + whose count is dropped */ + device.active_query = count < VISIBILITY_TEST_SLOTS ? + device.visibility_batches[device.visibility_batch].queries[count] : device.queries[0]; + glBeginQuery(VISIBILITY_QUERY, device.active_query); + return; + } #endif glBeginQuery(VISIBILITY_QUERY, device.queries[0]); } @@ -2109,22 +2136,27 @@ HRESULT WINAPI D3DDevice_EndVisibilityTest(DWORD index) (lens flares, rasterizer_lights.c), a split-screen window's or the screen's alike */ device.query_area[index] = target_scale[0] * target_scale[1] * (float)target_samples; +#ifndef HALO_ANDROID + if (device.visibility_results) + { + /* the GPU copies the count into the slot at the next frame's end + (visibility_copy_batch) */ + unsigned long *count = &device.visibility_batches[device.visibility_batch].count; + + if (*count < VISIBILITY_TEST_SLOTS) + { + device.visibility_batches[device.visibility_batch].slots[(*count)++] = (unsigned short)index; + device.query_pending[index] = TRUE; + } + return S_OK; + } +#endif /* swap the scratch query into the requested slot */ scratch = device.queries[0]; device.queries[0] = device.queries[index]; device.queries[index] = scratch; device.query_pending[index] = TRUE; device.visibility_unread[index] = TRUE; -#ifndef HALO_ANDROID - if (device.visibility_results) - { - /* the GPU writes the count into the slot once it is known (given - by name: Mesa's GL thread waits for everything before a - glGetQueryObjectuiv, even one into a bound buffer) */ - glGetQueryBufferObjectuiv(device.queries[index], device.visibility_results_buffer, GL_QUERY_RESULT, - (GLintptr)(index * sizeof(GLuint))); - } -#endif return S_OK; } @@ -4718,6 +4750,31 @@ static void write_screenshot(struct render_target_entry *target) free(pixels); } +#ifndef HALO_ANDROID +/* the GPU copies the counts of the previous frame's tests into their slots +(given by name: Mesa's GL thread waits for everything before a +glGetQueryObjectuiv, even one into a bound buffer). A copy right after each +test stopped the GPU until that test's draws were through, hundreds of times +a frame in The Library; a frame later the draws are long done, so the copies +do not wait. */ +static void visibility_copy_batch(void) +{ + unsigned long i; + + if (!device.visibility_results) + return; + /* this frame's tests wait for the next present */ + device.visibility_batch ^= 1; + for (i = 0; i < device.visibility_batches[device.visibility_batch].count; i++) + { + glGetQueryBufferObjectuiv(device.visibility_batches[device.visibility_batch].queries[i], + device.visibility_results_buffer, GL_QUERY_RESULT, + (GLintptr)(device.visibility_batches[device.visibility_batch].slots[i] * sizeof(GLuint))); + } + device.visibility_batches[device.visibility_batch].count = 0; +} + +#endif void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destination_rectangle, void *unused, void *unused2) { @@ -4739,6 +4796,9 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat platform_log("present back buffer %08lx texture %u", (unsigned long)device.back_buffer.Data, back_buffer->target.texture); render_target_resolve(&back_buffer->target); +#ifndef HALO_ANDROID + visibility_copy_batch(); +#endif if (screenshot_every > 0 && device.frame % (unsigned long)screenshot_every == 0) write_screenshot(back_buffer); From 7d821587bf706ec8ec75f69bb1583a8bb05d2f5b Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:09:43 +1300 Subject: [PATCH 094/187] Visibility tests' own scratch query, help text comment Slot 0 was the tests' scratch query and taken for slot 1, so the game's lens flare 0 read flare 1's visibility; a query of its own is the scratch (#150's full batch too), and every slot is a test's. The query buffer is unbound after it is mapped, so a fallback read is not taken for an offset into it. #171's comment as port: comments are. --- port/linux/src/d3d8_gl.c | 21 +++++++++++++-------- source/interface/hud_messaging.c | 6 +++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 9f866498..ae5f57f9 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -520,6 +520,11 @@ struct gl_device (render_target_get), which its count is divided by */ float query_area[VISIBILITY_TEST_SLOTS]; GLuint active_query; + /* the query a test runs on until it ends (swapped into its slot, or with + the results mapped and the batch full, dropped): a slot of its own, so + that every slot, 0 too, is a test's (the game's lens flares number + theirs from 0) */ + GLuint scratch_query; BOOL visibility_test_active; /* (queries read on the CPU) each slot's latest count known, and whether its query has yet to be read: the game spins on a result it is told is @@ -1545,6 +1550,7 @@ static void gl_initialize(void) #endif glGenSamplers(D3DTSS_MAXSTAGES, device.samplers); glGenQueries(VISIBILITY_TEST_SLOTS, device.queries); + glGenQueries(1, &device.scratch_query); #ifndef HALO_ANDROID glGenBuffers(1, &device.visibility_results_buffer); glBindBuffer(GL_QUERY_BUFFER, device.visibility_results_buffer); @@ -1559,6 +1565,9 @@ static void gl_initialize(void) } else platform_log("cannot map the visibility test results; tests wait for the GPU"); + /* (unbound: a query read into a bound query buffer takes its pointer + for an offset into it) */ + glBindBuffer(GL_QUERY_BUFFER, 0); { long every = config_integer("debug.gpu_flush_draws"); const char *renderer = (const char *)glGetString(GL_RENDERER); @@ -2094,12 +2103,12 @@ void WINAPI D3DDevice_BeginVisibilityTest(void) /* the batch's next query, or (with the batch full) a scratch one whose count is dropped */ device.active_query = count < VISIBILITY_TEST_SLOTS ? - device.visibility_batches[device.visibility_batch].queries[count] : device.queries[0]; + device.visibility_batches[device.visibility_batch].queries[count] : device.scratch_query; glBeginQuery(VISIBILITY_QUERY, device.active_query); return; } #endif - glBeginQuery(VISIBILITY_QUERY, device.queries[0]); + glBeginQuery(VISIBILITY_QUERY, device.scratch_query); } HRESULT WINAPI D3DDevice_EndVisibilityTest(DWORD index) @@ -2110,8 +2119,6 @@ HRESULT WINAPI D3DDevice_EndVisibilityTest(DWORD index) return S_OK; device.visibility_test_active = FALSE; index %= VISIBILITY_TEST_SLOTS; - if (!index) - index = 1; #ifdef HALO_ANDROID if (xgpu_capabilities.atomic_counters) { @@ -2152,8 +2159,8 @@ HRESULT WINAPI D3DDevice_EndVisibilityTest(DWORD index) } #endif /* swap the scratch query into the requested slot */ - scratch = device.queries[0]; - device.queries[0] = device.queries[index]; + scratch = device.scratch_query; + device.scratch_query = device.queries[index]; device.queries[index] = scratch; device.query_pending[index] = TRUE; device.visibility_unread[index] = TRUE; @@ -2177,8 +2184,6 @@ HRESULT WINAPI D3DDevice_GetVisibilityTestResult(DWORD index, UINT *result, ULON if (time_stamp) *time_stamp = 0; index %= VISIBILITY_TEST_SLOTS; - if (!index) - index = 1; if (!device.gl_ready || !device.query_pending[index]) { if (result) diff --git a/source/interface/hud_messaging.c b/source/interface/hud_messaging.c index 638eb68d..b9bfcc25 100644 --- a/source/interface/hud_messaging.c +++ b/source/interface/hud_messaging.c @@ -1685,9 +1685,9 @@ void hud_messaging_update( } else { - /* Help text has no custom icons. Leave this one out. - The button icon list is the wrong list, and its index - is still NONE. */ + /* port: help text has no custom icons: this one + is left out (the button icons' list is not + theirs, and its index is still NONE) */ static boolean help_custom_icon_reported = FALSE; if (!help_custom_icon_reported) From eb1b48992c1392c076dbf266272edcf4e7ddfe2f Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:02:55 +1300 Subject: [PATCH 095/187] Android quit only, comments as port's (#183) The side-by-side debug build's Gradle and manifest changes are left out: UpdateProvider's authority stays hard-coded, so its updater would not match them. --- port/android/app/build.gradle | 9 +-------- port/android/app/src/main/AndroidManifest.xml | 2 +- port/linux/src/sdl_platform.c | 6 +++--- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/port/android/app/build.gradle b/port/android/app/build.gradle index da36bd00..97b47577 100644 --- a/port/android/app/build.gradle +++ b/port/android/app/build.gradle @@ -16,15 +16,13 @@ def buildNumber = buildNumberText.isInteger() ? buildNumberText.toInteger() : 0 // main signed with it can install over the one before; without it, the // debug key of the computer that builds def signingKey = System.getenv("HALO_ANDROID_KEYSTORE") -def debugSideBySide = project.hasProperty("haloDebugSideBySide") android { namespace "com.halo.decomp" compileSdk 35 defaultConfig { - applicationId debugSideBySide ? "com.halo.decomp.debug" : "com.halo.decomp" - manifestPlaceholders = [updateAuthority: "com.halo.decomp.update"] + applicationId "com.halo.decomp" minSdk 28 targetSdk 35 versionCode Math.max(1, buildNumber) @@ -56,7 +54,6 @@ android { buildFeatures { buildConfig true - resValues true } signingConfigs { @@ -74,10 +71,6 @@ android { debug { debuggable true minifyEnabled false - if (debugSideBySide) { - resValue "string", "app_name", "Halo Debug" - manifestPlaceholders = [updateAuthority: "com.halo.decomp.debug.update"] - } signingConfig signingKey ? signingConfigs.halo : signingConfigs.debug } release { diff --git a/port/android/app/src/main/AndroidManifest.xml b/port/android/app/src/main/AndroidManifest.xml index a7889722..cccdd89c 100644 --- a/port/android/app/src/main/AndroidManifest.xml +++ b/port/android/app/src/main/AndroidManifest.xml @@ -79,7 +79,7 @@ diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 6ad1a3e4..6e3a595f 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -1218,12 +1218,12 @@ static void platform_show_pending_message(void) /* ---------- events */ -/* quits as closing the window does (the menus' Quit: -port/linux/game/menu_functions.c) */ +/* quits as closing the window does, when the events are next read (the +menus' Quit: port/linux/game/menu_functions.c); on Android at once */ void platform_request_quit(void) { #ifdef HALO_ANDROID - /* The Android guest does not import SDL_PushEvent; exit reaches host_exit. */ + /* (the guest has no SDL_PushEvent: exit ends the process, host_exit) */ exit(EXIT_SUCCESS); #else SDL_Event event; From 515e6a0ab6e7fe5fe57f67f758c7e6e1e14bf4fe Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:03:57 +1300 Subject: [PATCH 096/187] Profiles' Start as CE's again, comment as port's (#222) main_menu.xml is generated from CE's tags; with the keyboard's typing mode ended as it closes, Enter is A again and CE's own Start handler can stay. Its test is dropped with it. --- port/assets/menus/ce/main_menu.xml | 2 +- source/interface/virtual_keyboard.c | 5 +++-- tools/harness/tests/test_profile_keyboard.py | 16 ---------------- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/port/assets/menus/ce/main_menu.xml b/port/assets/menus/ce/main_menu.xml index 7bed3d12..63953084 100644 --- a/port/assets/menus/ce/main_menu.xml +++ b/port/assets/menus/ce/main_menu.xml @@ -32,7 +32,7 @@ - + diff --git a/source/interface/virtual_keyboard.c b/source/interface/virtual_keyboard.c index aa2bafcc..3e816535 100644 --- a/source/interface/virtual_keyboard.c +++ b/source/interface/virtual_keyboard.c @@ -347,8 +347,9 @@ static rectangle2d keyboard_rect[NUMBER_OF_VIRTUAL_KEYS] = static struct virtual_keyboard_globals virtual_keyboard_globals= {0}; -/* Keep the port's keyboard input mode in step with the keyboard's lifetime. -Processing stops once it closes, so the next process call cannot clear it. */ +/* port: the computer's keyboard types into the keyboard while it is up +(port/linux/src/xinput_sdl.c), set as it opens and closes: it is processed +only while it is up, so its processing cannot end the typing */ static void virtual_keyboard_set_active(boolean active) { extern void platform_text_typing(int typing); diff --git a/tools/harness/tests/test_profile_keyboard.py b/tools/harness/tests/test_profile_keyboard.py index 25b41ec9..ec3e4d37 100644 --- a/tools/harness/tests/test_profile_keyboard.py +++ b/tools/harness/tests/test_profile_keyboard.py @@ -2,7 +2,6 @@ import re import sys from pathlib import Path -import xml.etree.ElementTree as ET import pytest @@ -57,18 +56,3 @@ def test_original_sticky_typing_detected(case): def test_held_enter_guard_detected(): status, output = run(build('profile_keyboard', generated('held-enter')), 'held-enter') assert status == CHECK_FAILED, output - - -def test_profiles_accept_destinations(): - root = ET.fromstring(read('port/assets/menus/ce/main_menu.xml')) - widgets = {widget.get('name'): widget for widget in root.findall('widget')} - profile = widgets['main_menu/main_menu_item_profile'] - for event in ['a', 'start']: - handler = profile.find(f"on[@event='{event}']") - assert handler.get('open') == 'main_menu/profile_manager/player_profile_manager' - assert handler.get('run') == 'new game if no plyr profiles' - assert profile.find('conditional').get('widget') == 'main_menu/profile_manager/pm_entry_creating_profile' - multiplayer = widgets['main_menu/main_menu_item_multiplayer'] - for event in ['a', 'start']: - assert multiplayer.find(f"on[@event='{event}']").get('open') == \ - 'main_menu/multiplayer_type_select/multiplayer_type_select_screen' From 66d34b9e65900310f1afb2c0ae88a165442f0091 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:04:26 +1300 Subject: [PATCH 097/187] Keyboard crouch walk at 0.97, without its test (#190) 0.97 is safely below player_control.c's 0.98 cutoff, diagonals too, with no one-float-step margin to guard; the test that extracted the gate by its braces, and the harness flags only it used, are dropped. --- source/input/input_abstraction.c | 10 +- tools/harness/README.md | 2 - tools/harness/__init__.py | 6 +- tools/harness/tests/keyboard_crouch.c | 186 -------------------- tools/harness/tests/test_keyboard_crouch.py | 71 -------- 5 files changed, 6 insertions(+), 269 deletions(-) delete mode 100644 tools/harness/tests/keyboard_crouch.c delete mode 100644 tools/harness/tests/test_keyboard_crouch.py diff --git a/source/input/input_abstraction.c b/source/input/input_abstraction.c index d9b372ca..3d418072 100644 --- a/source/input/input_abstraction.c +++ b/source/input/input_abstraction.c @@ -265,14 +265,10 @@ static void keyboard_controls_update( /* (diagonals on the unit circle) */ real length = x && y ? 0.70710678f : 1.f; - /* port: full stick uncrouches a grounded biped (player_control.c). - Keep keyboard crouch below its strict 0.98f magnitude cutoff: this - is the largest float below 0.98f, applied after diagonal normalization. - Only keyboard movement with the keyboard's crouch action is scaled. */ + /* port: crouched, below the full stick's 0.98 that stands a grounded + biped up (player_control.c), as a stick's crouch walk is */ if (TEST_FLAG(held, HALO_KEYBOARD_CROUCH)) - { - length *= 0.97999996f; - } + length *= 0.97f; state->forward_movement = y * length; state->strafe = -x * length; diff --git a/tools/harness/README.md b/tools/harness/README.md index 7cdbc769..407a203e 100644 --- a/tools/harness/README.md +++ b/tools/harness/README.md @@ -13,8 +13,6 @@ finds every `tests/test_*.py`, so a new test needs no change to CI. - `tests/.c`: the fake world, only what the code reads, with what it calls stubbed as macros that name only the arguments they use; then `#include "under_test.inc"` and a `main` that runs the case named on its command line, checked with `CHECK`. -- `build` accepts optional compiler `flags` when a test needs the port's arithmetic settings (for example, - `-march=x86-64 -ffp-contract=off` for a strict single-precision boundary). - `tests/test_.py`: takes the code, enums and constants from the sources (`function`, `inline`, `enum_with`, `constant`) rather than copying them, so the test checks what the game builds; `build` compiles it (warnings are errors) and each case is a pytest test. diff --git a/tools/harness/__init__.py b/tools/harness/__init__.py index 99633979..e351e53f 100644 --- a/tools/harness/__init__.py +++ b/tools/harness/__init__.py @@ -80,14 +80,14 @@ CHECK_FAILED = 1 # a case's exit status when a CHECK fails (harness.h) @functools.lru_cache(maxsize=None) -def build(test, generated, flags=()): +def build(test, generated): """tests/.c compiled with the generated includes ((name, text), ...) for the game's 32-bit ABI.""" - work = Path(_directory.name) / f"{test}-{abs(hash((generated, flags))):x}" + work = Path(_directory.name) / f"{test}-{abs(hash(generated)):x}" work.mkdir(parents=True, exist_ok=True) for name, text in generated: (work / name).write_text(text) executable = work / test - command = [os.environ.get("CC", "clang"), "-m32", "-std=gnu99", "-O2", *flags, "-Wall", "-Werror", "-Wno-unused-function", + command = [os.environ.get("CC", "clang"), "-m32", "-std=gnu99", "-O2", "-Wall", "-Werror", "-Wno-unused-function", "-Wno-unused-variable", "-I", str(HARNESS / "include"), "-I", str(work), str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable), "-lm"] result = subprocess.run(command, capture_output=True, text=True) diff --git a/tools/harness/tests/keyboard_crouch.c b/tools/harness/tests/keyboard_crouch.c deleted file mode 100644 index 2c519984..00000000 --- a/tools/harness/tests/keyboard_crouch.c +++ /dev/null @@ -1,186 +0,0 @@ -/* Actual keyboard input over fake held actions, then the unchanged player-control crouch gate. */ -#include "harness.h" -#include -#include "config.inc" - -typedef struct { real i, j; } real_vector2d; -struct biped_datum { struct { unsigned long flags; } biped; }; -struct test_input { real_vector2d throttle; unsigned long unit_control_flags; }; - -#define MIN(a,b) ((a)>(b)?(b):(a)) -#define MAX(a,b) ((a)>(b)?(a):(b)) -#define PIN(n,floor,ceiling) MAX((floor), MIN((n), (ceiling))) -#define SET_FLAG(f,b,v) ((v) ? ((f)|=(unsigned)FLAG(b)) : ((f)&=(unsigned)~FLAG(b))) - -static unsigned long held_actions[MAXIMUM_GAMEPADS]; -static long now; -static boolean controls_enable_crouch; -static unsigned long halo_keyboard_actions(short controller_index) { return held_actions[controller_index]; } -static long system_milliseconds(void) { return now; } -#include "under_test.inc" - -static struct game_input_state keyboard(unsigned long held) -{ - struct game_input_state state = {0}; - held_actions[0] = held; - keyboard_controls_update(0, &state); - return state; -} - -static boolean crouches(struct game_input_state state, unsigned long biped_flags, boolean inhibited) -{ - struct biped_datum biped = {{biped_flags}}; - struct test_input input = {{state.forward_movement, state.strafe}, 0}; - if (inhibited) state.buttons[_game_control_crouch] = 0; - apply_crouch(&biped, &input, state.buttons); - return TEST_FLAG(input.unit_control_flags, _unit_control_crouch_modifier_bit); -} - -static real next_positive_float(real value) -{ - uint32_t bits; - memcpy(&bits, &value, sizeof(bits)); - bits++; - memcpy(&value, &bits, sizeof(value)); - return value; -} - -static unsigned long direction(long x, long y) -{ - return (x < 0 ? FLAG(HALO_KEYBOARD_STRAFE_LEFT) : x > 0 ? FLAG(HALO_KEYBOARD_STRAFE_RIGHT) : 0) | - (y < 0 ? FLAG(HALO_KEYBOARD_MOVE_BACKWARD) : y > 0 ? FLAG(HALO_KEYBOARD_MOVE_FORWARD) : 0); -} - -int main(int argc, char **argv) -{ - const char *case_name = argc > 1 ? argv[1] : ""; - unsigned long crouch = FLAG(HALO_KEYBOARD_CROUCH); - unsigned long forward = FLAG(HALO_KEYBOARD_MOVE_FORWARD); - long x, y; - - CASE("cardinals") - { - for (x = -1; x <= 1; x++) for (y = -1; y <= 1; y++) if ((x != 0) != (y != 0)) - { - struct game_input_state state = keyboard(crouch | direction(x, y)); - CHECK(crouches(state, 0, FALSE), "grounded crouch cancelled for (%ld,%ld)", x, y); - CHECK(state.forward_movement * y + state.strafe * -x > 0.f, "direction reversed"); - } - } - else CASE("diagonals") - { - for (x = -1; x <= 1; x += 2) for (y = -1; y <= 1; y += 2) - { - struct game_input_state state = keyboard(crouch | direction(x, y)); - CHECK(crouches(state, 0, FALSE), "diagonal crouch cancelled for (%ld,%ld)", x, y); - CHECK(state.forward_movement * y == state.strafe * -x && state.forward_movement * y > 0.f, - "diagonal direction changed"); - } - } - else CASE("maximum-safe-input") - { - struct game_input_state state = keyboard(crouch | forward); - CHECK(crouches(state, 0, FALSE), "cardinal cap fails gate"); - state.forward_movement = next_positive_float(state.forward_movement); - CHECK(!crouches(state, 0, FALSE), "cap is below the largest safe cardinal float"); - state = keyboard(crouch | forward | FLAG(HALO_KEYBOARD_STRAFE_LEFT)); - CHECK(crouches(state, 0, FALSE), "diagonal cap fails gate"); - state.forward_movement = next_positive_float(state.forward_movement); - state.strafe = next_positive_float(state.strafe); - CHECK(!crouches(state, 0, FALSE), "diagonal components are below the largest safe floats"); - } - else CASE("run-and-release") - { - for (x = -1; x <= 1; x++) for (y = -1; y <= 1; y++) if (x || y) - { - struct game_input_state running = keyboard(direction(x, y)); - struct game_input_state walking = keyboard(crouch | direction(x, y)); - struct game_input_state released = keyboard(direction(x, y)); - real component = x && y ? 0.70710678f : 1.f; - CHECK(running.forward_movement == y * component && running.strafe == -x * component, - "normal running changed"); - CHECK(crouches(walking, 0, FALSE) && !crouches(released, 0, FALSE), "release keeps crouch"); - CHECK(released.forward_movement == running.forward_movement && released.strafe == running.strafe, - "release does not restore full movement"); - } - } - else CASE("controller-and-mixed-input") - { - long controller; - for (controller = 0; controller < MAXIMUM_GAMEPADS; controller++) - { - struct game_input_state state = {0}; - state.forward_movement = 1.f; - state.buttons[_game_control_crouch] = 7; - held_actions[controller] = 0; - keyboard_controls_update(controller, &state); - CHECK(state.forward_movement == 1.f && !crouches(state, 0, FALSE), "controller full stick changed"); - state.forward_movement = 0.5f; - keyboard_controls_update(controller, &state); - CHECK(state.forward_movement == 0.5f && crouches(state, 0, FALSE), "controller partial stick changed"); - } - { - struct game_input_state state = {0}; - state.forward_movement = 1.f; - state.yaw = 0.4f; - state.pitch = -0.2f; - held_actions[0] = crouch; - keyboard_controls_update(0, &state); - CHECK(state.forward_movement == 1.f && !crouches(state, 0, FALSE), "keyboard crouch scales controller movement"); - state.buttons[_game_control_crouch] = 7; - held_actions[0] = forward; - keyboard_controls_update(0, &state); - CHECK(state.forward_movement == 1.f && !crouches(state, 0, FALSE), "controller crouch scales keyboard movement"); - held_actions[0] = forward | crouch; - keyboard_controls_update(0, &state); - CHECK(crouches(state, 0, FALSE), "mixed input overrides keyboard cap"); - CHECK(state.yaw == 0.4f && state.pitch == -0.2f && state.buttons[_game_control_crouch] == 7, - "look or controller button precedence changed"); - } - } - else CASE("cancelled-keys") - { - struct game_input_state state = {0}; - state.forward_movement = 0.4f; - state.strafe = -0.3f; - held_actions[0] = crouch | forward | FLAG(HALO_KEYBOARD_MOVE_BACKWARD) | - FLAG(HALO_KEYBOARD_STRAFE_LEFT) | FLAG(HALO_KEYBOARD_STRAFE_RIGHT); - keyboard_controls_update(0, &state); - CHECK(state.forward_movement == 0.4f && state.strafe == -0.3f, "opposing keys change analog fallback"); - CHECK(crouches(state, 0, FALSE), "stationary keyboard crouch fails"); - } - else CASE("hold-across-frame-rates") - { - long rates[] = {15, 30, 60, 144, 240, 1000}; - long rate, frame; - for (rate = 0; rate < (long)(sizeof(rates) / sizeof(*rates)); rate++) - { - memset(keyboard_controls, 0, sizeof(keyboard_controls)); - for (frame = 0; frame < rates[rate] * 5; frame++) - { - struct game_input_state state; - now = frame * 1000 / rates[rate]; - state = keyboard(crouch | forward | FLAG(HALO_KEYBOARD_RELOAD)); - CHECK(crouches(state, 0, FALSE), "held crouch fails at %ld FPS, frame %ld", rates[rate], frame); - CHECK(keyboard_controls[0].reload_ticks != 0, "reload lost"); - } - CHECK(keyboard(0).buttons[_game_control_crouch] == 0 && keyboard_controls[0].reload_ticks == 0, - "release retains held buttons"); - } - } - else CASE("crouch-gate-exceptions") - { - struct game_input_state state = keyboard(forward); - struct test_input input = {{0.f, 0.f}, 0}; - state.buttons[_game_control_crouch] = 1; - CHECK(crouches(state, FLAG(_biped_airborne_bit), FALSE), "airborne crouch changed"); - controls_enable_crouch = TRUE; - CHECK(crouches(state, 0, FALSE), "script-enabled full-speed crouch changed"); - state = keyboard(crouch | forward); - CHECK(!crouches(state, 0, TRUE), "inhibited crouch bypassed"); - apply_crouch(NULL, &input, state.buttons); - CHECK(input.unit_control_flags == 0, "non-biped gets crouch"); - } - else { fprintf(stderr, "unknown case: %s\n", case_name); return 2; } - return 0; -} diff --git a/tools/harness/tests/test_keyboard_crouch.py b/tools/harness/tests/test_keyboard_crouch.py deleted file mode 100644 index 15cab7ff..00000000 --- a/tools/harness/tests/test_keyboard_crouch.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Real keyboard movement and player-control crouch gate, including the strict float boundary.""" - -import sys -from pathlib import Path - -import pytest - -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) -from harness import CHECK_FAILED, build, constant, enum_with, function, inline, mutated, read, run # noqa: E402 - -CASES = ["cardinals", "diagonals", "maximum-safe-input", "run-and-release", - "controller-and-mixed-input", "cancelled-keys", "hold-across-frame-rates", "crouch-gate-exceptions"] - -# Match portable desktop builds: SSE float arithmetic, with no fused multiply-add. -# Bare -m32 uses x87 excess precision and changes the strict boundary comparison. -FLAGS = ("-march=x86-64", "-ffp-contract=off") - -NEGATIVE_CONTROLS = { - "original-full-speed": (("length *= 0.97999996f;", "length *= 1.f;"), "cardinals"), - "at-the-strict-cutoff": (("length *= 0.97999996f;", "length *= 0.98f;"), "cardinals"), - "unnormalized-diagonals": (("x && y ? 0.70710678f : 1.f", "1.f"), "diagonals"), - "slows-running-too": (("TEST_FLAG(held, HALO_KEYBOARD_CROUCH)", "TRUE"), "run-and-release"), - "uses-controller-crouch": (("TEST_FLAG(held, HALO_KEYBOARD_CROUCH)", - "state->buttons[_game_control_crouch]"), "controller-and-mixed-input"), -} - - -def generated(fault=None): - abstraction = read("source/input/input_abstraction.c") - player = read("source/game/player_control.c") - enums = "\n".join(enum_with(source, member) for source, member in ( - (abstraction, "_game_control_jump"), (player, "_button_jump"), - (read("port/linux/include/halo_keyboard.h"), "HALO_KEYBOARD_MOVE_FORWARD"), - (read("source/units/bipeds.h"), "_biped_airborne_bit"), - (read("source/units/units.h"), "_unit_control_crouch_modifier_bit"))) - for source, name in (("source/input/input.h", "MAXIMUM_GAMEPADS"), - ("source/cseries/cseries.h", "TICKS_PER_SECOND"), - ("source/cseries/cseries.h", "UNSIGNED_CHAR_MAX")): - enums += f"\n#define {name} {constant(read(source), name)}\n" - start = abstraction.index("struct game_input_state\n{") - enums += abstraction[start:abstraction.index("};", start) + 2] + "\n" - start = abstraction.index("static signed char const keyboard_game_controls") - text = abstraction[start:abstraction.index("static void keyboard_hold_ticks", start)] - text += function(abstraction, "keyboard_hold_ticks") + "\n" - update = function(abstraction, "keyboard_controls_update") - text += (mutated(update, *fault) if fault else update) + "\n" - text += inline(read("source/math/real_math.h"), "magnitude_squared2d") + "\n" - - # Compile the actual downstream condition and flag write, rather than a copied threshold. - start = player.index("if (biped &&") - end = player.index("{", start) + 1 - depth = 1 - while depth: - depth += (player[end] == "{") - (player[end] == "}") - end += 1 - gate = "static void apply_crouch(struct biped_datum *biped, struct test_input *input, " \ - "byte const *effective_buttons)\n{\n" + player[start:end] + "\n}\n" - return (("config.inc", enums), ("under_test.inc", text + gate)) - - -@pytest.mark.parametrize("case", CASES) -def test_case(case): - status, output = run(build("keyboard_crouch", generated(), FLAGS), case) - assert status == 0, output - - -@pytest.mark.parametrize("control", NEGATIVE_CONTROLS) -def test_negative_control(control): - fault, case = NEGATIVE_CONTROLS[control] - status, output = run(build("keyboard_crouch", generated(fault), FLAGS), case) - assert status == CHECK_FAILED, f"keyboard input with fault {control} passed {case}: the test cannot see it" From db9be6a002b360f3e6904b629405301dc432348a Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:05:07 +1300 Subject: [PATCH 098/187] Maps named com or lpt are not devices (#206) Only con, prn, aux and nul are devices by their three letters; com and lpt are with a digit after them. --- source/networking/network_client_manager.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/networking/network_client_manager.c b/source/networking/network_client_manager.c index 08f07424..2542e435 100644 --- a/source/networking/network_client_manager.c +++ b/source/networking/network_client_manager.c @@ -2372,7 +2372,7 @@ static boolean network_game_client_map_name_is_valid( for (device = 0; device < (short)NUMBEROF(devices); device++) { if (!_strnicmp(leaf, devices[device], 3) && - (stem == 3 || (device >= 4 && stem == 4 && leaf[3] >= '0' && leaf[3] <= '9'))) + ((device < 4 && stem == 3) || (device >= 4 && stem == 4 && leaf[3] >= '0' && leaf[3] <= '9'))) { return FALSE; } From 65cd4c29d81f9839f4fac89dceaf1450f00d30dd Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:06:25 +1300 Subject: [PATCH 099/187] usnprintf as MSVC's again, map formats' widths bounded (#197-#199) usnprintf terminated every string it wrote, so an exact fit lost its last character on Windows (a score limit of 100 shown as 10); the strings the PRs bound are terminated where they are written, as the rest of the game's are. The title copies are ustrncpy again (title_string[79] is terminated after). A map's format takes flags, then at most three digits of width and three of precision, so %5-d, %1.2.3d and huge widths are not passed to Windows's printf. --- source/game/game_engine.c | 14 +++++++++----- source/interface/hud_messaging.c | 3 +++ source/text/unicode.c | 19 ++++++++++++++----- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 86192fe7..04bd4cd6 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -1197,7 +1197,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy_terminated(title_string, outcome_string, 80); + ustrncpy(title_string, outcome_string, 80); break; case FALSE: @@ -1213,7 +1213,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy_terminated(title_string, outcome_string, 80); + ustrncpy(title_string, outcome_string, 80); } else { @@ -1227,7 +1227,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy_terminated(title_string, outcome_string, 80); + ustrncpy(title_string, outcome_string, 80); } break; @@ -1244,7 +1244,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy_terminated(title_string, outcome_string, 80); + ustrncpy(title_string, outcome_string, 80); } else { @@ -1258,7 +1258,7 @@ static void game_engine_generate_title_string( } else outcome_string = L""; - ustrncpy_terminated(title_string, outcome_string, 80); + ustrncpy(title_string, outcome_string, 80); } break; } @@ -2273,6 +2273,7 @@ static void game_engine_rasterize_scoreboard( /* port: bounded (the map's column names) */ usnprintf(row_string, NUMBEROF(row_string), L"\t%s\t%s\t%s\t%s", column_name, score_name, score_string, network ? L"Ping" : L""); + row_string[NUMBEROF(row_string) - 1] = 0; { long column; @@ -2347,6 +2348,7 @@ static void game_engine_rasterize_scoreboard( player->name, status_string, ping_string); + row_string[NUMBEROF(row_string) - 1] = 0; row_color = has_teams ? &team_colors[PIN(player->team_index, 0, 1)] : &color; /* port: a player talking (or muted) in voice chat, its speaker just right of the name */ @@ -2563,6 +2565,7 @@ static void game_engine_rasterize_in_game_score( game_engine->format_score_name(score_string); usnprintf(row_string, NUMBEROF(row_string), L"\t%s\t%s\t%s", column_name, score_name, score_string); + row_string[NUMBEROF(row_string) - 1] = 0; rasterize_in_game_score_draw_line(row_string, FALSE, &color, 1); for (entry_index = 0; entry_index < entry_count; entry_index++) @@ -2626,6 +2629,7 @@ static void game_engine_rasterize_in_game_score( place_string, player->name, status_string); + row_string[NUMBEROF(row_string) - 1] = 0; if (has_teams) row_color = &team_colors[PIN(player->team_index, 0, 1)]; diff --git a/source/interface/hud_messaging.c b/source/interface/hud_messaging.c index c5677c79..29248cc4 100644 --- a/source/interface/hud_messaging.c +++ b/source/interface/hud_messaging.c @@ -1888,7 +1888,10 @@ void hud_messaging_update( /* port: the item's text (the map's) is the format: as one only if it takes the count alone, and bounded */ if (item_text && ustring_format_takes(item_text, "d")) + { usnprintf(formatted, NUMBEROF(formatted), item_text, message->quantity / value_scale); + formatted[NUMBEROF(formatted) - 1] = 0; + } else ustrncpy_terminated(formatted, item_text, NUMBEROF(formatted)); rasterizer_draw_unicode_string( diff --git a/source/text/unicode.c b/source/text/unicode.c index 1d6bdcbf..21dbf388 100644 --- a/source/text/unicode.c +++ b/source/text/unicode.c @@ -1338,9 +1338,6 @@ int usnprintf( va_start(arglist, format); result = _vsnwprintf(string, size, format, arglist); va_end(arglist); - /* port: terminated when it is cut short, which MSVC's is not */ - if (size > 0) - string[size - 1] = 0; return result; } @@ -1360,6 +1357,7 @@ int ustring_format_takes( while (*format) { char kind; + short digits; if (*format++ != L'%') continue; @@ -1368,10 +1366,21 @@ int ustring_format_takes( format++; continue; } - while (*format == L'-' || *format == L'+' || *format == L' ' || *format == L'#' || *format == L'.' || - (*format >= L'0' && *format <= L'9')) + while (*format == L'-' || *format == L'+' || *format == L' ' || *format == L'#' || *format == L'0') + format++; + /* (a width, then a precision, of a few digits: the game's are, and + Windows's printf refuses what is not a conversion) */ + for (digits = 0; *format >= L'0' && *format <= L'9'; digits++) + format++; + if (digits > 3) + return FALSE; + if (*format == L'.') { format++; + for (digits = 0; *format >= L'0' && *format <= L'9'; digits++) + format++; + if (digits > 3) + return FALSE; } /* (the size prefixes the game's own formats use) */ if (*format == L'h' || *format == L'l' || *format == L'w') From 073baa3c696872ccc9bdc64834d08f00a91192ba Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:09:26 +1300 Subject: [PATCH 100/187] Scripted first-person cameras blended as before (#189) A scripted first-person camera looks along its unit's aim, which turns once a tick: placed by the observer and the object's move alone, it turned in 30 Hz steps in a vehicle; it is left to its snapshots' blend. --- port/linux/game/render_interpolation.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/port/linux/game/render_interpolation.c b/port/linux/game/render_interpolation.c index 6add1665..05aae5df 100644 --- a/port/linux/game/render_interpolation.c +++ b/port/linux/game/render_interpolation.c @@ -790,13 +790,16 @@ static struct observer_result const *render_interpolation_blended_camera( its pose snaps: it ran up to 2 m ahead of or behind the object, out through the lifepod's hull in a30. It is drawn where the observer has it, moved with the object from where the object is to where it is - drawn. (An animated camera has no such object.) */ + drawn. (An animated camera has no such object; a scripted first-person + camera looks along its unit's aim, which turns once a tick, and is + blended from its snapshots as the player's camera is.) */ if (director_get_perspective(local_player_index) == _director_perspective_scripted) { long relative_object_index = scripted_camera_object_relative_to(); real_matrix4x3 const *drawn; if (relative_object_index != NONE && + !scripted_camera_object_is_first_person_camera(relative_object_index) && object_try_and_get_and_verify_type(relative_object_index, _object_mask_all) && (drawn = render_interpolation_object_node_matrices(relative_object_index)) != NULL) { From d518f54a6f799d00fc7157986c99ce671abd9c99 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:13:08 +1300 Subject: [PATCH 101/187] No debug keyboard End or Escape restart outside the editor (#178) The Xbox debug keyboard's End and Escape stop the movie and restart the map; this keyboard reaches the game only through the console and the menus' text boxes, so they only ever did so by accident (End in a text box too). They are left out, and with them the keys the console consumed on closing and the test and CI steps that extracted them; Escape still closes the console, and the held-key guard still keeps it from pausing. --- .github/workflows/build.yml | 4 - source/input/input.h | 2 - source/input/input_xbox.c | 21 +-- source/main/console.c | 5 +- source/main/main.c | 7 +- tools/test_console_escape.py | 241 ----------------------------------- 6 files changed, 8 insertions(+), 272 deletions(-) delete mode 100644 tools/test_console_escape.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8775172..6d89d654 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,7 +70,6 @@ jobs: run: | python -m pytest -q tools/test_linux_port.py tools/test_host_watch_hash.py tools/harness python tools/test_light_storage.py - python tools/test_console_escape.py - name: Compile cache run: ccache --show-stats @@ -206,9 +205,6 @@ jobs: - name: Test light storage run: python tools/test_light_storage.py - - name: Test console Escape ownership - run: python tools/test_console_escape.py - - name: Compile cache run: ccache --show-stats diff --git a/source/input/input.h b/source/input/input.h index d6562989..bbc1253e 100644 --- a/source/input/input.h +++ b/source/input/input.h @@ -211,8 +211,6 @@ void input_dispose(void); void input_activate(void); void input_deactivate(void); void input_flush(void); -/* Consume a debug keyboard key until release without flushing other input. */ -void input_consume_key(short key_code); boolean input_key_is_down(short key_code); boolean input_get_key(struct key_stroke *key); const struct mouse_state *input_get_mouse_state(void); diff --git a/source/input/input_xbox.c b/source/input/input_xbox.c index 36c108d9..1cca8c37 100644 --- a/source/input/input_xbox.c +++ b/source/input/input_xbox.c @@ -267,7 +267,6 @@ static DWORD WINAPI input_keyboard_thread( static struct input_globals input_globals = {0}; static long gamepad_button_down_times[MAXIMUM_GAMEPADS][NUMBER_OF_GAMEPAD_BUTTONS]; static long key_down_times[NUMBER_OF_KEYS]; -static boolean consumed_keys[NUMBER_OF_KEYS]; static const byte gamepad_analog_button_indices[NUMBER_OF_GAMEPAD_ANALOG_BUTTONS] = { @@ -433,17 +432,6 @@ static void release_input_mutex( return; } -void input_consume_key(short key_code) -{ - if (key_code >= 0 && key_code < NUMBER_OF_KEYS) - { - /* A press and release can arrive in the same update. In that case - * clear the current pulse without consuming the next fresh press. */ - consumed_keys[key_code] = input_globals.key_latches[key_code] != FALSE; - input_globals.key_ticks[key_code] = 0; - } -} - void input_suppress( void) { @@ -491,7 +479,6 @@ void input_dispose( void input_flush( void) { - csmemset(consumed_keys, 0, sizeof(consumed_keys)); csmemset( input_globals.gamepad_states, 0, @@ -581,8 +568,7 @@ boolean input_key_is_down( { boolean result = FALSE; - if (!input_globals.suppressed && - !(key_code >= 0 && key_code < NUMBER_OF_KEYS && consumed_keys[key_code])) + if (!input_globals.suppressed) { switch (key_code) { @@ -1112,16 +1098,13 @@ static void input_update_keyboard_devices( if (keystroke.Flags & XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP) { input_globals.key_latches[key.key_code] = FALSE; - if (consumed_keys[key.key_code] || input_globals.key_ticks[key.key_code] > 1) + if (input_globals.key_ticks[key.key_code] > 1) { input_globals.key_ticks[key.key_code] = 0; } - consumed_keys[key.key_code] = FALSE; } else { - if (consumed_keys[key.key_code]) - continue; if (input_globals.buffered_key_write_index < MAXIMUM_BUFFERED_KEYSTROKES) { input_globals.buffered_keys[ diff --git a/source/main/console.c b/source/main/console.c index 908d988f..befe80fc 100644 --- a/source/main/console.c +++ b/source/main/console.c @@ -141,10 +141,6 @@ void console_close( { if (console_globals.active) { - /* The reset hotkeys belong to the console until their release, even - * when it closes with backquote or an empty command instead of Escape. */ - input_consume_key(_key_escape); - input_consume_key(_key_end); terminal_gets_end(&console_globals.input_state); console_globals.active = FALSE; } @@ -459,6 +455,7 @@ boolean console_update( match_assert("c:\\halo\\SOURCE\\main\\console.c", 184, key->key_code!=NONE); switch (key->key_code) { + /* port: escape closes it too */ case _key_escape: console_close(); return FALSE; diff --git a/source/main/main.c b/source/main/main.c index 67bd8544..daf9c65f 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -3474,8 +3474,11 @@ void main_loop( process_ui_widgets(); bink_playback_update(); - if ((!game_in_editor() && !console_is_active() && - (input_key_is_down(_key_end) || input_key_is_down(_key_escape))) || editor_should_exit()) + /* port: not the Xbox debug keyboard's End and Escape, which stop + the movie and restart the map: this keyboard reaches the game only + through the console and the menus' text boxes, whose End and Escape + they are (port/linux/src/xinput_sdl.c) */ + if (editor_should_exit()) { main_movie_stop(); diff --git a/tools/test_console_escape.py b/tools/test_console_escape.py deleted file mode 100644 index f92c7855..00000000 --- a/tools/test_console_escape.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Check console Escape ownership through production keyboard and main-loop code. - ---baseline-ref upstream/main reproduces the old failure without opening a game. -""" -import argparse -from pathlib import Path -import subprocess -import sys -import tempfile - -ROOT = Path(__file__).resolve().parent.parent - - -def block(source, marker): - start = source.index(marker) - end = source.index('{', start) + 1 - depth = 1 - while depth: - depth += (source[end] == '{') - (source[end] == '}') - end += 1 - return source[start:end] - - -PRELUDE = r''' -#define _CRT_SECURE_NO_WARNINGS -#include -#include -#include -typedef int boolean,BOOL;typedef unsigned DWORD;typedef unsigned char byte; -#define TRUE 1 -#define FALSE 0 -#define NONE (-1) -#define WINAPI -#define ERROR_SUCCESS 0 -#define ERROR_HANDLE_EOF 38 -#define XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP 1 -#define XINPUT_DEBUG_KEYSTROKE_FLAG_SHIFT 4 -#define XINPUT_DEBUG_KEYSTROKE_FLAG_CTRL 8 -#define XINPUT_DEBUG_KEYSTROKE_FLAG_ALT 16 -#define VK_OEM_3_BACKQUOTE 0xc0 -#define XDEVICE_TYPE_DEBUG_KEYBOARD 1 -#define XDEVICE_NO_SLOT 0 -#define LONG_BITS 32 -#define TICKS_PER_SECOND 30 -#define UNSIGNED_CHAR_MAX 255 -#define MAXIMUM_BUFFERED_KEYSTROKES 64 -#define MAXIMUM_NUMBER_OF_PREVIOUS_COMMANDS 8 -#define NUMBER_OF_VIRTUAL_CODES 256 -#define NUMBER_OF_ASCII_CODES 128 -#define FLAG(n) (1u<<(n)) -#define SET_FLAG(v,b,s) ((v)=(s)?((v)|FLAG(b)):((v)&~FLAG(b))) -#define MAX(a,b) ((a)>(b)?(a):(b)) -#define MIN(a,b) ((a)<(b)?(a):(b)) -#define PIN(v,a,b) MAX((a),MIN((v),(b))) -#define csmemset memset -#define CHECK(c) do{if(!(c)){fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#c);exit(1);}}while(0) -#define match_assert(file,line,c) CHECK(c) -enum {_key_modifier_control=1,_key_modifier_alt=2}; -struct key_stroke {short key_code,ascii_code,modifier_flags;}; -struct platform_keystroke {int virtual_key,ascii,flags;}; -typedef struct {int VirtualKey,Ascii,Flags;} XINPUT_DEBUG_KEYSTROKE,*PXINPUT_DEBUG_KEYSTROKE; -static struct platform_keystroke queue[64];static int read_at,write_at; -static int text_typing,now,engine,editor,editor_exit,movie_stops,commands,completions,closes; -static struct {int switch_to_structure_bsp_index,saving_map,reset_map,lost_map;} main_globals; -static int platform_next_keystroke(struct platform_keystroke *k){if(read_at==write_at)return 0;*k=queue[read_at++];return 1;} -static void enqueue(int vk,int flags){CHECK(write_at<64);queue[write_at++]=(struct platform_keystroke){vk,0,flags};} -static long system_milliseconds(void){return now;} -static int XGetDeviceChanges(int type,unsigned long *a,unsigned long *b){(void)type;(void)a;(void)b;return 0;} -static void XInputClose(void *handle){(void)handle;} -static void *XInputOpen(int a,long b,int c,void *d){(void)a;(void)b;(void)c;(void)d;return NULL;} -static int GetLastError(void){return 0;} -#define _error_silent 0 -static void error(int level,const char *message,...){(void)level;(void)message;CHECK(0);} -static int game_in_editor(void){return editor;} -static int editor_should_exit(void){return editor_exit;} -static int game_engine_running(void){return engine;} -static void main_movie_stop(void){movie_stops++;} -struct edit_text {int unused;}; -struct terminal_gets_state {char result[512];short key_count;struct key_stroke keys[64];struct edit_text edit;}; -static boolean terminal_gets_begin(struct terminal_gets_state *s){s->key_count=0;return TRUE;} -static void terminal_gets_end(struct terminal_gets_state *s){(void)s;closes++;} -static void edit_text_selection_reset(struct edit_text *e){(void)e;} -static int profile_global_enable; -static boolean console_process_command(const char *s){(void)s;commands++;return TRUE;} -static void console_complete(void){completions++;} -enum {SDL_SCANCODE_ESCAPE,SDL_SCANCODE_END,SDL_SCANCODE_W,SDL_SCANCODE_COUNT=512}; -struct platform_input_state {byte keys[SDL_SCANCODE_COUNT];int menus;}; -''' -TESTS = r''' -static unsigned probes; -static void reset(void){ - memset(&input_globals,0,sizeof(input_globals));memset(&console_globals,0,sizeof(console_globals)); - memset(&main_globals,0,sizeof(main_globals));memset(consumed_keys,0,sizeof(consumed_keys)); - read_at=write_at=engine=editor=editor_exit=movie_stops=commands=completions=closes=0;now=0; - memset(input_globals.gamepad_states,0x35,sizeof(input_globals.gamepad_states)); -} -static void tick(void){now+=16;input_update_keyboard_devices();read_at=write_at=0;} -static void terminal_input(void){ - console_globals.input_state.key_count=0; - struct key_stroke key;while(input_get_key(&key))console_globals.input_state.keys[console_globals.input_state.key_count++]=key; -} -static void press_escape(int fast){enqueue(0x1b,0);if(fast)enqueue(0x1b,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick();} -static void escape_close(int fast,int multiplayer){ - reset();engine=multiplayer;console_open();strcpy(console_globals.input_state.result,"(quit)"); - unsigned char controllers[sizeof(input_globals.gamepad_states)];memcpy(controllers,input_globals.gamepad_states,sizeof(controllers)); - press_escape(fast);reset_hotkey();terminal_input();console_update(); - if(BASELINE){CHECK(main_globals.reset_map==!multiplayer);CHECK(console_is_active());return;} - CHECK(!main_globals.reset_map&&!movie_stops&&!console_is_active()&&closes==1&&commands==0); - CHECK(!memcmp(controllers,input_globals.gamepad_states,sizeof(controllers))); - CHECK(!input_key_is_down(_key_escape)); - for(int frame=0;frame<300;frame++){ - if(frame%10==0)enqueue(0x1b,2); /* auto-repeat, filtered after close */ - tick();reset_hotkey();CHECK(!main_globals.reset_map);CHECK(!input_key_is_down(_key_escape));probes++; - } - enqueue(0x1b,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick(); - console_open();press_escape(0);terminal_input();console_update();CHECK(!console_is_active()); -} -static void end_and_other_close(void){ - for(int closing=0;closing<3;closing++){ - reset();console_open();enqueue(0x23,0);tick();reset_hotkey(); - CHECK(!main_globals.reset_map);CHECK(input_key_is_down(_key_end)); - if(closing==0)console_close(); - else{enqueue(closing==1?VK_OEM_3_BACKQUOTE:0x0d,0);tick();terminal_input();console_update();} - CHECK(!console_is_active()); - for(int frame=0;frame<300;frame++){tick();reset_hotkey();CHECK(!main_globals.reset_map&&!input_key_is_down(_key_end));probes++;} - enqueue(0x23,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick();console_open();enqueue(0x23,0);tick();CHECK(input_key_is_down(_key_end)); - } - /* Commands and completion retain their normal behavior. */ - reset();console_open();strcpy(console_globals.input_state.result,"fov_desired 100"); - enqueue(0x0d,0);tick();terminal_input();console_update();CHECK(commands==1&&console_is_active()); - enqueue(0x09,0);tick();terminal_input();console_update();CHECK(completions==1&&console_is_active()); -} -static void consumption(void){ - /* Releasing on the first frame after close must not expose a new pulse. */ - reset();console_open();press_escape(0);reset_hotkey();terminal_input();console_update(); - enqueue(0x1b,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick();reset_hotkey(); - CHECK(!console_is_active()&&!main_globals.reset_map&&!input_key_is_down(_key_escape)); - /* Escape cancels the remaining command keys in the same buffered batch. */ - reset();console_open();strcpy(console_globals.input_state.result,"(quit)"); - enqueue(0x1b,0);enqueue(0x0d,0);tick();terminal_input();console_update(); - CHECK(!console_is_active()&&!commands); - reset();input_globals.key_latches[_key_escape]=TRUE;input_globals.key_ticks[_key_escape]=5; - input_globals.key_latches[_key_w]=TRUE;input_globals.key_ticks[_key_w]=7; - input_consume_key(_key_escape);CHECK(!input_key_is_down(_key_escape));CHECK(input_key_is_down(_key_w)==7); - console_open();enqueue(0x1b,2);tick();CHECK(input_globals.buffered_key_write_index==0&&!input_key_is_down(_key_escape)); - enqueue(0x1b,XINPUT_DEBUG_KEYSTROKE_FLAG_KEYUP);tick();CHECK(!consumed_keys[_key_escape]); - enqueue(0x1b,0);tick();CHECK(input_key_is_down(_key_escape)); - /* A full flush clears consumption; invalid keys cannot touch state. */ - input_consume_key(_key_escape);input_flush();CHECK(!consumed_keys[_key_escape]); - input_consume_key(-1);input_consume_key(NUMBER_OF_KEYS);CHECK(!input_key_is_down(_key_escape)); -} -static void ownership(void){ - for(int menu=0;menu<2;menu++)for(int fast=0;fast<2;fast++){ - reset();struct platform_input_state input={.menus=menu};keys_held_over_switch(&input); - console_open();keys_held_over_switch(&input); - input.keys[SDL_SCANCODE_ESCAPE]=1;input.keys[SDL_SCANCODE_W]=1;keys_held_over_switch(&input); - console_close(); - for(int frame=0;frame<300;frame++){ - input.keys[SDL_SCANCODE_ESCAPE]=!fast;input.keys[SDL_SCANCODE_W]=!fast;keys_held_over_switch(&input); - CHECK(!input.keys[SDL_SCANCODE_ESCAPE]&&!input.keys[SDL_SCANCODE_W]);probes++; - } - memset(input.keys,0,sizeof(input.keys));keys_held_over_switch(&input); - input.keys[SDL_SCANCODE_ESCAPE]=1;keys_held_over_switch(&input);CHECK(input.keys[SDL_SCANCODE_ESCAPE]); - /* The existing pause/menu transition still swallows the opening press. */ - input.menus=!menu;keys_held_over_switch(&input);CHECK(!input.keys[SDL_SCANCODE_ESCAPE]); - memset(input.keys,0,sizeof(input.keys));keys_held_over_switch(&input); - input.keys[SDL_SCANCODE_ESCAPE]=1;keys_held_over_switch(&input);CHECK(input.keys[SDL_SCANCODE_ESCAPE]); - } -} -static void legacy_and_editor(void){ - reset();input_globals.key_ticks[_key_escape]=1;reset_hotkey();CHECK(main_globals.reset_map&&movie_stops==1); - reset();input_globals.key_ticks[_key_end]=1;reset_hotkey();CHECK(main_globals.reset_map); - reset();engine=1;input_globals.key_ticks[_key_escape]=1;reset_hotkey();CHECK(!main_globals.reset_map&&movie_stops==1); - reset();editor=1;input_globals.key_ticks[_key_escape]=1;reset_hotkey();CHECK(!main_globals.reset_map); - console_open();editor_exit=1;reset_hotkey();CHECK(main_globals.reset_map&&movie_stops==1); -} -int main(void){ - for(int fast=0;fast<2;fast++)for(int multiplayer=0;multiplayer<2;multiplayer++)escape_close(fast,multiplayer); - if(BASELINE){puts("REPRODUCED: console Escape requests campaign reset before console update and does not close the console");return 0;} - end_and_other_close();consumption();ownership();legacy_and_editor(); - printf("PASS: %u held-input frames; console Escape closes without reset/pause spill, End editing/other close paths, repeat/release/fast taps, normal command/completion, keyboard menu transitions, unchanged co-op controller data and legacy/editor hotkeys\n",probes); -} -''' - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--cc', default='clang') - parser.add_argument('--baseline-ref') - args = parser.parse_args() - - def read(path): - if args.baseline_ref: - return subprocess.check_output(['git', 'show', f'{args.baseline_ref}:{path}'], cwd=ROOT, text=True) - return (ROOT/path).read_text() - - inputs = read('source/input/input_xbox.c') - console = read('source/main/console.c') - port = read('port/linux/src/xinput_sdl.c') - main_loop = read('source/main/main.c') - key_header = read('source/input/input.h') - start = key_header.rfind('enum', 0, key_header.index('_key_escape = 0')) - keys = block(key_header[start:], 'enum') + ';\n' - globals_source = r''' -static struct {void *keyboard_handle;byte key_ticks[NUMBER_OF_KEYS],key_latches[NUMBER_OF_KEYS]; - boolean suppressed;short buffered_key_read_index,buffered_key_write_index; - struct key_stroke buffered_keys[MAXIMUM_BUFFERED_KEYSTROKES];byte gamepad_states[4][32];} input_globals; -static long key_down_times[NUMBER_OF_KEYS]; -static boolean consumed_keys[NUMBER_OF_KEYS]; -''' - source = PRELUDE + keys + globals_source - source += block(console, 'struct console_globals') + ';\nstatic struct console_globals console_globals;\n' - source += block(console, 'boolean console_is_active(') + '\n' - for marker in ('static const short virtual_key_to_key_code', 'static const short ascii_to_key_code'): - start = inputs.index(marker) - source += inputs[start:inputs.index(';', start)+1] + '\n' - for marker in ('static void update_hold_ticks(', 'void input_flush(', 'boolean input_key_is_down(', 'boolean input_get_key('): - source += block(inputs[inputs.index('/* ---------- public code */'):], marker) + '\n' - # The device poll calls the queue adapter declared later in this harness. - source += 'DWORD WINAPI XInputDebugGetKeystroke(PXINPUT_DEBUG_KEYSTROKE);\n' - source += block(inputs[inputs.index('/* ---------- private code */'):], 'static void input_update_keyboard_devices(') + '\n' - source += (block(inputs, 'void input_consume_key(') if not args.baseline_ref else 'static void input_consume_key(short key){(void)key;}') + '\n' - source += block(port, 'DWORD WINAPI XInputDebugGetKeystroke(') + '\n' - for marker in ('void console_open(', 'void console_close(', 'boolean console_update('): - source += block(console, marker) + '\n' - source += block(port, 'static void keys_held_over_switch(') + '\n' - source += block(main_loop, 'void main_reset_map(') + '\n' - source += 'static void reset_hotkey(void){' + block(main_loop, 'if ((!game_in_editor() &&') + '}\n' - source += f'\n#define BASELINE {int(bool(args.baseline_ref))}\n' + TESTS - flags = ['-std=gnu11', '-O2', '-fuse-ld=lld'] - flags += ['--target=i686-pc-windows-msvc'] if sys.platform == 'win32' else ['-m32'] - with tempfile.TemporaryDirectory(prefix='halo-console-escape-') as directory: - path = Path(directory) - (path/'test.c').write_text(source) - exe = path/'test.exe' - subprocess.run([args.cc, *flags, str(path/'test.c'), '-o', str(exe)], check=True) - subprocess.run([str(exe)], check=True, timeout=15) - - -if __name__ == '__main__': - main() From f6c7f93f98fb82da530c2c47a813d05d60ea75d5 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:14:40 +1300 Subject: [PATCH 102/187] Looping sounds' replaced components only, no fade endpoints (#187) A completed fade comes back at full gain only to a second query in the same frame, which the game never makes: update_channels stops a voice whose fade out reaches 0, and sound_start_fade starts a new fade from it. Keeping the endpoint made every new voice's gain depend on fade_interpolation_end being set where it is created. That part is left out, and its test cases with it; replaced looping components are still retired. --- source/sound/sound_manager.c | 10 +--------- tools/harness/tests/sound_lifecycle.c | 11 ----------- tools/harness/tests/test_sound_lifecycle.py | 5 +---- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/source/sound/sound_manager.c b/source/sound/sound_manager.c index 0d432250..3e29c53f 100644 --- a/source/sound/sound_manager.c +++ b/source/sound/sound_manager.c @@ -1615,9 +1615,6 @@ static long update_potentially_audible_looping_sound( sound->track_proc = track_loop_track_sound; sound->fade_stop_time = 0; sound->fade_start_time = 0; - /* port: an unfaded voice starts at full gain. */ - sound->fade_interpolation_start = 1.f; - sound->fade_interpolation_end = 1.f; sound->next_definition_index = NONE; sound->pitch_range_index = sound_definition_find_pitch_range_by_pitch( @@ -1801,9 +1798,7 @@ static real sound_calculate_fade( long sound_index) { struct sound_datum *sound = sound_get(sound_index); - /* port: a completed fade keeps its endpoint. Another query in this frame (or - after a cache delay) must not bring a stopped voice back to full gain. */ - real fade = sound->fade_interpolation_end; + real fade = 1.f; if (sound->fade_start_time != sound->fade_stop_time) { @@ -2553,9 +2548,6 @@ long sound_new_impulse( NONE); sound->fade_stop_time = 0; sound->fade_start_time = 0; - /* port: an unfaded voice starts at full gain. */ - sound->fade_interpolation_start = 1.f; - sound->fade_interpolation_end = 1.f; sound->loop_track_index = NONE; _sound_cache_sound_request( sound_permutation_get( diff --git a/tools/harness/tests/sound_lifecycle.c b/tools/harness/tests/sound_lifecycle.c index 67b92a54..cabbfd97 100644 --- a/tools/harness/tests/sound_lifecycle.c +++ b/tools/harness/tests/sound_lifecycle.c @@ -107,17 +107,6 @@ int main(int argc,char **argv) { if(!strcmp(case_name,"four-tracks")) test_loop_definition.tracks.count=4; refresh(_looping_sound_refresh_start);long primary=loops[0].tracks[0].primary_sound_index,secondary=NONE; CASE("fresh-loop") { CHECK(sound_calculate_fade(primary)==1.f,"new loop audible");return 0; } - if(!strcmp(case_name,"fade-end") || !strcmp(case_name,"crossfade")) { - sound_start_fade(!strcmp(case_name,"crossfade")?_sound_fade_mode_crossfade:_sound_fade_mode_linear,.1f,NONE,primary); - for(int f=0;f<=100;f++) { - sound_manager_globals.render_time=1000+f; - /* Store each result as real: i386's x87 may otherwise keep excess precision. */ - volatile real a=sound_calculate_fade(primary),b=sound_calculate_fade(primary); - CHECK(a==b,"repeated fade queries agree at frame %d: %a / %a",f,(double)a,(double)b); - } - CHECK(sound_calculate_fade(primary)==0.f,"fade remains zero at its endpoint"); - sound_manager_globals.render_time=5000;CHECK(sound_calculate_fade(primary)==0.f,"completed fade never revives");return 0; - } CASE("pending") { secondary=update_potentially_audible_looping_sound(2,0,0,_sound_loop_track); } CASE("restart") { refresh(_looping_sound_refresh_start); } CASE("intro-loop") { diff --git a/tools/harness/tests/test_sound_lifecycle.py b/tools/harness/tests/test_sound_lifecycle.py index a5e029dd..0f8676d7 100644 --- a/tools/harness/tests/test_sound_lifecycle.py +++ b/tools/harness/tests/test_sound_lifecycle.py @@ -9,15 +9,12 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from harness import CHECK_FAILED, build, enum_with, function, mutated, read, run from harness.audio import manager_types -CASES = ["fresh-loop", "fresh-impulse", "fade-end", "crossfade", "pending", "restart", "intro-loop", +CASES = ["fresh-loop", "fresh-impulse", "pending", "restart", "intro-loop", "rapid", "other-owner", "other-track", "stop-cue", "fake-impulse", "repeated-stop", "four-tracks"] NEGATIVE_CONTROLS = { - "revives-completed-fade": (("real fade = sound->fade_interpolation_end;", "real fade = 1.f;"), "fade-end"), "keeps-replaced-voice": (("sound_index != except_sound_index &&", "FALSE &&"), "restart"), "cancels-other-owner": (("sound->source_identifier == looping_sound_index &&", "TRUE &&"), "other-owner"), "cancels-other-track": (("sound->loop_track_index == track_index)", "TRUE)"), "other-track"), - "silent-new-loop": (("sound->fade_interpolation_end = 1.f;", "sound->fade_interpolation_end = 0.f;"), "fresh-loop"), - "silent-new-impulse": (("sound->fade_interpolation_end = 1.f;", "sound->fade_interpolation_end = 0.f;"), "fresh-impulse"), } From 0982cdb93e2075819c99d5995d640ef20ec21351 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:27:38 +1300 Subject: [PATCH 103/187] Steam's on-screen keyboard only, sysroot links kept in, SDL pins noted (#218) A Wayland touch screen's keyboard types text events, which the menus' fields do not read: text input is started only for Steam's keyboard (SDL_ENABLE_STEAM_SCREEN_KEYBOARD). The sysroot's extraction writes nothing through a link that leads out of it. The three SDL versions (Windows, Android, the Linux sysroot) name each other. #222's test stubs the screen keyboard and passes the text field's new password argument. --- port/linux/src/sdl_platform.c | 20 ++++++++++++-------- tools/android_build.py | 1 + tools/harness/tests/profile_keyboard.c | 5 +++-- tools/linux_sysroot.py | 7 ++++++- tools/windows_build.py | 1 + 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 6d33998c..d8268e96 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -1658,13 +1658,14 @@ void platform_video_window_size(int *width, int *height) /* ---------- the system's on-screen keyboard */ /* A menu's text field is typed into (platform_text_field, xinput_sdl.c). -Where the system has an on-screen keyboard that text input brings up (Steam's, -in Big Picture and in the Steam Deck's Game Mode, which ask for it with -SDL_ENABLE_STEAM_SCREEN_KEYBOARD; a Wayland touch screen without a keyboard), -SDL's text input runs while the field is typed into: the keyboard comes up -with the field and goes with it, and what it types arrives as keys. Elsewhere -text input stays off, as before, so that no input method takes the keys the -field reads. */ +Where Steam's on-screen keyboard is there to bring up (in Big Picture and in +the Steam Deck's Game Mode, which ask for it with +SDL_ENABLE_STEAM_SCREEN_KEYBOARD), SDL's text input runs while the field is +typed into: the keyboard comes up with the field and goes with it, and what +it types arrives as keys. Elsewhere text input stays off, as before, so that +no input method takes the keys the field reads: a Wayland touch screen's +keyboard (text-input-v3) would type text events, which the field does not +read. */ static SDL_AtomicInt screen_keyboard_wanted; /* (each field begun, which brings the keyboard up again: Steam does not say when its keyboard goes, by its own Enter or closed by hand, so SDL holds it @@ -1702,8 +1703,11 @@ static void screen_keyboard_update(void) if (requests != requests_handled) { requests_handled = requests; - if (!SDL_HasScreenKeyboardSupport()) + if (!SDL_HasScreenKeyboardSupport() || + !SDL_GetHintBoolean(SDL_HINT_ENABLE_STEAM_SCREEN_KEYBOARD, false)) + { return; + } open_time = SDL_GetTicks(); if (SDL_TextInputActive(platform_window)) { diff --git a/tools/android_build.py b/tools/android_build.py index 53381667..1e03410a 100644 --- a/tools/android_build.py +++ b/tools/android_build.py @@ -53,6 +53,7 @@ ZLIB_DEFINES = ("-DZ_PREFIX", "-Dz_errmsg=z_port_errmsg") MUSL_VERSION = "1.2.5" MUSL_DIR = THIRD_PARTY / f"musl-{MUSL_VERSION}" MUSL_URL = f"https://musl.libc.org/releases/musl-{MUSL_VERSION}.tar.gz" +# (as tools/windows_build.py's and tools/linux_sysroot.py's SDL_VERSION) SDL_TAG = "release-3.4.16" SDL_DIR = THIRD_PARTY / "SDL3" SDL_URL = "https://github.com/libsdl-org/SDL.git" diff --git a/tools/harness/tests/profile_keyboard.c b/tools/harness/tests/profile_keyboard.c index 5b33f50a..28fbfd4b 100644 --- a/tools/harness/tests/profile_keyboard.c +++ b/tools/harness/tests/profile_keyboard.c @@ -10,6 +10,7 @@ typedef short SHORT; struct platform_input_state { unsigned char keys[TEST_SCANCODE_COUNT], mouse_buttons[8]; BOOL mouse_released, menus; }; #define SDL_SCANCODE_COUNT TEST_SCANCODE_COUNT #define console_is_active() FALSE +#define platform_screen_keyboard(show, password) ((void)0) typedef struct { unsigned short wButtons; BYTE bAnalogButtons[8]; SHORT sThumbLX, sThumbLY; } XINPUT_GAMEPAD; #define SDL_BUTTON_X1 4 static BOOL text_typing, text_typing_enter_armed, text_typing_keyboard, text_typing_field; @@ -96,9 +97,9 @@ int main(int argc, char **argv) else CASE("duplicate-name") { unique = FALSE; wcscpy(name,L"Other"); virtual_keyboard_select(); CHECK(errors==1 && !virtual_keyboard_globals.last_exit_saved_text, "duplicate name accepted"); } else CASE("field-owner") { - platform_text_field(TRUE); virtual_keyboard_select(); + platform_text_field(TRUE, FALSE); virtual_keyboard_select(); CHECK(!virtual_keyboard_globals.active && text_typing, "closing keyboard cleared text field ownership"); - platform_text_field(FALSE); + platform_text_field(FALSE, FALSE); } else CASE("reopen") { diff --git a/tools/linux_sysroot.py b/tools/linux_sysroot.py index d774a67d..6edc34bb 100644 --- a/tools/linux_sysroot.py +++ b/tools/linux_sysroot.py @@ -167,6 +167,7 @@ PACKAGES: List[Tuple[str, str, str]] = [ # SDL3's source release (the version of the other ports' SDL), from GitHub # or libsdl.org +# (as tools/windows_build.py's SDL_VERSION and tools/android_build.py's SDL_TAG) SDL_VERSION = "3.4.16" SDL_URLS = [ f"https://github.com/libsdl-org/SDL/releases/download/release-{SDL_VERSION}/SDL3-{SDL_VERSION}.tar.gz", @@ -256,12 +257,16 @@ UNUSED = (("usr", "share", "doc"), ("usr", "share", "man"), ("usr", "share", "lo def extract(archive: tarfile.TarFile, destination: Path, strip: int = 0) -> None: """an archive's files, folders and links into destination, nothing - outside it (no device files, no absolute or upward names)""" + outside it (no device files, no absolute or upward names, nothing + through a link of its own that leads out of it)""" + root = destination.resolve() for member in archive.getmembers(): parts = [part for part in Path(member.name).parts if part not in (".", "/")][strip:] if not parts or ".." in parts or any(tuple(parts[:len(unused)]) == unused for unused in UNUSED): continue target = destination.joinpath(*parts) + if not target.parent.resolve().is_relative_to(root): + continue if member.isdir(): target.mkdir(parents=True, exist_ok=True) elif member.isfile() or member.islnk(): diff --git a/tools/windows_build.py b/tools/windows_build.py index 111e0169..f3d0334d 100644 --- a/tools/windows_build.py +++ b/tools/windows_build.py @@ -30,6 +30,7 @@ PORT_DIR = Path("port/windows") PORT_CONFIG = PORT_DIR / "port.json" BUILD = Path("build/windows") +# (as tools/android_build.py's SDL_TAG and tools/linux_sysroot.py's SDL_VERSION) SDL_VERSION = "3.4.16" SDL_URL = ( f"https://github.com/libsdl-org/SDL/releases/download/release-{SDL_VERSION}/" From 08d91f82fa526bd66e0f9fbaac6c2b6b31a8a61b Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:48:56 +1300 Subject: [PATCH 104/187] Animation enhancements behind game.enhanced_animations, node count kept (#144) The enhancements are on unless config.toml's game.enhanced_animations (HALO_ENHANCED_ANIMATIONS) turns them off; the setting is not in the menus. unit_preprocess_node_orientations sets model_node_count again before the fitting checks use it, as it did: the PR had removed it, which left it unset for every unit. models.h for model_finds_node, and the PR's indentation and spacing as the files'. --- port/linux/README.md | 1 + port/linux/src/port_config.c | 6 ++++++ source/units/bipeds.c | 23 ++++++++++---------- source/units/units.c | 42 ++++++++++++++++++++++++++++++------ 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index dbd664ab..c4bf6826 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -349,6 +349,7 @@ the setting for one start of the game. It has priority over the file. | `controls.` | (the table in "Controls") | `HALO_KEY_` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`, `push_to_talk`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. | | `game.console_log` | `"important"` | `HALO_CONSOLE_LOG` | What the console shows on the screen. `"important"`: bans, players that the host drops for cheating, the reasons that the game refuses a command, and the asserts that stop the game. `"all"`: all the lines. `"none"`: only the asserts that stop the game. The output of a command always shows. `debug.txt` gets all the lines. | | `game.language` | `""` | `HALO_LANGUAGE` | The language of the menus: `ja`, `de`, `fr`, `es` or `it`. Empty: English. | +| `game.enhanced_animations` | `true` | `HALO_ENHANCED_ANIMATIONS` | `true`: the player bipeds' grenade throws keep their legs moving, blended by speed and direction (crouched throws stay crouched, throws in the air use the jump's legs), Warthog and Scorpion riders stay seated to throw and let go of the grips to throw and reload, and a player turns with the aim while throwing, as while meleeing. `false`: the original animations, which freeze the legs during a throw and stand a rider up. Only in config.toml, not in the menus. | | `paths.data` | `""` | `HALO_DATA_ROOT` | The data root. Refer to "Start the game". | | `paths.saves` | `""` | `HALO_SAVE_ROOT` | The save root. Refer to "Files and folders". | | `network.address` | `""` | `HALO_NET_ADDRESS` | The IPv4 address of this machine for system link. Refer to "Play on one computer". | diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index b028cfca..f68d4115 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -260,6 +260,12 @@ static const struct config_setting config_settings[] = { "game.language", _config_string, "\"\"", "HALO_LANGUAGE", _environment_value, _platform_all, "The language the game asks the Xbox for: \"ja\", \"de\", \"fr\", \"es\" or \"it\";\n" "empty for English. The game data decides what is translated." }, + { "game.enhanced_animations", _config_boolean, "true", "HALO_ENHANCED_ANIMATIONS", _environment_value, _platform_all, + "The player bipeds' grenade throws keep their legs moving (crouched,\n" + "in the air and in a vehicle's seat too), riders' hands leave the grips\n" + "to throw and reload, and a player turns with the aim while throwing;\n" + "false: the original animations, which freeze the legs and stand a\n" + "rider up." }, { "game.custom_edition", _config_boolean, "true", "HALO_CUSTOM_EDITION", _environment_value, _platform_all, "Load and run Halo Custom Edition maps (not those that need OpenSauce):\n" "put them and Custom Edition's bitmaps.map, sounds.map and loc.map in\n" diff --git a/source/units/bipeds.c b/source/units/bipeds.c index f44869a9..26984245 100644 --- a/source/units/bipeds.c +++ b/source/units/bipeds.c @@ -3338,21 +3338,22 @@ static void biped_update_turning( biped_snap_facing(biped_index); biped_verify_object_vectors(biped_index, "post-fly-turn"); } - else if (biped->unit.animation.base_seat_index!=_unit_base_seat_asleep) - { - boolean flaming = - biped->unit.animation.base_seat_index==_unit_base_seat_flaming; - /* port: a player throwing a grenade turns with the aim, as one - meleeing does: the melee plays over the moving or idle state, the - throw is a state of its own, which the original never turned in */ + else if (biped->unit.animation.base_seat_index!=_unit_base_seat_asleep) + { + boolean flaming = + biped->unit.animation.base_seat_index==_unit_base_seat_flaming; + /* port: a player throwing a grenade turns with the aim, as one + meleeing does: the melee plays over the moving or idle state, the + throw is a state of its own, which the original never turned in + (game.enhanced_animations: unit_animation_enhanced) */ boolean throwing_turn = biped->unit.player_index!=NONE && biped->unit.animation.state==_unit_state_throw_grenade && unit_animation_enhanced(biped_index); - real_vector3d turn_axis; - real turn_error; - real facing_alignment; - boolean turn_right; + real_vector3d turn_axis; + real turn_error; + real facing_alignment; + boolean turn_right; if (TEST_FLAG(flags, _biped_climbs_anything_bit)) { diff --git a/source/units/units.c b/source/units/units.c index 78f2d16c..588e6932 100644 --- a/source/units/units.c +++ b/source/units/units.c @@ -676,6 +676,7 @@ symbols in this file: #include "main/console.h" #include "models/model_animation_definitions.h" #include "models/model_definitions.h" +#include "models/models.h" /* port: model_find_node (the grenade throw's legs) */ #include "objects/damage.h" #include "objects/damage_effect_definitions.h" #include "objects/object_lights.h" @@ -3313,7 +3314,13 @@ void unit_impulse( return; } -/* port: the animation enhancements are for the player bipeds (cyborg and + +/* the platform layer's (port/linux/src/port_config.c) */ +int config_boolean(const char *name); +unsigned long config_changes(void); + +/* port: the animation enhancements (game.enhanced_animations, on unless +config.toml turns them off) are for the player bipeds (cyborg and cyborg_mp), whose animations they were made with; every other unit keeps the original animations */ static char const *const enhanced_animation_units[] = @@ -3325,10 +3332,24 @@ static char const *const enhanced_animation_units[] = boolean unit_animation_enhanced( long unit_index) { - struct unit_datum *unit = unit_get(unit_index); - char const *name = tag_name_strip_path(tag_get_name(unit->definition_index)); + static boolean enhanced = TRUE; + static unsigned long read_at = (unsigned long)-1; + struct unit_datum *unit; + char const *name; long name_index; + /* (read again when the settings change) */ + if (read_at != config_changes()) + { + read_at = config_changes(); + enhanced = config_boolean("game.enhanced_animations") != 0; + } + if (!enhanced) + { + return FALSE; + } + unit = unit_get(unit_index); + name = tag_name_strip_path(tag_get_name(unit->definition_index)); for (name_index = 0; name_index < NUMBEROF(enhanced_animation_units); name_index++) { if (!_stricmp(name, enhanced_animation_units[name_index])) @@ -3339,6 +3360,7 @@ boolean unit_animation_enhanced( return FALSE; } + /* port: a seat whose graph has no reload of its own (a Scorpion rider's) reloads with the standing seat's, the same weapon's: a reload is a replacement animation, which moves only the arms and the weapon */ @@ -3383,6 +3405,7 @@ static short unit_standing_weapon_type_animation( return NONE; } + void unit_animation_start_action( long unit_index, short action) @@ -10826,6 +10849,7 @@ static boolean unit_animation_set_state( done: return result; } + /* port: a unit throwing a grenade on the move keeps its lower body (the pelvis and the legs) in the movement animation; the throw keeps the spine and everything above it. The throw is a base animation, which sets every @@ -10846,6 +10870,7 @@ static boolean unit_node_is_at_or_below( return FALSE; } + /* the speed, in world units per second, at and above which the legs move fully; below it they blend toward the rest pose */ #define GRENADE_THROW_FULL_MOVE_SPEED 0.75f @@ -10960,8 +10985,8 @@ static struct animation *unit_weapon_class_base_animation( return NULL; } - return animation; - } + return animation; +} /* port: the nodes a unit's orientations hold: its model's, no more than a model can have (object_new sizes them from the model) */ @@ -11111,7 +11136,7 @@ static void unit_grenade_throw_keep_legs_moving( weapon_class = TAG_BLOCK_GET_ELEMENT(&unit_seat->weapon_classes, unit->unit.animation.weapon_index, struct animation_graph_weapon_class); model = model_definition_get(unit_definition->object.model.index); now = game_time_get(); - if (model->nodes.count > MAXIMUM_NODES_PER_MODEL) + if (model->nodes.count > MAXIMUM_NODES_PER_MODEL) { return; } @@ -11272,6 +11297,7 @@ static void unit_grenade_throw_keep_legs_moving( return; } + void unit_preprocess_node_orientations( long unit_index, struct real_orientation *node_orientations) @@ -11288,9 +11314,11 @@ void unit_preprocess_node_orientations( unit_definition = unit_definition_get(unit->definition_index); animation_graph = animation_graph_definition_get( unit_definition->object.animation_graph.index); + /* port: every animation below is one the graph has and that fits the + model (unit_animation_get_fitting) */ + model_node_count = unit_animation_model_node_count(unit_index); unit_grenade_throw_keep_legs_moving(unit_index, node_orientations); - if (unit->unit.animation.action_animation.index != NONE && (animation = unit_animation_get_fitting(animation_graph, unit->unit.animation.action_animation.index, model_node_count)) != NULL) From 5fb7737797be17c109b49516a780d32218fe24c0 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:23:29 +1300 Subject: [PATCH 105/187] Screenshot key desktop only, without its test (#109) The Android guest has none of the SDL calls the PNG takes (SDL_CreateDirectory, SDL_SavePNG...), so its build did not link: the screenshot is written on the desktop builds only, and the key does nothing on Android. Its test, which CI did not run and which no longer built against main's headers, is dropped. --- port/linux/README.md | 2 +- port/linux/src/d3d8_gl.c | 18 ++- tools/test_screenshot.py | 257 --------------------------------------- 3 files changed, 8 insertions(+), 269 deletions(-) delete mode 100644 tools/test_screenshot.py diff --git a/port/linux/README.md b/port/linux/README.md index eb17ae27..95b728a2 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -215,7 +215,7 @@ gamepads' only. Always: \` opens the developer console, F12 releases or captures the mouse, F11 changes between fullscreen and window. Screenshot (default F10, -rebindable under Controls Setup > Actions, below Pause Menu) saves a PNG of the completed +rebindable under Controls Setup > Actions, below Pause Menu; not on Android) saves a PNG of the completed frame to `screenshots/` beside `maps/`, named `YYYY-MM-DD_HH.MM.SS.png` in local time, and prints the filename in the console. Captures in the same second get a numeric suffix so previous screenshots are preserved. diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 68f786cb..fc012949 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -4715,6 +4715,9 @@ void halo_screen_anti_alias(short x0, short y0, short x1, short y1) /* ---------- presentation */ +#ifndef HALO_ANDROID +/* the screenshot key's PNG (controls.screenshot; the Android guest has none +of the SDL calls it takes) */ static void write_key_screenshot(struct render_target_entry *target) { char directory[512], path[640], timestamp[32], filename[64]; @@ -4756,11 +4759,7 @@ static void write_key_screenshot(struct render_target_entry *target) SDL_SetError("Invalid screenshot dimensions"); goto failed; } -#ifdef HALO_ANDROID - surface = SDL_CreateSurface((int)width, (int)height, SDL_PIXELFORMAT_RGBA32); -#else surface = SDL_CreateSurface((int)width, (int)height, SDL_PIXELFORMAT_BGRA32); -#endif if (!surface) goto failed; @@ -4779,13 +4778,7 @@ static void write_key_screenshot(struct render_target_entry *target) glPixelStorei(GL_PACK_ROW_LENGTH, surface->pitch / 4); glPixelStorei(GL_PACK_SKIP_ROWS, 0); glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - glReadPixels(0, 0, (GLsizei)width, (GLsizei)height, -#ifdef HALO_ANDROID - GL_RGBA, -#else - GL_BGRA, -#endif - GL_UNSIGNED_BYTE, surface->pixels); + glReadPixels(0, 0, (GLsizei)width, (GLsizei)height, GL_BGRA, GL_UNSIGNED_BYTE, surface->pixels); error = glGetError(); glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw_framebuffer); @@ -4819,6 +4812,7 @@ failed: SDL_DestroySurface(surface); } +#endif static void write_screenshot(struct render_target_entry *target) { const char *directory = *config_string("debug.screenshot_directory") ? @@ -4919,8 +4913,10 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat #endif if (screenshot_every > 0 && device.frame % (unsigned long)screenshot_every == 0) write_screenshot(back_buffer); +#ifndef HALO_ANDROID if (platform_screenshot_take_request()) write_key_screenshot(back_buffer); +#endif platform_video_drawable_size(&window_width, &window_height); /* letterbox to the back buffer's aspect ratio */ diff --git a/tools/test_screenshot.py b/tools/test_screenshot.py deleted file mode 100644 index c4b7d601..00000000 --- a/tools/test_screenshot.py +++ /dev/null @@ -1,257 +0,0 @@ -"""Check rebindable screenshot actions and real PNG readback in a hidden GL context.""" -import argparse -import os -import subprocess -import sys -import tempfile -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent - - -def block(source, marker): - start = source.index(marker) - end = source.index('{', start) + 1 - depth = 1 - while depth: - depth += (source[end] == '{') - (source[end] == '}') - end += 1 - return source[start:end] - - -PRELUDE = r''' -#include -#include -#include -#include -#include -#include -#include "gl.h" -typedef int BOOL; -#define TRUE 1 -#define FALSE 0 -#define CHECK(c) do { if (!(c)) { fprintf(stderr, "FAIL line %d: %s (%s)\n", __LINE__, #c, SDL_GetError()); exit(1); } } while (0) -#include "../include/halo_keyboard.h" -#undef INPUT_MOUSE -#include "sdl_platform.h" -#define GL_DEFINE(name) __typeof__(name) name; -GL_FUNCTIONS(GL_DEFINE) -#undef GL_DEFINE -struct render_target_entry { struct {unsigned long gl_width, gl_height; GLuint texture;} target; }; -static GLuint capture_framebuffer; -static char data_root[512], message[256]; -static BOOL fail_save, screenshot_requested; -static int input_lock, locks; -static Uint64 wheel_press_until_ms; -static int wheel_direction; -static unsigned long config_revision; -static const char *screenshot_binding = "F10", *fire_binding = "Mouse Left"; -static unsigned long config_changes(void) {return config_revision;} -static const char *config_string(const char *name) { - if (!strcmp(name, "controls.screenshot")) return screenshot_binding; - if (!strcmp(name, "controls.fire")) return fire_binding; - if (!strcmp(name, "controls.move_forward")) return "W"; - if (!strcmp(name, "controls.pause")) return "Escape"; - if (!strcmp(name, "controls.scoreboard")) return "Tab"; - return ""; -} -static void platform_log(const char *format, ...) {(void)format; CHECK(FALSE);} -typedef struct {unsigned short wButtons;} XINPUT_GAMEPAD; -#define XINPUT_GAMEPAD_START 0x10 -#define XINPUT_GAMEPAD_BACK 0x20 -enum {_binding_capture_waiting, _binding_capture_taken}; -static int binding_capture, binding_capture_result, binding_captured_input; -static Uint64 binding_taken_ms; -static void pthread_mutex_lock(int *lock) { (void)lock; CHECK(!locks); locks++; } -static void pthread_mutex_unlock(int *lock) { (void)lock; CHECK(locks == 1); locks--; } -static const char *platform_data_root(void) {return data_root;} -static GLuint framebuffer_get(GLuint color, GLuint depth) { - (void)color; (void)depth; - /* A cache miss binds both framebuffer targets in the real renderer. */ - glBindFramebuffer(GL_FRAMEBUFFER, capture_framebuffer); - return capture_framebuffer; -} -static void console_printf(unsigned char clear, const char *format, ...) { - va_list args; (void)clear; - va_start(args, format); vsnprintf(message, sizeof(message), format, args); va_end(args); -} -static bool fixed_current_time(SDL_Time *time) {*time = 0; return true;} -static bool fixed_date(SDL_Time time, SDL_DateTime *date, bool local) { - (void)time; CHECK(local); - *date = (SDL_DateTime){.year=2026, .month=8, .day=25, .hour=14, .minute=3, .second=27}; - return true; -} -static bool save_png(SDL_Surface *surface, const char *path) { - if (fail_save) return SDL_SetError("Simulated disk write failure"); - return SDL_SavePNG(surface, path); -} -#define SDL_GetCurrentTime fixed_current_time -#define SDL_TimeToDateTime fixed_date -#define SDL_SavePNG save_png -''' - -TESTS = r''' -static BOOL poll_screenshot(const struct platform_input_state *input) { - keyboard_screenshot(keyboard_bound_actions(input)); - return platform_screenshot_take_request(); -} -static void check_bindings(void) { - struct platform_input_state input = {0}; - XINPUT_GAMEPAD pad = {0}; - SDL_Event event = {0}; - input.keys[SDL_SCANCODE_F2] = 1; CHECK(!poll_screenshot(&input)); - input.keys[SDL_SCANCODE_F10] = 1; CHECK(poll_screenshot(&input)); - CHECK(!poll_screenshot(&input) && !platform_screenshot_take_request() && !locks); - input.keys[SDL_SCANCODE_F10] = 0; CHECK(!poll_screenshot(&input)); - input.keys[SDL_SCANCODE_F10] = 1; CHECK(poll_screenshot(&input)); - /* Rebinding removes the old default; the two slots use the normal parser. */ - screenshot_binding = "F2, Mouse 4"; config_revision++; - input.keys[SDL_SCANCODE_F2] = 0; CHECK(!poll_screenshot(&input)); - input.keys[SDL_SCANCODE_F2] = 1; CHECK(poll_screenshot(&input)); - input.keys[SDL_SCANCODE_F2] = 0; CHECK(!poll_screenshot(&input)); - input.mouse_buttons[SDL_BUTTON_X1] = 1; CHECK(poll_screenshot(&input)); - CHECK(!poll_screenshot(&input)); - input.mouse_buttons[SDL_BUTTON_X1] = 0; CHECK(!poll_screenshot(&input)); - /* The default is free for other actions. No hidden screenshot protection. */ - fire_binding = "F10"; config_revision++; - CHECK(keyboard_bound_actions(&input) & (1UL << HALO_KEYBOARD_FIRE)); - CHECK(!poll_screenshot(&input)); - screenshot_binding = ""; config_revision++; - input.keys[SDL_SCANCODE_F2] = 1; CHECK(!poll_screenshot(&input)); - screenshot_binding = "Wheel Down"; config_revision++; - wheel_direction = -1; wheel_press_until_ms = SDL_GetTicks() + 1000; - CHECK(poll_screenshot(&input)); CHECK(!poll_screenshot(&input)); - wheel_press_until_ms = 0; CHECK(!poll_screenshot(&input)); - screenshot_binding = "F10"; config_revision++; - input.menus = TRUE; CHECK(poll_screenshot(&input)); - input.keys[SDL_SCANCODE_ESCAPE] = input.keys[SDL_SCANCODE_TAB] = input.keys[SDL_SCANCODE_W] = 1; - unsigned long held = keyboard_bound_actions(&input); - keyboard_controls(held, &pad); - CHECK(pad.wButtons == (XINPUT_GAMEPAD_START | XINPUT_GAMEPAD_BACK)); - CHECK(keyboard_actions_held == held && (held & (1UL << HALO_KEYBOARD_MOVE_FORWARD))); - keyboard_screenshot(0); CHECK(!platform_screenshot_take_request()); - /* Normal binding capture accepts both F2 and F10, with no capture fired. */ - event.key.down = true; - for (int i = 0; i < 2; i++) { - binding_capture = _binding_capture_waiting; - event.key.scancode = i ? SDL_SCANCODE_F10 : SDL_SCANCODE_F2; - capture_key(event); - CHECK(binding_capture == _binding_capture_taken && binding_capture_result == 1); - CHECK(binding_captured_input == event.key.scancode && !platform_screenshot_take_request()); - } - binding_capture = _binding_capture_waiting; - event.key.repeat = true; capture_key(event); - CHECK(binding_capture == _binding_capture_waiting); -} -static void check_state(GLuint pack_buffer) { - GLint value; - glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &value); CHECK(value == 0); - glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &value); CHECK(value == 0); - glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &value); CHECK(value == (GLint)pack_buffer); - glGetIntegerv(GL_PACK_ALIGNMENT, &value); CHECK(value == 8); - glGetIntegerv(GL_PACK_ROW_LENGTH, &value); CHECK(value == 8); - glGetIntegerv(GL_PACK_SKIP_ROWS, &value); CHECK(value == 1); - glGetIntegerv(GL_PACK_SKIP_PIXELS, &value); CHECK(value == 2); - CHECK(glGetError() == GL_NO_ERROR); -} -static void check_png(const char *filename, const Uint8 *colors) { - char path[640]; Uint8 r, g, b, a; - snprintf(path, sizeof(path), "%s/screenshots/%s", data_root, filename); - SDL_Surface *image = SDL_LoadPNG(path); - CHECK(image && image->w == 3 && image->h == 2); - for (int y = 0; y < 2; y++) for (int x = 0; x < 3; x++) { - const Uint8 *expected = colors + (y * 3 + x) * 4; - CHECK(SDL_ReadSurfacePixel(image, x, y, &r, &g, &b, &a)); - CHECK(r == expected[0] && g == expected[1] && b == expected[2] && a == 255); - } - SDL_DestroySurface(image); -} -int main(int argc, char **argv) { - CHECK(argc == 2); snprintf(data_root, sizeof(data_root), "%s", argv[1]); - check_bindings(); - - CHECK(SDL_Init(SDL_INIT_VIDEO)); - CHECK(SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4)); - CHECK(SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5)); - CHECK(SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE)); - SDL_Window *window = SDL_CreateWindow("Screenshot regression", 32, 32, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); - CHECK(window); - SDL_GLContext context = SDL_GL_CreateContext(window); CHECK(context); -#define HALO_TEST_LOAD_GL(name) name = (__typeof__(name))SDL_GL_GetProcAddress(#name); CHECK(name); - GL_FUNCTIONS(HALO_TEST_LOAD_GL) -#undef HALO_TEST_LOAD_GL - struct render_target_entry target = {.target = {.gl_width = 3, .gl_height = 2}}; - const Uint8 colors[24] = {255,0,0,1, 0,255,0,2, 0,0,255,3, 255,255,255,4, 13,29,47,5, 127,63,31,6}; - glGenTextures(1, &target.target.texture); glBindTexture(GL_TEXTURE_2D, target.target.texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 3, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, colors); - glGenFramebuffers(1, &capture_framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, capture_framebuffer); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.target.texture, 0); - CHECK(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - GLuint pack_buffer; glGenBuffers(1, &pack_buffer); glBindBuffer(GL_PIXEL_PACK_BUFFER, pack_buffer); - glBufferData(GL_PIXEL_PACK_BUFFER, 32, NULL, GL_STREAM_READ); - glPixelStorei(GL_PACK_ALIGNMENT, 8); glPixelStorei(GL_PACK_ROW_LENGTH, 8); - glPixelStorei(GL_PACK_SKIP_ROWS, 1); glPixelStorei(GL_PACK_SKIP_PIXELS, 2); - CHECK(glGetError() == GL_NO_ERROR); - write_key_screenshot(&target); - CHECK(!strcmp(message, "Saved screenshot as 2026-08-25_14.03.27.png")); - check_state(pack_buffer); check_png("2026-08-25_14.03.27.png", colors); - write_key_screenshot(&target); - CHECK(!strcmp(message, "Saved screenshot as 2026-08-25_14.03.27_2.png")); - check_state(pack_buffer); check_png("2026-08-25_14.03.27_2.png", colors); - check_png("2026-08-25_14.03.27.png", colors); - fail_save = TRUE; write_key_screenshot(&target); - CHECK(!strncmp(message, "Screenshot failed:", 18)); check_state(pack_buffer); - fail_save = FALSE; - target.target.gl_width = 0; write_key_screenshot(&target); - CHECK(!strncmp(message, "Screenshot failed:", 18)); check_state(pack_buffer); - glDeleteBuffers(1, &pack_buffer); glDeleteFramebuffers(1, &capture_framebuffer); - glDeleteTextures(1, &target.target.texture); - SDL_GL_DestroyContext(context); SDL_DestroyWindow(window); SDL_Quit(); - puts("PASS: F10 default; normal rebind/clear/two slots/mouse/wheel; F2/F10 binding capture; gameplay unchanged; real GL-to-PNG pixels, orientation/opaque alpha; timestamps/collisions; GL state and failures"); - return 0; -} -''' - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--cc', default='clang') - args = parser.parse_args() - render = (ROOT / 'port/linux/src/d3d8_gl.c').read_text() - events = (ROOT / 'port/linux/src/sdl_platform.c').read_text() - keyboard = (ROOT / 'port/linux/src/xinput_sdl.c').read_text() - menu = (ROOT / 'port/linux/game/menu_functions.c').read_text() - config = (ROOT / 'port/linux/src/port_config.c').read_text() - assert '{ "controls.pause", L"PAUSE MENU", 2 },\n\t{ "controls.screenshot", L"SCREENSHOT", 2 },' in menu - assert r'"controls.screenshot", _config_string, "\"F10\""' in config - assert 'SDL_SCANCODE_F2' not in events and 'SDL_SCANCODE_F10' not in events - unit = (PRELUDE + block(events, 'void platform_screenshot_request(void)') + - block(events, 'BOOL platform_screenshot_take_request(void)') + - '\n' + keyboard[keyboard.index('#define MAXIMUM_BINDINGS'):keyboard.index('unsigned long halo_keyboard_actions(')] + - '\nstatic void capture_key(SDL_Event event) { do {\n' + - block(events, 'if (binding_capture == _binding_capture_waiting && event.key.down') + '\n} while (0); }\n' + - block(render, 'static void write_key_screenshot(') + TESTS) - compiler = [args.cc, '-std=gnu11', '-O2', '-fuse-ld=lld', '-I' + str(ROOT / 'port/linux/src')] - env = dict(os.environ) - if sys.platform == 'win32': - sdl = next((ROOT / 'build/windows/third_party').glob('SDL3-*/include')).parent - compiler += ['--target=i686-pc-windows-msvc', '-I' + str(sdl / 'include')] - libraries = [str(sdl / 'lib/x86/SDL3.lib')] - env['PATH'] = str(sdl / 'lib/x86') + os.pathsep + env['PATH'] - else: - compiler += subprocess.check_output(['pkg-config', '--cflags', 'sdl3'], text=True).split() - libraries = subprocess.check_output(['pkg-config', '--libs', 'sdl3'], text=True).split() - with tempfile.TemporaryDirectory(prefix='halo-screenshot-test-') as directory: - path = Path(directory) - (path / 'screenshot.c').write_text(unit) - subprocess.run([*compiler, str(path / 'screenshot.c'), *libraries, '-o', str(path / 'screenshot.exe')], check=True) - subprocess.run([str(path / 'screenshot.exe'), str(path)], env=env, check=True, timeout=30) - files = sorted((path / 'screenshots').glob('*.png')) - assert len(files) == 2 - for file in files: - assert file.read_bytes().startswith(b'\x89PNG\r\n\x1a\n') - - -if __name__ == '__main__': - main() From c92b9defc26e32e9e94a2877b8cc0f1f284cbf8d Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:24:30 +1300 Subject: [PATCH 106/187] The secondary target at the screen's scale, on main's targets (#83) The 320x240 colour target the reflections and active camouflage draw into gets the screen's pixels a unit, as the screen-sized targets do; with resolution_scaling "original" and on Android it is as before. The PR's depth texture of the colour's size is left out: GL draws into attachments of different sizes (their intersection), main already pairs this target with the screen's depth, and it would have bypassed bind_targets' multisampling. --- port/linux/src/d3d8_gl.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index fc012949..2c708905 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -1150,6 +1150,10 @@ static BOOL surface_is_shadow_map(const D3DSurface *surface) description.height == SHADOW_MAP_SIZE; } +/* the secondary render target, the size of the Xbox's (rasterizer_xbox.c) */ +#define SECONDARY_TARGET_WIDTH 320 +#define SECONDARY_TARGET_HEIGHT 240 + /* the targets render_target_get found last, by what it found them from: each draw asks again for the same two (bind_targets) */ #define RECENT_RENDER_TARGET_COUNT 4 @@ -1202,9 +1206,13 @@ static struct render_target_entry *render_target_get(const D3DSurface *surface) } } surface_dimensions(surface, &width, &height, &depth); - /* the screen's targets are drawn at the screen's scale, and the shadow - maps at display.shadow_resolution's (halo_shadow_map_scale) */ - if (width == (unsigned long)halo_screen_width() && height == SCREEN_HEIGHT) + /* the screen's targets are drawn at the screen's scale, and so is the + secondary target (the reflections' and active camouflage's: its size is + rasterizer_xbox.c's), which keeps the game's 320x240 units, that its + shaders' constants are in, and gets the screen's pixels a unit; the + shadow maps at display.shadow_resolution's (halo_shadow_map_scale) */ + if ((width == (unsigned long)halo_screen_width() && height == SCREEN_HEIGHT) || + (!depth && width == SECONDARY_TARGET_WIDTH && height == SECONDARY_TARGET_HEIGHT)) { scale[0] = screen_scale[0]; scale[1] = screen_scale[1]; From 4505a43e536cc74d9cbd75c1288c40865561850f Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:34:33 +1300 Subject: [PATCH 107/187] Overshield links' comments and lines as the files', part 0 checked (#116) render_model_parts now follows a link whose previous part is 0, which retail did not: of the 10862 parts of the 24 stock maps' models none has one (the needler's 2 and 3 are their only links), so it changes nothing there. The comments say what the links are for, and the long conditions are wrapped. --- source/models/models.c | 22 ++++++++++++------- .../rasterizer_transparent_enclosure.h | 5 +++-- .../rasterizer_xbox_transparent_geometry.c | 9 +++++--- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/source/models/models.c b/source/models/models.c index cb06148d..62956635 100644 --- a/source/models/models.c +++ b/source/models/models.c @@ -388,8 +388,10 @@ static void render_model_parts( if (sort_filth_countnext_part_index>0 || part->previous_part_index>=0)) + /* port: part zero can head a link + (models_fix_transparent_part_links); no stock + model's part has a previous part of 0 */ + (part->next_part_index>0 || part->previous_part_index>=0)) { sort_filth[sort_filth_count].part_index = part_index; sort_filth[sort_filth_count].next_part_index = part->next_part_index; @@ -462,8 +464,11 @@ static void render_model_parts( /* ---------- public code */ -/* port: use native energy-before-glass links, without changing glass passes. - Keep authored links and ambiguous/skinned models as they are. */ +/* port: a pickup's energy inside its two-sided glass shell is drawn before +the shell with the engine's own part links (render_model_parts), as Halo CE +Restored's tags link them, so that the centroid sort cannot put it after the +glass; models with links of their own, skinned ones and those with other +transparent parts are left as they are */ static void model_geometry_fix_transparent_part_links( struct model const *model, struct model_geometry *geometry) { @@ -479,6 +484,7 @@ static void model_geometry_fix_transparent_part_links( { struct shader *shader; struct model_shader_reference const *reference; + if (parts[i].flags || parts[i].previous_part_index != NONE || parts[i].next_part_index != NONE || !VALID_INDEX(parts[i].shader_index, model->shaders.count)) { @@ -506,8 +512,8 @@ static void model_geometry_fix_transparent_part_links( shader_definition_get(TAG_BLOCK_GET_ELEMENT(&model->shaders, parts[energy].shader_index, struct model_shader_reference)->shader.index), &parts[energy].vertex_buffer)) { - /* A native link cannot target part zero. These parts have no authored - links; swap their complete descriptors only when glass is part zero. */ + /* (a link to part zero is none: with the glass part zero, the two + parts, which have no links of their own, change places) */ if (glass == 0) { struct model_geometry_part swap = parts[glass]; @@ -521,8 +527,8 @@ static void model_geometry_fix_transparent_part_links( } } -/* port: the meshes do not change between frames; their links need no cache - invalidation beyond loading fresh tags for the next map. */ +/* port: the links of every model the map loaded (at its start: the meshes +do not change, and the next map's tags are loaded afresh) */ void models_fix_transparent_part_links(void) { struct tag_iterator iterator; diff --git a/source/rasterizer/rasterizer_transparent_enclosure.h b/source/rasterizer/rasterizer_transparent_enclosure.h index 93ec0c1c..3fa60608 100644 --- a/source/rasterizer/rasterizer_transparent_enclosure.h +++ b/source/rasterizer/rasterizer_transparent_enclosure.h @@ -1,5 +1,6 @@ -/* port: recognize a closed convex shell once when model tags load. Positions - are the first three floats in both supported model vertex formats. */ +/* port: whether a closed convex shell holds every vertex of another part +(models_fix_transparent_part_links, once as a map's tags load); a position is +the first three reals of both model vertex formats */ #ifndef __RASTERIZER_TRANSPARENT_ENCLOSURE_H #define __RASTERIZER_TRANSPARENT_ENCLOSURE_H diff --git a/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c b/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c index dfb258ae..afc971bc 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c +++ b/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c @@ -895,9 +895,12 @@ boolean rasterizer_transparent_geometry_is_enclosure( core->extra_layers.count || core->lens_flare.index != NONE || !outer || !inner || !triangles || !outer->hardware_format || !inner->hardware_format || !triangles->hardware_format || outer->offset || inner->offset || - (outer->type != _rasterizer_vertex_type_model_compressed && outer->type != _rasterizer_vertex_type_model_uncompressed) || - (inner->type != _rasterizer_vertex_type_model_compressed && inner->type != _rasterizer_vertex_type_model_uncompressed) || - (triangles->type != _triangle_buffer_type_triangles && triangles->type != _triangle_buffer_type_precompiled_strip)) + (outer->type != _rasterizer_vertex_type_model_compressed && + outer->type != _rasterizer_vertex_type_model_uncompressed) || + (inner->type != _rasterizer_vertex_type_model_compressed && + inner->type != _rasterizer_vertex_type_model_uncompressed) || + (triangles->type != _triangle_buffer_type_triangles && + triangles->type != _triangle_buffer_type_precompiled_strip)) { return FALSE; } From f479e34914604df5a22a2bdb0b38f180a1f702d8 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:13:07 +1300 Subject: [PATCH 108/187] OpenCE icon for Android, Windows and the desktop window ORION's icon (port/assets/icon, credited in AUTHORS.md) is the Android launcher's adaptive icon (the render as its foreground, its background's gradient behind it, the armour as its themed layer), halo.exe's icon (port/windows/halo.rc, compiled by llvm-rc) and the window's icon on the desktop builds (SDL_SetWindowIcon). tools/app_icon.py makes them all, in place of tools/android_icon.py and its emblem. --- AUTHORS.md | 5 + .../res/drawable/ic_launcher_background.xml | 5 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 +- .../mipmap-hdpi/ic_launcher_foreground.png | Bin 3930 -> 11416 bytes .../mipmap-hdpi/ic_launcher_monochrome.png | Bin 0 -> 7787 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 2511 -> 6828 bytes .../mipmap-mdpi/ic_launcher_monochrome.png | Bin 0 -> 4591 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 5435 -> 16243 bytes .../mipmap-xhdpi/ic_launcher_monochrome.png | Bin 0 -> 11424 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 8540 -> 26818 bytes .../mipmap-xxhdpi/ic_launcher_monochrome.png | Bin 0 -> 19722 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 11828 -> 38696 bytes .../mipmap-xxxhdpi/ic_launcher_monochrome.png | Bin 0 -> 29160 bytes .../res/values/ic_launcher_background.xml | 5 - port/android/art/android-icon.png | Bin 5955 -> 0 bytes port/assets/icon/opence-icon-256.png | Bin 0 -> 32492 bytes port/assets/icon/opence-icon-render.png | Bin 0 -> 234469 bytes port/assets/icon/opence-icon.png | Bin 0 -> 420637 bytes port/linux/src/sdl_platform.c | 13 ++ port/windows/halo.rc | 4 + port/windows/opence-icon.ico | Bin 0 -> 67233 bytes tools/android_icon.py | 91 ------------ tools/app_icon.py | 135 ++++++++++++++++++ tools/embed_assets.py | 12 +- tools/windows_build.py | 21 ++- 25 files changed, 196 insertions(+), 101 deletions(-) create mode 100644 port/android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 port/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png create mode 100644 port/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png create mode 100644 port/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png create mode 100644 port/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png create mode 100644 port/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png delete mode 100644 port/android/app/src/main/res/values/ic_launcher_background.xml delete mode 100644 port/android/art/android-icon.png create mode 100644 port/assets/icon/opence-icon-256.png create mode 100644 port/assets/icon/opence-icon-render.png create mode 100644 port/assets/icon/opence-icon.png create mode 100644 port/windows/halo.rc create mode 100644 port/windows/opence-icon.ico delete mode 100644 tools/android_icon.py create mode 100644 tools/app_icon.py diff --git a/AUTHORS.md b/AUTHORS.md index 2e71d80b..913dd699 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,5 +1,10 @@ # Authors +## Art + +- **The game's icon** (the Android app's launcher icon, `halo.exe`'s icon + and the desktop windows' icon): by ORION (`port/assets/icon`). + ## Third-party art - **Voice chat's speaker icons** (who talks, and who is muted, in the lobby, diff --git a/port/android/app/src/main/res/drawable/ic_launcher_background.xml b/port/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..d8c8ef1b --- /dev/null +++ b/port/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/port/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/port/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index f7040662..a2d856da 100644 --- a/port/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/port/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,7 +1,7 @@ - + - + - + diff --git a/port/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/port/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index 3e6c189265f74542609115911374567a95396678..16891c4ad419287f0839f984efacdba71857aa87 100644 GIT binary patch literal 11416 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 literal 3930 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 diff --git a/port/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/port/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..e37bfeda81e30f68005c990859513343426194a3 GIT binary patch literal 7787 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 literal 0 HcmV?d00001 diff --git a/port/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/port/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index 3142f10aa30152dad249b47aa7060b90e3eda278..a4639eca4094035990a8102b7aa50d10e8daefe3 100644 GIT binary patch literal 6828 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 delta 2502 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 Z{{@ao&9X{u!!ZB=002ovPDHLkV1hkY;Y|Pl literal 0 HcmV?d00001 diff --git a/port/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/port/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index bb85fa55d6095856d58d160bec775f2c5c4dab8b..92c0bbf34834ac2645bcc38c68e70c52d028806b 100644 GIT binary patch literal 16243 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 NJXe09RIPvr{U1P6$Xx&c literal 5435 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 literal 0 HcmV?d00001 diff --git a/port/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/port/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index eee258b73281a003835c2727e688386c25c13430..7df51a167532f911b6daaff1558d4065b0fb061d 100644 GIT binary patch literal 26818 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

    kfR}r3?%Si$=8|Po5~mQ$GfGq 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 literal 8540 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 LzGl@U`#1jwbth(i diff --git a/port/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/port/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..553205d7b6ddb15cadded3f8ac9fe44130bf616b GIT binary patch literal 19722 zcmdRUu`*c{u^y?N>qmQ6sQ&4%o+t||kUM$4)dgCL`Y zkiHqcnT;;LXv=q-n>XKyBCSW6fxN-f;_QC!35H7TV*9Q5s1r#Du_6a>X8-?wBv67r zTVWOD$VdJE;IT==(+`JWCk!g~asBGIf)p&c9;`|0uC-OjeL_I<>_Jnxs8=&u4ej4o zXOkNo;@Xv;V$;i(Oa!T!X-Dg^Ox2{KW&BAI^l-GEhCy3Y|}fqZ|2Z-%Rww8K8eMoC?K3W6PciQ--LU z0@(pR3$W3>`M<9f=tKsEJD@dU6qA}OOMiQa-06PI?|$v7e6o1YjBtlDOm}+Zsk-sW z3%M#0%8&%mAb{}V*~u<7sks1YTGT7aO$dBMNHoAalWIyj|~}O}})bd>SSvPXmWhZI_J@=LrgTm}K12!h`!)i@=r?00*|V=kIDl z`BaU2aRM+Au5dV!c4UEY_2yAwP+r>heW`7%)ma>0!^^|CaZ;J^+v|R^(wx84iNj|; zB|nngNz5U9P8y)I)55#(w9@CL{NhLe;+QajOJ{g^9H}$E>Fjnr+n6F;32xiZ0^0T4 zUt>^;e|F3v`=Cgq>ii8x2qBUJexgE%1^xkT+JN}=S0VF%<%QJQaYw_` zKjJWgy^K~NO)#L*XG3iv#LQXpdlZC^*9p&2np_Q+?lhF zkZp2t_IyeHVd@dkL1t{2eu7F&nMPL#=Jv~d_RQ+rsOHe7+VJ%KYT$kK!KZMXQ>l~~ z$|gJr;}S(D&4+$Qhy$##KuzfRxMcxAVC7HZOsN+Fmv}6L%~eB`p17VFHABL|4P_OV zJ)qrp`)l9-XIDMTppdq=$8C7}4{vVG0{*#xOHZ|U7OrSW~bp5{NTT;-+j^1}(&5tHkQdMcjU(*7ec~6@=mpeu|Qnw2DOvgb_N|`u1 z`97$o^fFEIg&0r+FLTgi^%+Km*A=gVI6TZS85_zStHlJ**!k6S@(4B@u4&-Svo;ih z3G7NyUY*;-VXvz?=^vTC-F&zeyzkZQ6ZF^4hIvk}+x+*DB`M}FMPO;bKPQOtK5w>) zbee*O>|wqNAVdzgdCY3s`o+jKlpLLzGHy@`6*4dGKczCOZa);@u08WYmTZ_$2I!;a znuBIJ*#1vGF7*I8MuuTJd$$AIx=Dp0L4bdtHP1yWyji~d&NZ~&Xsug~@8UxNGN-6;>^?e{MOunaYG z4|at$qHUtM&mM>R%R7oUlW-4hPEM-s? z65-gss2egyJ>c40Dh_yQRW!lIk3cm<&f=!Vk!POm$bi=o@+GemHv3OegLsc!iD zY{eJBN}zmxug#)87JV@{EIZWub~QyV7%p}?NG)9RsU~Ku8XD11K zy3wb0CziAN}%L^em&<(?yFJoEThBsEX zM=<<+t4pAu(@Z0(iW7pnp^RiFNvlijG~bVuG5FI=b(VD{1MACn1miBzhB;rH$lZtqO-slxb}G3fO|P7HTlvQ z&^aOQUyxRsHx-k#_XkxtM@tkOsBknzh-9nJK~dHx^aGa0#Z^wLGb(acoF7)JB^5}h zOl&s)qgVb&6~-jKFJm3$h&S6bYu2kh;mMwJ^y##{VNkRKnVp~C71lHB!pU!4>&`K> z=4xicTiMqx0L$Uh5vbX$R+BOXO@pV+UH;33qaTaB*x-Lzl1pZkB7d3hEs^B;z6McU zEWUeH#M!Q+LJh#Nhyjcf`iqf$9WINxkrbwm;*j|jtEdi^XFU5@-}{IYfchY@R21{E zsuvYvXpUVP@5yYdB&ri=5VaJ=W3JTGFz2yGqJ%em+V`zDHoah7!jnX76OBg+Dp^FJ z82aP_&pMeJ_kQu^9OWFVVXn_#dDuulw^BpJU2v~;?y{u@6d)l31=m0J)Ezr$Sh7&W z^BKfi_x%dPjvrr`?%es$_mas3pFtkD0o({PKX(JQ7v+tpMn7*#{w`6!Op7_<+n=M0 z;V>`X_+@~*K^#Dhc=2$T>VEkf`CF{c8qP5*w}ldF+39aQEn#r zpEhV&P2a+m=&cg!==XO+@4JN`I+bvF&!l6MO^Ee=lJlb(rs{>P7J)I;!_M8l~&uKTG#(7 z%(0Z9m+~=TOG_eEwu6MWi@K!Kx}ad{w6pHE2w+S~(^w@)-f->8~@}W-vCQUO%nAm8FsH(AS&`?$TJ? zqYEp3Vv~Y>-n8XP@h$s6J|mlX?DBAWm7hBJ2OqjZ$}P}9L9 zS#QEO(uZ+Z_uLQf3@P{bqhseVX&LLG^rm^S31-+A;CdBY28@k2edpHG^Gps6@U~Ro z2N_A zu=M^_<^Q=m#OC>Ft=!aXU7eX*QBOKztQ2DRgx}|X5i&TYXumC`ygTIOp^S&1?gb3X za?$WQj5k5aOBM5yNGkuHJ-#xZ$5&s>>?V01Alpl^fQnVt(a9KmVZ$xJ=EALz$a$e7 zm&mIV;s$wjo-v;ZTO7U)(Db24{zL7J-EP08e+92Kd};fRs^1;IikeyTQjrLuaG!eB zS_vgVZ#IiO|KhFdlf3H6WBE2T7IGJ4^vBWQzH#b`@Cd;JZzd3VpIcc_@pA0m8KV1_ zwI*@MHCq~R(V0H8QZJfJmYw1lC1&sgH}KV~?mi%J*a>)YbFhtf^m*uGAMqc+ehuAo zC;fU+G&PE35usr~#Rau8T(E*98eNTEwf8F*f|WtLJn#`U&*}&smPJ<95wb66&qf0V zuI?!jnVihSP1Jp85TJ1?{QglVDNsF#G#kJ~80SW@iFKL={zzoI=~9Q)*uTgd zO1(M1#fOyeU*GgKh}Fp7ih&IGAde`XF?W7Dof&HN6JHQRSEk0^Sbf8(zm%7$`P;6q z0*RG#e!Ko}DO0L{bu~jIFAhO31Gswwhm^B06iWZ0uAdLlCQ8U}h>uO$q}u10-R_sp9WjOhQ{A}lhZ43EBdvm(ret1_66?hjR9ow z@p-tB`-?7VmAZif|Wxsf6kmHy#0YU(bFza3HtF09^7OVWf^w577TwqbjvUE z;hi${6z{U_V<30KoBqXZ@Wx+sSzYf(O};oi(zT0Xsyl4P#^Msi$q6@rb*oqvcg>gCAAM}ru!1YG&4gPA)1f!_O3Z=N?#^pf8O2VDhY zu?!!|lNxujb#}x)b})Y*N38ficS_0$jd)R1>Nc!{r*)yeUtfhrq=b0E?d221*VUJ=B;#ni!s!|!k&X-*iiQ@N()>0`)v`KI z@-(i0{alY-u?T$rb}#34d&p-eQ#uA6mxU{9kJ=k@ODM0uAE0RcLCOV$2~fNQg#T3= zw^0n;Xr@`9omR|JUF$bb6U=#rVQ?LA_$+nP6ouVE-SZh2e7Hv9qoEZ}W=7s}v6NaV zKGVTMXTj{i1l-xb_OAw)G9l@0#XnQ%wuE`V2KtD0N{aVcpU7rL5d^VDEeQp)F_!yE ztuV1DvJB^q8BpxQF%Uk0f7#%(7`|iicdn#8e!SpWTGEFvfN-w$B{L%XAa-4-;(OBA zD8J<>c>=au9f*UQ9+XUJl4xLeG5xXoi`qby8l)&6&QCMyJ6uFdHqR%H0o!en+0HDW zj0Ehyf%cH%+te!ozN+~0qQm^B*FXJ=1U$ROjAYxFbxaD0dydA7VNq(o$BR|;ml^)U z$0Z+mx6~ZwdJ*@yCetX* zlHV?gZ1u$1h7cSqEyA~tBbx6S#{^sbQFFxiLmYp9P=bQd>z%z;1dd_|m`4-1rnq~l z5TdtFBjaWX-@p9LOveU#$3V?q0*Oh|z@Xjt029117Phy-%9S+D#G=9(6S?19!bt?# z+&fSU({!Smao0zcvt-@49gnrXSi87ONnc}2iKy&s-JLPr@eZ~o6siit`<{!3$7mw_Lh0L zpI>q#wP>aLkA&2ulXH#|%aPX!wJ`r9J2q-EZVq2W97-^0>q)Q;E1LW0?OS?J*T;>= zhEe^1J+|X-euQ5+!kAcrIa>v~Q5Cg%wv5go*}v-*_TC+FnDU7eO-9~BOP}zXCr5E( zsxUoS5OzJCxz(<0muuTcq?l-Q#}<*!5Eg`|`TMSAYO6d$ssX@#VNO$M4^&9A%6pAa z3N;`NDy0VU!xShaeX@Bm?1xK()n~08Bg;Ijzo6I<-g@CXp;#(p3;w6~uF}0%fn~Te z>Sa_ekw?n+MxeN8*SssyC(}uSJyuO~E8r29k8m%Gmg74*;!fhIQ2DrvN){WlWT=;9 zJa%lB z!CI+FBd98_>78wtvPBF!bX6P2I{9#(| zkK-g_>G#Z#QE;xhkWzmkYTp6_Zdju_-o~~3h|d1I;8IH)_?2`r^dszp;Ahd#5im^N#FA zJq8!@rQ)SMIec^d64UuF*e_FTio!WN0X2s!T2fhT?0oZ7Sq4KSN0(<+~Y{UIAZ^VfUVCr@OH4^RWh9 zu?2hrpMB7aeGV4FEt@Af?~_PfF;*QW-+c{4@8=unc(6~^oPLJQ?ptXhhzi9ov>|MO z*{%iB2@n_$O_XF8yrvsn;MnrQur&Oq3n@OhzS1i*i@r_rG8f`}CWAX{Np2LSrbfMc z^n5NDMi%dWXjzpdx7W6mH# z;>3P_E53Lv82NQKwRI_3rJ2YDi85>cMdCTCcwQgPa0|X~Cq5@L)62L`# zl$^J=K<`nW4zcISL&nSDS_9Ty;CJiUwd#b5>ScQNJ4RGStov^S>D!CpuX9pMtKvS4 zi~BG5`D7#~?@SxypoOCN`{OuXjMU>dWyesJAiRqH5pxgs zFK6E`$$33OJl1;gonTuc=SUROI=_Dy%FuZJTzqC2e^t zZIu)~e2nHAJ$S={To#i40g_1A)nZFbCC2AozkQi^`x`MJDo54*_D93Z!_w(hr9>M@a6eC?n&!zTzy=I#RyP$Dx zLi!deBuYH@dpjIVUR$I6fq;$lEht|3LPHhC4LqO4!q8jSVFiwO99h-C64a-W``M;+ zU){PcoOImzv;1d?e_@ci9siRd+pODJeV6Q18qWNeg^s-?8p_=VGqC)wfZPAG@%#pP@{<`HsW+_)<#;w1F?BXK?jR#A787 zfV!uYL0Ual_F+!i{QHgp)+Oa)87!RqMyWiYGHsiZ6o{x0-yDkwp<|0vpGvl<3&n(B zcEmypPYSz&&8V#8V?wdId4YAAYlAitZ?lGV%;}Z&S&K{deBymsp}w~o_6Y?1Sh`YF zn-a$l)16r~z!5sd-SaT27LDF>ZMSmK`FXSH^|grOZA0l4ha+(Uw_#n~#P9od@GYQs z=kt7&Wu#t?+eGT}Z|bhR?`#sB4c>>gsVnG<%am0VC2;S;I`rcUSNq2ul}pYmhdTP9 z{g)es{1gg1^AzTw>O*xU&SGt{`(^8|>ja;6*HF97XUrjytE5jQj~{Aku00uiIZ}AB z{qH zhO|E$pWOxk0`YsL^>c2O8m#Z8OJrMoD9#i66x**+90`QMr21W^Y`z3!ZDv zkbZH0A4%_9r5B3(g_3n+wq4)W&*b`gj11ne60Srq(*G)0tG+e<0baAW#nW;W{Ywq# z)SJoR9Kq!tE{d`mEiA9F9e@wF2d8;>y<8wqv&IfE0>u=?tj6BnQnF--`Sf=hgffbS2eyDh*A(iAz^GYfO#->gRo@<%A1-R& zewfa=ZhreDquPkgUHaU3;V@&h?pvbVjO-s_js;K!q*jcP7a)@{;coqxndK4+?wd5o zDsOEXb_Azb`{9n?!yPpJx&Hf%C;w6*zV=pnh9XVRA)37X9kb$hmb1!TMWyeb8VIU8 ztNpH!mT&tQ*3O2bHtI#)3zD6p#JsXPaPB1#FIcnqfPeHMYEwT1!^%Vum1up#R&OdV z#yCngSb$B!tY?x2%o`kE6;)n`EF=eOnN#Qf4R!HuT-dCb_k!t~w?q&%RG}r%S(Brm; z+>jM0h*XoJmrl+ku{ly!pWt(lX*}+geG59XE*EVNxO-ou84sX2L*@N-{L(PG)2F}!T|P5?e{gD#%vs(PK8)kFs^XQC zKSOM|@ELkeDaiFpSsSkt^A8FZn0@GAvTSxI02E^&m&LXy@0j-_1PDCPWF3JL!0DUG$6B+aIZ2#?Rl%R-BBKtF{}N+qc?JH#WP4u$G)&0%@(i<7Bl zyfYiDRy1b>ia|w9P|wHpTq0Z1N|m?cf{Uwm zmEY#-F~8$QA5aICVs$PMX-|+*`w&7Z^WzI+nzeSU+%xm*VsGs0IQ$$`dgUEB-B=ws zVjfF!+xZ__Wh&gni8raOUcG~?L+J}%+N69nR;VnDjUS(rm)Vd?w`uc8F~YZ&2KGHu zl&#_nr#%f zwEE$Yn~d)nN1-k3H=k7X*JkrC=Z7I--RHrV;irSJ(8h#5N2CljjF=yCJ`&JpPi+ND zw=uW-W*^5tFAKwzvNZkl{rx-5Qas}L@zw?DpOdLpG&Bu_Ued1F!+3Bi#a*7a`J(b` znr*&mQuG}i)oS#u6ZEfZ_J4S!ashDva;fiGt2FT{R({$HM%g6aUNbTM7{}F(62RYE z5Rj83Gn7eUI@@_FEHy0<1Ku1Gg#?s93*DcGpB?osDxr@ags%}Fu?)vL2nT#*P^64x zB{do+zC+UhfAzSEi3cKuSyLS1#?c*^jdTpG-?sZULZ!%LvtP7bTdHSTX~&;}wxS_g zIbMy4t9E2h4EdwI#vo0>|7_>=$;2sU0}}snshy4P5C{hwz%xF>pp8{dto8EjRKIa`sS61{61`KTfSfV}KQus#glBwBfu z+qiXnZYea$i_bE=tc-Z|a7&VW>V;wOP36J?aVXZl70GZ+kx;FA1*U$oS*un=gpA-T z?p6bT7H8x5Ajp&sg(iW2gdkL*nQsOI&g$=z7kChq8{L@nKeWBIIl z2qtXMSEN~F>v(nW#f&=7dl7{RwV_-V2-Z!0yaJvpKBJk z#t7(%)n1yETT?&4;6blM7?}9*XjDtD@Zn5hDTfib4_6;DjhYkhs9zn(gbs?|45_Bt z+~xtjDSP*V`egocFvnPHg#WO)6iY3seZfAv!;x7z(?L7uPlyx=-YlDhR8AVZ?*>-`bjjT^9P?|@39&P;S{aj+HaKLeibQO#2EPoS{LO7}t%y}H4C#M&&D0jUjb zzaFh4)%#42M$z{I2m?<)=(&z2JCnu+`%P0g19{=->cj?iv}?3p!#$I5ZDd=DH4V*Z z(YJ&tmx5I546sPzPj!mG4mKl1kDw(F&;vYO#P?qSs^K6CfPd_3f*2dzFZ@0OLl@V> zH?ewgAX?uKB{N^h>wnJD$=~7oJtRWjKLYedK|*l8%)gp1cQ;F?;VOBxh>6q$RUY5e zDS==$RX%@Jxm$a9nG3ZAWs-?R+=my(N%kM2HpneFfakf^ zQVqW*@iWO66UmcE+SQoRo~J(5w9y^rRJQ7Bnz+B^Zc$`+{>7@L#)cIf=3wlpcNw-k zk>(wWM^n6pcOmpJV7x%4No1$ZAJgH=%)&&bd0)H`F1W=#`B!amA0BVi;X9hzLCrWW zAJ%+b|bGM)omi?<;_g;l&$0Mu-CxzXPS%dzr4vpy9<2INliug3s9xx zDkz?}wc}k)nw(GU*6D#OZI^%F>q0O%x+mUSUYI4{p@yPJNdkY0%qLNWbD$IyXQrz$!w+OI*N3nJ8yCT)@m_BBsN8AiMY)_^c+itl96g#rXk^KNIR0EPb4nEDsBVh!7C?-B-tKCJ zZLCJiF17mBCj}>IS|inh2?hC6$2C87qjTT>AX;1?m9go5BNK0Vv+fjf#0V$dUyF5J zre)S7l{U|8#EDy(?j(#`iA%Xi>!%B^Jc{nqbJ5n*z0XVs(xkuD*wG|(+Uwe0 z)<`G8uX%`#KHE`!K3ufl;bj$@455E5;B?5J(@o}`dxK6X-@gV*Kg;Y;%<{{I(cYgM3jL`*SKz7h!jZ z1(vs9wfNZZMs8e2n3mT36OhA&9BfxW#qt;l2^?9!3E+rI4t5uTVGJ0I(=z#z4-mMxJ6~cU<5{dOch`ikHf4r zoNcSOk^;X6$7ik`2cDa)LRFR@@_|Ak!i|cmUg7o{!K{}IzGs;&Y^_`8KcIeTf7XU{ z+?Cu_cn{b_8J3K%1s8XpwFVaQJoEJTtnknp`4iSC)sc|}J1_+{}b!|^D#$S^q`7Y#>XsOr2&fdF>k?(+C z%hZR{fVazj{)aiMoq#wu!F?{Of7yW{&pokWFjaqNf8$S&aHbp&3gKaDob8lwmdy5P znBCi#tY3Vw#xV4SDX1$-YBPDxtmDd0z&|f4xe4|P_U1hq_{Tys zov*E59^qo``@W6t`e-1JtnLd;x>fq(PIkAolsnI8D)fxI%`cEo#YAT15ksR(>}lvm0afiLkVjP_|5DptReN*vv!et8+dlk=bk-vjTd*>tP0{(xb?P!?wJc zTeD!fmv^7ulXQ`;D5U1RpV#M2s;GI{I8|5AGKAPL5Kc>&%K!Gs`1j97?=jAGUMkKR zRsDtCg216>dSVg`x5wrP7w(?0J{YdJHc)h50zmjOlIVsiqe-e)emz)(#ZF7rI$S&cqTd1PwuiJuc4ymVt7 z{vqLFm@k_t{22;Mn2TR=JvmaxEpgwYCRo+vcIVM~50!LaqautGrP5Br14~#Hg)mL1 zGS3IzR55w+UE6a8w>m`9%Y7L)EXk9Z%?abA4+2tv8_-7L4$hZHd*`P{MS#I3; z8pIp0*&^zTe%uraqT}(Oj4|Kp?S9(}7>-&}n;O-bM8xn%PRdAvQoAU)UWK?#zs{Z9 zrxVdoA_Yra2T_udix-W5J>(DE{KPrT0QL0h!;_7?q5e%s@Du-OjF9rukA-Jt2ze%) z!1^-#nG0ZzcQL~dBcS<9O^`qTTl8!E;^(r(wkk&8!ut?)_f>SSlsl=>)t^QCicWZ5 zceq|u2non;;H%ENQ3k8+{SYZs?_6?bi&(AIB~!T=uY4Uz6NJW3HxY#xyCRQYWh0h$ zUn&PmTN=bqK@-9T3H8H}h6Jb<5RJhy)_hM)XBEfmPPMsTG%SD+5t}9t{Mz{6c>)o_X z+=TSWcOi_c!TpBs#}Us0P@;B4Vp$P5YkvVkntTUe%n%p9|I|62@%@JfsC5C6&gNGh zi-XfBoTJRej?B6tjCAoJ_Xr7gINiPr9#)d?tnEGAye@Rzm-6?_>zjw=H`gYO1@*BmRE=}e$|5|TXKd$_jXo(_&kQw5X$U{P`DP& z`YtlTS!^a_Sw|@<8OK|h{O!Xlwa7(LR$euIYm)}+I&#~$%ntAN8zQfa{psAx^gR{1 z{{^2hzS`^Bvh6<=1jUy$pEPo#);aK=DMFfnxdH;wcYym?gU=(J;vyP;4fzEEiJwv} zxG^KeQ}t7=lR^1cMm$}bdKq0y!-1~~*jr`F-|@1Hbe`p&?jcb_%J-SjH)#i%ga|7x zpCNo}EU#i3VaGZwJBW>1k#wT)@!;Jp?y~mg4`ETLjHpIR{7l(i*U`OWJr;)H@BIV= z^R&SEG-lTmRpT$j4e|`xCDpn;}fP{Zr4d0}n-7X3jx*OlD%+Y(oGG1b?p z7%;=q|1{iw9qO~EDfS8{!3kBi=ln@mFfeam6#FeQ?GtgiiD{ED!~L7@f6kBYnEEDm zNH&6mBMBMI&bz{cmOy%_gNyxUGi!Sr8_Wt?T0zE^z=?q|QqVm7$j z3iCjJO1agiIxtGFLwOP^jeu@~8D!qfI$3=YHos4Xlr49d8y}cZOU#qcg4ulvk~wI8 zt%Huon>giuRSydNIf)Spv{PsM!zb#Iez~qg6Tk-Xp+WwegCX~qVY6KYAQ{1oV{-%N|JB* zJn4GL|8PVG26PtotwqUlXyv6bj{|DZIeTKt_hMeusNy7avJH0@e>4CaC6&2#%|}7V zWM1tLa$a-CjJC{FZW{l!YO}iGDhZAon1C<`@2EqLX1RKWn#yDrnmDK~0H-ctLHrQU zPZR)~*gy8w8dL#C*aX7>Y=v#7U~9HNc##y0_2Y@jl1IZKdl3U+a+=KFpo{N{;>`8e z#PX5o1^NBFw)rW#)^o*1ElapjxfVQ~`p|%%tFf7`#nm$!rsNW4!%qY-d7qw|wI<81 z1<~77=!uz6?|Eg42O5*kr61M9W8kq`+YPObbX}^)!H@Yy#nJr&EPAww`W?NX!Ll6Y zffktv&VQdif7Jlry~nZPVNobqUdwAaXE1Q`P-2~C^^2I`AuRg2g|?0kAw)7cyK59! z^1I#$An4TWYtz1&49}STd+}Q#4w|msyt?N`7?s1j z)O@_)ELY*K$D97}S}UT%8PjWO6&oU|xY9(}vEozhpTi_)Hc{r`2nczvQ{!;O`&^g& z=4ExXOXg`YPPNGWkFb0&17aV!duG_?Xs4#q+;3Ffo&5-EYkIf~o2fa>Kki`I=nL}@ z|0~(Ac>6?#m5Wa~#&yuFILIgWobz2hX(#NmL`6P+@+Q8TFRoqtZX z&rS4R;X?N!gVn@iT3kdkUmgXG@=9jFpi@J!Z6_;Fam@<)39v?b=F zEL_MyFGa7#Ljs@ySrAstx*0YnWneZMt* z^Dw4N%fVXxolb|t$`v4Z?0g0X?Iua~3u`a>)f^==lI^slPv;mYzN)VI&_=pDG=Hxi zoSwlp)mo2$GAS4abfOo^wkhs{vTIweQw$2M-g*q$y*I}VEvc$Wrn=60SC6he2a&I|NT=bRR<9J9ruHJ2EJw|0l}ihK6U&K1?puz^(c!xOdO(^v@7^#|B9{bGo8)-v~Pwxg>)7TpvDUrthwkWeIZXv2cX< zIRAOvR1tnP3In=Cg;A*69gk ze2%8gV+AKy0!6veU%~ocx5mvVoKSIl2w#J6MQ(oZ=)Yzm# zLl3PO7CQu;!?EvEZ_$Xi=b(=abY!1v$!A2v`}F%ZcyIrO*{zo*(8|}jIBMtYvlGTO zPZ{Oibxj<9`+C||bv|CiSk~cOHN~`2^AGlR+S=foW`mcaWD9{Zg~Dkd+py@gII~DS zl;e^vCeH3jtfAbYvswB+jXM9?g=i>&8FVF&$H{*GTJuv zq472GCERtclB8L1iCTicpPDnvs*!ZeTZsz|?Ic|}J+i&Sc?jPt8gEl~z>OV0wYV03 zL^YkW=YfoIFSz!-rLfck5{2{&hKs^8+EWydfJ7jlQf0|Yv)BO?b;&?;$;(F{MsU`L z$p>>jGtq*Yz5@LU?Gl2LkZhlf7{VXjoM+|V*E`+Mn&z}WzxlcU>%xF(Q-|!zo^|kt zlvUh7zrG2YE7fMwEekSivK2@nqS|+e7V=^9DbC}VLSjt_7lA?13}y23^@kvEg59N= zQw`-Y>W8t7^pRvcH}h#aMXE&w$0t>cCz%yz7;^$Q;^K2+*RrE`%cUlNKHGL^!U^Q& zx6c1k<9Ibh3TT2JQyAnUs#~S@CDje&J3bmws}JG0Rep`DE_`HsV!)#L9M;|8dcGR&^@#jv;B6{wc*XF{_~Z0p$ti5Dy-JZb6PRQ<3}?mP6e9y zKfRp$KNEZ#$GIzqNGOrxjgZl@Fz1O7!yM;ObC_eyAw*~>g~l+Y7MiNua;G>(e)> zce37i{`L0BZl(^zdScm}LTk{j`JxiEV?NBfp4a~Pcc7s3C z1iWMrZZjQ0MkQZWe-nkA8;=+a+Wc&9EOh&)C3s}LFr8kkjeNrmMsf8bNgo5`xB>eJ z_d_|h@!%aTw0FRk#$#+G-*y~Y*S5`xyp$EVpKiHxct&QIhlP$G+bWqXSDVBw4hFSV zRXE`)Hp{GXo`WiftUreDlq%F5eiVojarG-d3Hl!O&1Pf6(n2_F{{1)g=NyAADfsmQ zaQS`Hbb@{=cMVW{*4Z?ZW15FLAsjWi z-XFO>DBdoNgX^Kz0Z{*)__LB-?PVI0U}K~?eR3g#+Je=X($7$t)N8^#M_8sBoN5ng zXm-UBi5-;b4>vOiRQR9wTesaMVYenZQ_8wCwbUSu?_)S{o9Cj9i;$9+GnZ{Ir9t8QV;FGKP+9RwwtNVh^yVXAs}uW*8H$6=l)nRe>q~J85`#^BH+c)2>HQ(V(eEqw%JCUeCnX1m zN2lClt^#Fn-f(Eqj}zDwpvAcaz?LAxe{Vb>=zAhy>t0dH{m3VG@I8ob)py6a!|CM% zk|pFPC@E><5FU7cm3cX70aI2%5F2JXk#<#CXjwQQh9nxuFX{)ktph^3KUJgCZ*J`C zN4^BCg5i*IY8t$@okQOW@7SR1Wt++soxc5523jWcwjy~aHWy6;!?RyfT;(a~h_SC7 zFOFM5Pb1Gx>*y9fnz=PAdhYy*hhehndY3jI=x?gVzzfEJkd%m4Ak8hEXUH04uM>CH z@)PZ-ptj=%&mW;JpJ2}Wtb@@HgnTP1Ct>7hWHCCjejec->mGdq6M&C? z*Wvm%^r1}H-i_~;8cK)JBNIsYm3#i|8?`Q7lQv=NVP!6Y&bxB$W~(M(5$Nc_T%X=O zwm%2G=&4&izWedL`>VHdJ}&Pb?Qn*WPltL-arUVf#&B)5+d>`UDpLTb)R0_@+L62m zXyqw(+cAX@_KZ_-mQh^yhr9$mLjAQbB+Jg1^2u1HbTSZ17{CK60H+f1prXpz&w>nx zyQTpU!>psW0lf65AfdeTCRqioGgLR|PV8S0^20G(`K_#%;FJx#cS} zl@Fw@#q_nbWU=`gS!_yeEN=}$#P6*7XNjr)VX^mK2cPzs1Rc3}oo}kQ94|`ND;>&k zrp8PNEXPpS#95_STJh`&)ax8)Z#`_CY{n=oG-74h^aY5J;?FPUXAA-c%cqG<+OjM6 zY-tVv)R>S~@TLA_suGAGchiC41eI4)t7g7XUc5Gk+58oweo*NZs9G;<#;3!RZPSSqKdaH6>C-jJaKm$Q=nu*S4D-YYT>Y{(#B6$Y~Bv zr@KJ8uQ93T+-7ZlqWIQPbpGyJQGdkhE`*!XZQIxtUm~7K2c6Yh5RzSLk2+6r>qAv7 zmTD-Iacigf1+J=)M_`T^n1#1?|8KRk;sDepUK=y&~TbDMNqulL6Kk67hfsMm2}P>*JJvFKe4~Mn!#}fC^dGN5k?5< zYL1vvSKX~=BDfAY{qt|_ zi@yL`ac|I0(Yum-efqI|Xhd7!q?K8hI!J{p^umxg=HDm_YzM}?O_{~ckV+@=toF^v z(_B7G>baO32HqcaS!$C{vXb%n99Knlchfz%Q2T8BZvQ-StR{#P^AEPnCaFbyA#g9a zPt#?#*1r@_Z?uqllUj$+cz&pbAhNJUhHM8^g!*) zgG!@O(edm#T{s3vluMFqS5$YqMb`q?;j?UqF~hp zVm}%NP?^%^=c3if>Uy(cKWm0Lt;acnijb6?pOK1rzu?u;p}>CqtR6GR!0Ot1MRJL- z`@m#Vbx_P%MJOg(;M2-%{2{YodqHp+l&i@^y|u(_v)CJE27%f(REnK^8;LxQsI% N8%vnQOLO1U{{kqB>fHbU literal 0 HcmV?d00001 diff --git a/port/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/port/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index cc3bdbce375aa41ac35f95ba872ac8c08ef230f9..90f0703fc3a0a514754bf2934e40de8f6e9890ae 100644 GIT binary patch literal 38696 zcmeEt^;gv07w#~0Hw+~qAkEO--Q5TbrG#`yhlIdLNOwqsba%IaAc9DP(k)&0!~4De z#9izDz+%A~IOpuM_p|r2&$FX6)fKQY$T2`55Vn${tTqUQQ1kqSh64O%L>*|##k^iwCtqV^A{-dqwSTM|7km0`+@}m3JC?4;5z-dREqUzwEm>4JP#F1*7@C+K3xL?PXo^@ zIHNyx(Ae@G?=o@Mlus8v4IUa2I7IUQfBS#J1YxyE%nXUz+>3-^wHU%T=wWW!YoPZ$ z5_Ra+%zimsj1~UgD3Y?B5t20Emw58&pIsGIxQ7PN?pE@{7>jh}7msQ2KK7u6S=+~O z4Y93S#UPj>(rlViAMy1e6lSTNicNyvf@VU7m#<1E{eJYgN1t~kowwOfg$KOAW#UD* z;0!)Syk@Yf9KOob1(B3-gYh9D(6JvWN!-|LRG=fIlXe^_xb*?zi4{n~0^SMHv;nn3 z&}A)<`aZto2(*tr@v;8!vH;*lZ-ZFq$b@GZjT$LjXzGz^O3e$^P%3 zKp8oZL8b!v=f};U7R;WnAmi718+agUAP?tab@pF;)1ai&QK5W!qJr7S3FTN!-dtT+BKnsvhZX|0 zZII}o;CvtZ@m%mK!rGNZJG%3~>{*hrWt*_~)_-5!ii5g?4=wMBttuf!zUa>%rtMm6 zL-LXyLIq8nbuQ60&Ow(T`R2)o;1d}TF82^WvLjjoe!n;auZ4 zS6@?X9te8QLE>PV?Nau2ooPWZUNl1tD1wZ?7zlsg32idqRS_{FYCC=xA_F3cca) za=lviuBMa->_AZ7qlpK)@Ffotus~GX^(Xy)ktY2-62%=1!v+FHbo@gf~~OH zy^n`tsYK$(|Fl%?>kDU_*+YhMy}Z3+_#A%5tI-0ECQod~#ay&uUC}zwsHl8H45C2r z$L|r1Y`Axl?t|)@1E=Q=!&}Sx;691nUqd8!bN@=>X4D5b2J!3HudNNxWG%wJOPM

    -2kQt&&u8f(J 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%0Dcpv=wUE)g`Lh~$^eMsqpx~{01t^Dy& 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 eb>A=UafhwMh+m - - - #1E538A - diff --git a/port/android/art/android-icon.png b/port/android/art/android-icon.png deleted file mode 100644 index 9c43c585a1aa0a8f3202f5af77371b67fbf4e121..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5955 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 H8uWhvq<@Wy diff --git a/port/assets/icon/opence-icon-256.png b/port/assets/icon/opence-icon-256.png new file mode 100644 index 0000000000000000000000000000000000000000..71f19675abda3c18ce2ef6e923cbde284f3ba589 GIT binary patch literal 32492 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

    IT)q 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%0DS;19;hJ%K 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

    Xj};QhPjuxZC8V2<)DFmIp9xz>9`M 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≪_mhV2NPq0XEe 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 D+`5|W%T=Z@>fh=Q7&HYe81`-7$t1F-qB2M?8 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 E<}0Gq!V0 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 z zc&;|sn1a#Neb7F9nef{#pEV)He`48`LQ9d?>#97x&

    ZF#U8%0Z0To{>vy)(cdAy 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�PNdSFuu0S}Yr{_C+M2{?%EwZBS;7 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 literal 0 HcmV?d00001 diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 97a3288c..b4b647e7 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -98,6 +98,9 @@ void dsound_sdl_output_device_check(void); void updater_start(void); void updater_poll(SDL_Window *window); static void screen_keyboard_update(void); +/* the windows' icon, a PNG (tools/embed_assets.py, from port/assets/icon) */ +extern const unsigned int platform_window_icon[]; +extern const unsigned long platform_window_icon_size; #endif BOOL platform_sdl_initialize(void) @@ -812,6 +815,16 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) return FALSE; } #ifndef HALO_ANDROID + /* the game's icon, which the desktop shows for the window (on Windows + also halo.exe's own, port/windows/halo.rc) */ + if (platform_window_icon_size) + { + SDL_Surface *icon = SDL_LoadPNG_IO(SDL_IOFromConstMem(platform_window_icon, platform_window_icon_size), true); + + if (!icon || !SDL_SetWindowIcon(platform_window, icon)) + platform_log("cannot set the window's icon: %s", SDL_GetError()); + SDL_DestroySurface(icon); + } platform_fullscreen_requested = platform_fullscreen_setting(); platform_fullscreen_kind_apply(); #endif diff --git a/port/windows/halo.rc b/port/windows/halo.rc new file mode 100644 index 00000000..0dff6f23 --- /dev/null +++ b/port/windows/halo.rc @@ -0,0 +1,4 @@ +/* halo.exe's resources (tools/windows_build.py): its icon (tools/app_icon.py), +which Explorer and the taskbar show and SDL's windows take, as the first icon +of the executable */ +1 ICON "opence-icon.ico" diff --git a/port/windows/opence-icon.ico b/port/windows/opence-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4e1c92fa3411abb0a4593c1cc5a0974b6f32fe13 GIT binary patch literal 67233 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�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 L>nJrSScm@)6N6v? literal 0 HcmV?d00001 diff --git a/tools/android_icon.py b/tools/android_icon.py deleted file mode 100644 index b00769fd..00000000 --- a/tools/android_icon.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -"""Makes the Android app's launcher icon from its artwork, -port/android/art/android-icon.png (a light emblem on a solid background): - - python tools/android_icon.py - -The icon is an adaptive one (Android 8 and later): the artwork's background -colour as its background layer, and the emblem, cut out onto transparency, -as its foreground and monochrome (themed icon) layers. Launchers mask -adaptive icons to their own shapes and show only the middle of the 108 dp -layers, so the emblem is scaled to fit Android's 66 dp safe zone, the circle -no mask cuts into. Writes the layers for every screen density into -port/android/app/src/main/res (mipmap-*), and the icon's definition -(mipmap-anydpi-v26/ic_launcher.xml); run it again when the artwork changes. -Needs Pillow. -""" - -import math -from pathlib import Path - -from PIL import Image - -ROOT = Path(__file__).resolve().parent.parent -ARTWORK = ROOT / "port/android/art/android-icon.png" -RESOURCES = ROOT / "port/android/app/src/main/res" - -# the layers' size, and the safe zone's diameter, in dp -LAYER_DP = 108 -SAFE_DP = 64 # (66, less a little room) -# pixels per dp of each density -DENSITIES = {"mdpi": 1.0, "hdpi": 1.5, "xhdpi": 2.0, "xxhdpi": 3.0, "xxxhdpi": 4.0} - - -def main() -> None: - artwork = Image.open(ARTWORK).convert("RGBA") - width, height = artwork.size - background = artwork.getpixel((0, 0))[:3] - emblem_colour = max((artwork.getpixel((x, y))[:3] for x in range(width) for y in range(height)), - key=sum) - - # the emblem on transparency: each pixel's opacity is how far it is from - # the background towards the emblem's colour (the artwork's edges are - # blended between the two) - span = sum(emblem_colour) - sum(background) - emblem = Image.new("RGBA", artwork.size) - furthest = 0.0 - for y in range(height): - for x in range(width): - pixel = artwork.getpixel((x, y)) - opacity = max(0.0, min(1.0, (sum(pixel[:3]) - sum(background)) / span)) - if opacity > 0.0: - emblem.putpixel((x, y), (*emblem_colour, round(opacity * 255))) - furthest = max(furthest, math.hypot(x + 0.5 - width / 2, y + 0.5 - height / 2)) - - # the artwork scaled so that the emblem's furthest point from the middle - # is on the safe zone's edge - artwork_dp = (SAFE_DP / 2) / furthest * width - for density, scale in DENSITIES.items(): - size = round(LAYER_DP * scale) - side = round(artwork_dp * scale) - layer = Image.new("RGBA", (size, size)) - layer.alpha_composite(emblem.resize((side, side), Image.LANCZOS), ((size - side) // 2, (size - side) // 2)) - directory = RESOURCES / f"mipmap-{density}" - directory.mkdir(parents=True, exist_ok=True) - layer.save(directory / "ic_launcher_foreground.png", optimize=True) - print(f"{directory.relative_to(ROOT)}/ic_launcher_foreground.png: {size}x{size}") - - colour = "#{:02X}{:02X}{:02X}".format(*background) - values = RESOURCES / "values" - values.mkdir(parents=True, exist_ok=True) - (values / "ic_launcher_background.xml").write_text( - '\n' - "\n" - "\n" - f' {colour}\n' - "\n", encoding="utf-8") - adaptive = RESOURCES / "mipmap-anydpi-v26" - adaptive.mkdir(parents=True, exist_ok=True) - (adaptive / "ic_launcher.xml").write_text( - '\n' - "\n" - '\n' - ' \n' - ' \n' - ' \n' - "\n", encoding="utf-8") - print(f"background {colour}; the emblem {artwork_dp:.1f} dp across its artwork") - - -if __name__ == "__main__": - main() diff --git a/tools/app_icon.py b/tools/app_icon.py new file mode 100644 index 00000000..a08917f7 --- /dev/null +++ b/tools/app_icon.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Makes the game's icons from its artwork, port/assets/icon: + + python tools/app_icon.py + +- opence-icon.png, the helmet on its background (512x512): the Windows + executable's icon (port/windows/opence-icon.ico, which port/windows/halo.rc + puts in halo.exe) and the desktop builds' window icon (opence-icon-256.png, + which tools/embed_assets.py embeds and sdl_platform.c gives the window); +- opence-icon-render.png, the helmet alone on transparency (1024x1024, the same + framing): the Android app's launcher icon. + +The launcher icon is an adaptive one (Android 8 and later): the render as its +foreground layer, the background's gradient as its background layer, and the +render's light parts (the armour, not the visor) as its monochrome (themed +icon) layer. Launchers mask adaptive icons to their own shapes and show only +the middle 72 dp of the 108 dp layers, so the render fills those 72 dp, as +the picture fills the other icons, and its edges are carried out to the +layers' edges for the launchers that move the layers. Writes the layers for +every screen density into port/android/app/src/main/res (mipmap-*), and the +icon's definition (mipmap-anydpi-v26/ic_launcher.xml) and background +(drawable/ic_launcher_background.xml). Run it again when the artwork +changes. Needs Pillow and NumPy. +""" + +from pathlib import Path + +import numpy as np +from PIL import Image + +ROOT = Path(__file__).resolve().parent.parent +ARTWORK = ROOT / "port/assets/icon" +ICON = ARTWORK / "opence-icon.png" +RENDER = ARTWORK / "opence-icon-render.png" +WINDOW_ICON = ARTWORK / "opence-icon-256.png" +WINDOWS_ICON = ROOT / "port/windows/opence-icon.ico" +RESOURCES = ROOT / "port/android/app/src/main/res" + +# the layers' size, and the part of them a launcher shows, in dp +LAYER_DP = 108 +VISIBLE_DP = 72 +# pixels per dp of each density +DENSITIES = {"mdpi": 1.0, "hdpi": 1.5, "xhdpi": 2.0, "xxhdpi": 3.0, "xxxhdpi": 4.0} +WINDOWS_SIZES = [16, 20, 24, 32, 40, 48, 64, 128, 256] + + +def background_gradient(icon: Image.Image, render: Image.Image) -> tuple: + """the icon's background as a line through its rows' colours (where the + render leaves it bare): the colours at the top and the bottom of the + launcher's layer, which reaches (LAYER_DP - VISIBLE_DP) / 2 beyond the + picture at each side""" + pixels = np.asarray(icon.convert("RGB"), dtype=np.float64) + bare = np.asarray(render.resize(icon.size, Image.LANCZOS).getchannel("A")) == 0 + rows, colours = [], [] + for y in range(icon.height): + if bare[y].any(): + rows.append(y) + colours.append(pixels[y][bare[y]].mean(axis=0)) + fit = np.polyfit(np.array(rows, dtype=np.float64), np.array(colours), 1) + margin = (LAYER_DP - VISIBLE_DP) / 2 / VISIBLE_DP * icon.height + + def at(y: float) -> tuple: + return tuple(int(round(min(255.0, max(0.0, fit[0][c] * y + fit[1][c])))) for c in range(3)) + + return at(-margin), at(icon.height + margin) + + +def carried_out(image: Image.Image, size: int) -> Image.Image: + """image in the middle of a square of size, its edge pixels carried out + to the square's edges""" + pad = (size - image.width) // 2 + data = np.asarray(image) + padded = np.pad(data, ((pad, size - image.height - pad), (pad, size - image.width - pad), (0, 0)), mode="edge") + return Image.fromarray(padded, "RGBA") + + +def monochrome(render: Image.Image) -> Image.Image: + """the render's light parts, white with their lightness as opacity: the + armour without the darker visor""" + data = np.asarray(render, dtype=np.float64) + lightness = (0.2126 * data[..., 0] + 0.7152 * data[..., 1] + 0.0722 * data[..., 2]) / 255.0 + alpha = np.clip(data[..., 3] / 255.0 * np.clip((lightness - 0.25) / 0.5, 0.0, 1.0), 0.0, 1.0) + out = np.zeros(data.shape, dtype=np.uint8) + out[..., :3] = 255 + out[..., 3] = np.round(alpha * 255).astype(np.uint8) + return Image.fromarray(out, "RGBA") + + +def main() -> None: + icon = Image.open(ICON).convert("RGBA") + render = Image.open(RENDER).convert("RGBA") + + # the desktop's icons + icon.resize((256, 256), Image.LANCZOS).save(WINDOW_ICON, optimize=True) + print(f"{WINDOW_ICON.relative_to(ROOT)}: 256x256") + icon.save(WINDOWS_ICON, sizes=[(size, size) for size in WINDOWS_SIZES]) + print(f"{WINDOWS_ICON.relative_to(ROOT)}: {', '.join(str(size) for size in WINDOWS_SIZES)}") + + # the launcher's layers + themed = monochrome(render) + for density, scale in DENSITIES.items(): + size = round(LAYER_DP * scale) + visible = round(VISIBLE_DP * scale) + directory = RESOURCES / f"mipmap-{density}" + directory.mkdir(parents=True, exist_ok=True) + for name, layer in (("ic_launcher_foreground", render), ("ic_launcher_monochrome", themed)): + carried_out(layer.resize((visible, visible), Image.LANCZOS), size).save( + directory / f"{name}.png", optimize=True) + print(f"{directory.relative_to(ROOT)}: {size}x{size}") + + top, bottom = background_gradient(icon, render) + drawable = RESOURCES / "drawable" + drawable.mkdir(parents=True, exist_ok=True) + (drawable / "ic_launcher_background.xml").write_text( + '\n' + "\n" + '\n' + ' \n'.format(*top, *bottom) + + "\n", encoding="utf-8") + adaptive = RESOURCES / "mipmap-anydpi-v26" + adaptive.mkdir(parents=True, exist_ok=True) + (adaptive / "ic_launcher.xml").write_text( + '\n' + "\n" + '\n' + ' \n' + ' \n' + ' \n' + "\n", encoding="utf-8") + print("background #{:02X}{:02X}{:02X} to #{:02X}{:02X}{:02X}".format(*top, *bottom)) + + +if __name__ == "__main__": + main() diff --git a/tools/embed_assets.py b/tools/embed_assets.py index fc22b4ff..0b0f6c19 100644 --- a/tools/embed_assets.py +++ b/tools/embed_assets.py @@ -40,6 +40,8 @@ MENU_LIST = MENU_ASSETS / "menus.json" SMAA_ASSETS = Path("port/third_party/smaa") SMAA_FILES = (("SMAA.hlsl", "xgpu_smaa_shader"), ("area_tex.zlib", "xgpu_smaa_area_texture"), ("search_tex.zlib", "xgpu_smaa_search_texture")) +# the desktop windows' icon (tools/app_icon.py; port/linux/src/sdl_platform.c) +WINDOW_ICON = Path("port/assets/icon/opence-icon-256.png") PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" @@ -78,7 +80,8 @@ def hud_asset_inputs() -> List[Path]: inputs = [listing for listing in (LAYOUT, TITLE_LIST, FONT_LIST, MENU_LIST) if (ROOT / listing).is_file()] return [*inputs, *(folder / f"{asset['name']}.png" for folder, asset, _ in textures()), *(FONT_ASSETS / name for name in font_files()), *(MENU_ASSETS / name for name in menu_files()), - *(SMAA_ASSETS / name for name, _ in smaa_files())] + *(SMAA_ASSETS / name for name, _ in smaa_files()), + *([WINDOW_ICON] if (ROOT / WINDOW_ICON).is_file() else [])] def hud_configure_inputs() -> List[Path]: @@ -220,6 +223,13 @@ def main() -> None: lines.extend(words(data) if data else ["\t0,"]) lines.append("};") lines.append(f"const unsigned long {symbol}_size = {len(data)};") + # the windows' icon, a PNG (of size 0 where the checkout has none) + data = (ROOT / WINDOW_ICON).read_bytes() if (ROOT / WINDOW_ICON).is_file() else b"" + lines.append("") + lines.append("const unsigned int platform_window_icon[] = {") + lines.extend(words(data) if data else ["\t0,"]) + lines.append("};") + lines.append(f"const unsigned long platform_window_icon_size = {len(data)};") lines.append("") lines.append("#endif") output = Path(sys.argv[1]) diff --git a/tools/windows_build.py b/tools/windows_build.py index f3d0334d..66726687 100644 --- a/tools/windows_build.py +++ b/tools/windows_build.py @@ -183,6 +183,15 @@ PROFILE_RUNTIME_HEADERS = [ ] +def windows_rc(cc: str) -> str: + """LLVM's resource compiler of the clang named cc: beside it, where cc is + a path, else the one on the PATH""" + path = Path(cc) + if path.parent == Path("."): + return "llvm-rc" + return str(path.with_name("llvm-rc" + path.suffix)) + + def clang_release(cc: str) -> Optional[str]: """the release (22.1.0) of the clang named cc, or None""" try: @@ -293,6 +302,16 @@ def generate_windows_build(n: Writer, sln: Any) -> None: rspfile="$out.rsp", rspfile_content="$in_newline", ) + # the executable's resources (port/windows/halo.rc: its icon), compiled + # by LLVM's resource compiler, beside the clang that builds the game + n.variable("windows_rc", windows_rc(cc)) + n.rule( + name="windows_rc", + command="$windows_rc /no-preprocess /FO $out $in", + description="WINDOWS RC $out", + ) + resources = BUILD / "halo.res" + n.build(outputs=resources, rule="windows_rc", inputs=PORT_DIR / "halo.rc", implicit=[PORT_DIR / "opence-icon.ico"]) n.rule( name="windows_copy", command="$python -c \"import shutil,sys; shutil.copyfile(sys.argv[1], sys.argv[2])\" $in $out", @@ -454,7 +473,7 @@ def generate_windows_build(n: Writer, sln: Any) -> None: n.build( outputs=output, rule="windows_link", - inputs=objects + extra_objects, + inputs=objects + extra_objects + [resources], variables={"ldflags": " ".join(base_ldflags + extra_ldflags), "libs": libs}, ) From 42a97d4f07b73ceb96f3c53cbeb28dcebe5d0dd8 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:33:22 +1300 Subject: [PATCH 109/187] libtiff left out of the builds Its only user, tiff_export, wrote the Xbox's movie recorder's frames, which nothing starts: the builds leave out source/bitmaps/libtiff/ and tiff_file.c, and port/linux/game/tiff_file.c's tiff_export says it writes none. port.json's exclusions may name a folder (ending in /). --- port/linux/game/tiff_file.c | 21 +++++++++++++++++++++ port/linux/port.json | 6 +++--- tools/linux_build.py | 7 ++++--- 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 port/linux/game/tiff_file.c diff --git a/port/linux/game/tiff_file.c b/port/linux/game/tiff_file.c new file mode 100644 index 00000000..3936e945 --- /dev/null +++ b/port/linux/game/tiff_file.c @@ -0,0 +1,21 @@ +/* +TIFF_FILE.C + +port: the builds leave out source/bitmaps/tiff_file.c and the libtiff it +writes with (port/linux/port.json). Its one function, tiff_export, is called +only by the Xbox's movie recorder (main.c's screenshot_record, while +main_movie_start's movie runs), which nothing starts; the port's screenshots +are its own (d3d8_gl.c). +*/ + +#include "cseries/cseries.h" +#include "bitmaps/tiff_file.h" + +char const *tiff_export( + struct file_reference *file, + struct bitmap_data *bitmap) +{ + (void)file; + (void)bitmap; + return "this build writes no TIFF files"; +} diff --git a/port/linux/port.json b/port/linux/port.json index 9ffc8ae8..10408910 100644 --- a/port/linux/port.json +++ b/port/linux/port.json @@ -1,11 +1,11 @@ { "comment": "Native build inputs, shared by the Windows and Android builds. See port/linux/README.md.", "game": { - "comment": "The game: every C file under root but those excluded (zlib's and libtiff's own programs), built with these defines and include directories.", + "comment": "The game: every C file under root but those excluded (a folder ends in /: libtiff and the TIFF writer that uses it, which only the Xbox's movie recorder called, port/linux/game/tiff_file.c; zlib's own programs), built with these defines and include directories.", "root": "source", "exclude": [ - "source/bitmaps/libtiff/mkg3states.c", - "source/bitmaps/libtiff/mkspans.c", + "source/bitmaps/libtiff/", + "source/bitmaps/tiff_file.c", "source/memory/zlib/contrib/asm386/gvmat32c.c", "source/memory/zlib/contrib/minizip/miniunz.c", "source/memory/zlib/contrib/minizip/minizip.c", diff --git a/tools/linux_build.py b/tools/linux_build.py index 5ac13fde..08d06e3d 100644 --- a/tools/linux_build.py +++ b/tools/linux_build.py @@ -29,12 +29,13 @@ def xdk_headers() -> List[Path]: def game_sources(config: Dict[str, Any]) -> List[Path]: """the game's C sources (port.json "game"): every one under its root but - those excluded""" + those excluded (a file, or a folder: a name ending in "/")""" game = config["game"] - excluded = set(game.get("exclude", [])) + excluded = game.get("exclude", []) return sorted( source for source in Path(game["root"]).rglob("*.c") - if source.as_posix() not in excluded + if not any(source.as_posix() == name or (name.endswith("/") and source.as_posix().startswith(name)) + for name in excluded) ) From 2aa38971893733f0e8f94ed574ebef0c72bdae98 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:59:22 +1300 Subject: [PATCH 110/187] Setting rows merged and values widened for a finger only (#50) A finger's targets: a tap on a setting row's label reaches its value, and the values are wider. The mouse clicks the labels and values where CE's menus have them, as before the PR. --- source/interface/ui_widget.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 1ce6429e..8512864a 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -7362,10 +7362,14 @@ void render_ui_widgets( { /* fit the legends while the rows are still targets (merging removes them); widen after merging (the merged values are - row-tall) */ + row-tall). A finger's targets only: the mouse clicks the + labels and values where CE's menus have them */ ui_mouse_fit_button_targets(); - ui_mouse_merge_setting_rows(); - ui_mouse_widen_values(); + if (ui_mouse_pointer_is_touch) + { + ui_mouse_merge_setting_rows(); + ui_mouse_widen_values(); + } ui_mouse_targets_settled = TRUE; } if (widget_globals.fade_to_black >= 0.0f && From d90f84faa3323774479cbdd44de6141a47e07ada Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:50:26 -0400 Subject: [PATCH 111/187] Add on-screen touch controls for the game on Android An overlay over the game, from JoshRob297's halo-ce-touch, which ported theLlamaNet's halo-ce-android: a stick, the controller's buttons with word labels, swipe aiming, optional gyroscope aiming, the phone's vibration, and an editor to move, resize, hide, copy, export and import the buttons. It is player 1's controller, merged into port 0's state as the keyboard is. Changes from the fork: - The controls show only in a game. In the menus and during cinematics they hide and let the fingers through, so HiIAmMoot's menu touch (upstream #50) and its tap to skip work under them. The game tells the overlay through a new host import, host_touch_scene. - They hide while a controller with a stick is connected, such as a handheld's built-in one, and never show without a touchscreen (a TV). The new setting input.touch_controls ("auto", "on", "off"; default "auto") overrides this. Hiding with a controller follows diegotorres00's upstream #58. - Swipes that start in Android's edge-gesture zones do not turn the view, and a press shorter than a frame is held long enough for the game to see it, as HiIAmMoot suggested in the review of #58. - The swipe is not the mouse's aiming, so it keeps the controller's aim assist. - The phone vibrates from port 0's XInputSetState, so the game's own vibration setting applies and no game source changes. It vibrates only while the touch controls show. - The fork's cheats menu and its built-in cheat buttons are left out. Co-authored-by: JoshRob297 <99092299+JoshRob297@users.noreply.github.com> Co-authored-by: theLlamaNet <107216990+theLlamaNet@users.noreply.github.com> (cherry picked from commit 7bd7481bdede370558a3c93d286e37a9c24237bd) (cherry picked from commit 5911e2312b6960de58dfa1cad2fefce1a17b97f7) --- port/android/README.md | 57 +- .../java/com/halo/decomp/GyroscopeAim.java | 35 + .../java/com/halo/decomp/HaloActivity.java | 117 +++ .../java/com/halo/decomp/TouchControls.java | 693 ++++++++++++++++++ .../java/com/halo/decomp/TouchLayout.java | 189 +++++ port/android/guest/runtime/guest_host.h | 12 + port/android/host/host_touch.c | 173 +++++ port/android/host_imports.list | 4 + port/linux/game/touch_game.c | 7 + port/linux/src/port_config.c | 5 + port/linux/src/touch_input.c | 115 +++ port/linux/src/touch_input.h | 14 + port/linux/src/xinput_sdl.c | 19 +- tools/test_touch_layout.py | 18 + tools/tests/GyroscopeAimTest.java | 34 + tools/tests/TouchLayoutTest.java | 109 +++ 16 files changed, 1593 insertions(+), 8 deletions(-) create mode 100644 port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java create mode 100644 port/android/app/src/main/java/com/halo/decomp/TouchControls.java create mode 100644 port/android/app/src/main/java/com/halo/decomp/TouchLayout.java create mode 100644 port/android/host/host_touch.c create mode 100644 tools/test_touch_layout.py create mode 100644 tools/tests/GyroscopeAimTest.java create mode 100644 tools/tests/TouchLayoutTest.java diff --git a/port/android/README.md b/port/android/README.md index 7f1975aa..ec113df0 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -6,7 +6,7 @@ The game shows its graphics with OpenGL ES 3. It plays sound through SDL3 (AAudio). It accepts input from game controllers, for example a PlayStation -5 DualSense on Bluetooth. The app needs Android 9 (API 28) or later. It +5 DualSense on Bluetooth, and from the touchscreen (refer to "Controls"). The app needs Android 9 (API 28) or later. It operates on 64-bit-only devices, for example the Pixel 9 Pro XL. The Android build uses the platform layer of the Linux build @@ -136,13 +136,58 @@ scroll a list (down or right steps back, up or left steps forward). A drag stops at the first and the last item, and it does not change a setting's value. Touches that start in the edge-gesture zones of Android do not tap or scroll at the sides, and do not scroll at the top and bottom, because the -first swipe from an edge in full screen only shows the system bars. Apart -from skipping cinematics, the gameplay does not accept touch input. +first swipe from an edge in full screen only shows the system bars. A tap during a cinematic that can be skipped skips it, as A does. On the on-screen keyboard, tap a key to press it, "B =BACK" to cancel and "A =ENTER" to accept the name. +### Touch controls + +In a game, the app shows touch controls over the picture. They are a +controller for player 1: + +| Control | Function in the game | +| --- | --- | +| stick (lower left) | move | +| swipe on the screen away from the buttons | look | +| Fire | right trigger | +| Grenade | left trigger | +| A / Jump, B / Melee, X / Reload, Y / Weapon | A, B, X, Y | +| Crouch, Zoom | left and right stick clicks | +| Light, Gren. type | white, black | +| Pause, Back | start, back | +| Up, Down, Left, Right | D-pad | + +A finger that holds a button can also swipe to look, so you can fire and +aim with one thumb. A short tap reaches the game even when it is shorter +than one frame. Swipes that start in the edge-gesture zones of Android do +not turn the view. The view does not use the mouse settings: the swipe +keeps the aim assist of the controller. + +The touch controls show only in a game. In the menus and during +cinematics they hide, and the touchscreen operates the menus as described +above. They also hide when a controller is connected, for example the +built-in controller of a handheld. A device without a touchscreen (a TV) +never shows them. The setting `input.touch_controls` changes this (refer +to "Settings"). + +The buttons at the top of the screen: + +- "Hide" removes the controls until you push "Touch". +- "Options" opens these items: + - "General": the phone's vibration (on by default; it follows the + vibration setting of the game's profile), aiming with the gyroscope + (off by default), "Hide or add buttons" (hide a button, add a copy of + a button) and "Edit buttons size". + - "Edit buttons layout": drag the controls to new positions, then push + "Save and exit". "Export" and "Import" write and read a layout file + with the file picker of the system. + - "Look sensitivity". + +The app keeps the layout in its own preferences, not in `config.toml`. +Removing the app's data or the app removes the layout. + ## Settings The settings are in `config.toml` in the data folder of the app. To change @@ -161,6 +206,7 @@ These settings are only for Android: | Setting | Function | | --- | --- | +| `input.touch_controls` | The touch controls in a game. `"auto"` (the default): shown on a touchscreen while no controller is connected. `"on"`: also shown with a controller. `"off"`: never shown. A device without a touchscreen never shows them. The menus take taps with each value. | | `display.screen_width` | The number of columns of the 480-line picture. `0` (the default): the shape of the display (1068 on a 20:9 phone). `640`: the 4:3 shape of the Xbox. | | `debug.sample_seconds` | Refer to "Find problems". | | `debug.memory_watch` | `true` (the default): the app notices the game's writes to textures and vertices by page protection. `false`: it compares page contents once a frame instead, which is slower. Refer to "Limits". | @@ -365,8 +411,9 @@ assembly of the port is necessary: - The device must let the app reserve the fixed guest addresses, from `0x80000000` to approximately `0x89000000`. If the addresses are not available, the app shows a message. -- Touch operates the menus and skips cinematics. In the game, use a controller - or a keyboard. +- Touch operates the menus and skips cinematics. In the game, the touch + controls appear when no controller is connected; their size follows the + height of the screen, so they are larger on a tablet than on a phone. - Kernels with 16 KB pages (a developer option of Android 15) do not operate. The Xbox memory uses 4 KB pages. - The x86 Android emulator runs the app through its ARM translation. The diff --git a/port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java b/port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java new file mode 100644 index 00000000..18ac94c3 --- /dev/null +++ b/port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java @@ -0,0 +1,35 @@ +package com.halo.decomp; + +/** Integrates angular velocity in display coordinates; output is in radians. */ +final class GyroscopeAim { + private long previousTime; + private int previousRotation = -1; + private float previousX, previousY; + + void reset() { previousTime = 0; previousRotation = -1; } + + boolean sample(long time, float x, float y, int rotation, float[] delta) { + delta[0] = delta[1] = 0; + if (!Float.isFinite(x) || !Float.isFinite(y) || rotation < 0 || rotation > 3) { + reset(); return false; + } + float screenX = x, screenY = y; + switch (rotation) { + case 1: screenX = y; screenY = -x; break; + case 2: screenX = -x; screenY = -y; break; + case 3: screenX = -y; screenY = x; break; + } + // Small stationary sensor noise must not gradually drift the camera. + if (Math.abs(screenX) < 0.01f) screenX = 0; + if (Math.abs(screenY) < 0.01f) screenY = 0; + float dt = (time-previousTime)*1e-9f; + boolean valid = previousTime != 0 && rotation == previousRotation && dt > 0 && dt <= 0.1f; + if (valid) { + delta[0] = -(screenY+previousY)*0.5f*dt; + delta[1] = -(screenX+previousX)*0.5f*dt; + } + previousTime = time; previousRotation = rotation; + previousX = screenX; previousY = screenY; + return valid && (delta[0] != 0 || delta[1] != 0); + } +} diff --git a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java index 4d907a47..2cce0b25 100644 --- a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java +++ b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java @@ -1,8 +1,17 @@ package com.halo.decomp; +import android.app.AlertDialog; import android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.net.wifi.WifiManager; import android.os.Bundle; +import android.view.ViewGroup; +import android.widget.Toast; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import android.graphics.Insets; import android.os.Build; import android.view.Display; @@ -25,6 +34,10 @@ public class HaloActivity extends SDLActivity { * whole, never changed in place */ private volatile int[] gestureInsets = new int[] { 0, 0, 0, 0 }; + /** the on-screen touch controls, over SDL's surface */ + private TouchControls touchControls; + private static final int EXPORT_LAYOUT = 401, IMPORT_LAYOUT = 402; + private String pendingLayoutExport; @Override protected String[] getLibraries() { @@ -34,6 +47,13 @@ public class HaloActivity extends SDLActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (savedInstanceState != null) + pendingLayoutExport = savedInstanceState.getString("pending-layout-export"); + if (mLayout != null) { + touchControls = new TouchControls(this); + mLayout.addView(touchControls, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); preferHighestRefreshRate(); trackGestureInsets(); @@ -42,8 +62,105 @@ public class HaloActivity extends SDLActivity { Updater.start(this); } + /** the system's file picker, for the touch layout; no storage permission */ + public void chooseLayoutFile(boolean export, String configuration) { + new AlertDialog.Builder(this).setTitle(export ? "Export layout" : "Import layout") + .setMessage(export ? "Choose the folder and file name for your touch layout." + : "Choose an exported touch layout. It replaces your buttons, sensitivity and General settings.") + .setNegativeButton("Cancel", null) + .setPositiveButton(export ? "Choose location" : "Choose file", (dialog, which) -> { + Intent intent = new Intent(export ? Intent.ACTION_CREATE_DOCUMENT : Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(export ? "text/plain" : "*/*"); + if (export) { + pendingLayoutExport = configuration; + intent.putExtra(Intent.EXTRA_TITLE, "halo-touch-layout.halolayout"); + } + try { startActivityForResult(intent, export ? EXPORT_LAYOUT : IMPORT_LAYOUT); } + catch (android.content.ActivityNotFoundException e) { + pendingLayoutExport = null; + Toast.makeText(this, "No document picker is available.", Toast.LENGTH_LONG).show(); + } + }).show(); + } + + @Override protected void onSaveInstanceState(Bundle state) { + super.onSaveInstanceState(state); + state.putString("pending-layout-export", pendingLayoutExport); + } + + @Override protected void onActivityResult(int request, int result, Intent data) { + if (request != EXPORT_LAYOUT && request != IMPORT_LAYOUT) { + super.onActivityResult(request, result, data); return; + } + String exported = pendingLayoutExport; pendingLayoutExport = null; + if (result != RESULT_OK || data == null || data.getData() == null) return; + Uri document = data.getData(); + new Thread(() -> { + try { + if (request == EXPORT_LAYOUT) { + if (exported == null) throw new java.io.IOException("Layout snapshot is unavailable"); + try (OutputStream output = getContentResolver().openOutputStream(document, "wt")) { + if (output == null) throw new java.io.IOException("Cannot open destination"); + output.write(exported.getBytes(StandardCharsets.UTF_8)); + } + runOnUiThread(() -> Toast.makeText(this, "Layout exported.", Toast.LENGTH_SHORT).show()); + } else { + ByteArrayOutputStream contents = new ByteArrayOutputStream(); + try (InputStream input = getContentResolver().openInputStream(document)) { + if (input == null) throw new java.io.IOException("Cannot open layout file"); + byte[] buffer = new byte[4096]; int size; + while ((size = input.read(buffer)) != -1) { + if (contents.size()+size > 65536) throw new java.io.IOException("Layout file is too large"); + contents.write(buffer, 0, size); + } + } + String configuration = new String(contents.toByteArray(), StandardCharsets.UTF_8); + // checked away from the UI thread, then applied to the overlay at once + TouchLayout.importConfiguration(configuration); + runOnUiThread(() -> { + if (isFinishing() || isDestroyed() || touchControls == null) return; + try { + touchControls.importLayout(configuration); + Toast.makeText(this, "Layout imported and saved.", Toast.LENGTH_SHORT).show(); + } catch (IllegalArgumentException e) { layoutFileError(e); } + }); + } + } catch (Exception e) { runOnUiThread(() -> layoutFileError(e)); } + }, "halo-touch-layout-file").start(); + } + + private void layoutFileError(Exception error) { + if (isFinishing() || isDestroyed()) return; + new AlertDialog.Builder(this).setTitle("Layout file") + .setMessage("Could not complete the operation: "+error.getMessage()) + .setPositiveButton("OK", null).show(); + } + + @Override protected void onResume() { + super.onResume(); + if (touchControls != null && getWindow().getDecorView().hasWindowFocus()) + touchControls.startDeviceInput(); + } + + @Override + protected void onPause() { + if (touchControls != null) touchControls.stopDeviceInput(); + super.onPause(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (touchControls != null) { + if (hasFocus) touchControls.startDeviceInput(); + else touchControls.stopDeviceInput(); + } + super.onWindowFocusChanged(hasFocus); + } + @Override protected void onDestroy() { + if (touchControls != null) touchControls.stopDeviceInput(); if (multicastLock != null && multicastLock.isHeld()) multicastLock.release(); multicastLock = null; diff --git a/port/android/app/src/main/java/com/halo/decomp/TouchControls.java b/port/android/app/src/main/java/com/halo/decomp/TouchControls.java new file mode 100644 index 00000000..5513d2ec --- /dev/null +++ b/port/android/app/src/main/java/com/halo/decomp/TouchControls.java @@ -0,0 +1,693 @@ +package com.halo.decomp; + +import android.app.AlertDialog; +import android.media.AudioAttributes; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Build; +import android.os.SystemClock; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.view.WindowManager; +import android.widget.Switch; +import android.content.Context; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.SeekBar; +import android.widget.TextView; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.hardware.input.InputManager; +import android.view.InputDevice; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.SparseIntArray; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Toast; + +/** + * The on-screen touch controls: a multitouch overlay over SDL's surface, + * with a stick, the controller's buttons, swipe (and optionally gyroscope) + * aiming and full-display layout editing. Its state goes to the game + * through port/android/host/host_touch.c, as port 0's controller. + * + * It shows only in a game, never over the menus or a cinematic (which take + * taps themselves, port/linux/src/touch_input.c), and with + * input.touch_controls = "auto" only on a touchscreen while no controller + * is connected. While hidden it lets every finger through to the game. + */ +public final class TouchControls extends View implements SensorEventListener, InputManager.InputDeviceListener { + private static final int LEFT = TouchLayout.LEFT, LOOK = -5; + private static final int TOGGLE = -3, EDIT = -4, EXPORT = -6, IMPORT = -7; + private static final class Button { + final String label; + final float radius; + final int bit, trigger; + Button(String label, float radius, int bit, int trigger) { + this.label = label; this.radius = radius; + this.bit = bit; this.trigger = trigger; + } + } + // Button bits follow SDL_GamepadButton; triggers are SDL axes 4 and 5. + private final Button[] buttons = { + new Button("A / Jump", 36, 0, -1), + new Button("B / Melee", 32, 1, -1), + new Button("X / Reload", 34, 2, -1), + new Button("Y / Weapon", 32, 3, -1), + new Button("Fire", 39, -1, 5), + new Button("Grenade", 35, -1, 4), + new Button("Crouch", 32, 7, -1), + new Button("Zoom", 32, 8, -1), + new Button("Light", 27, 9, -1), + new Button("Gren. type", 29, 10, -1), + new Button("Pause", 28, 6, -1), + new Button("Back", 28, 4, -1), + new Button("Up", 25, 11, -1), + new Button("Down", 25, 12, -1), + new Button("Left", 25, 13, -1), + new Button("Right", 25, 14, -1), + new Button("", 64, -1, -1), // movement stick keeps its saved index + new Button("Fire", 39, -1, 5) + }; + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final SparseIntArray owners = new SparseIntArray(); + private final SparseArray buttonTouches = new SparseArray<>(); + private final int[] axes = new int[6]; + private TouchLayout layout = new TouchLayout(); + private final SharedPreferences preferences; + private boolean editing, optionsOpen; + private final SensorManager sensors; + private final Sensor gyroscope; + private final Vibrator vibrator; + private final GyroscopeAim gyroAim = new GyroscopeAim(); + private final float[] gyroDelta = new float[2]; + private boolean deviceInputActive, gyroRegistered; + private int lastAmplitude; + private long lastVibration; + private final AudioAttributes rumbleAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_GAME).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build(); + private static native int nativeRumble(); + /** reads what the game says every frame: when to show, and the rumble */ + private final Runnable poll = new Runnable() { + public void run() { + if (!deviceInputActive) return; + boolean wasShown = shown(); + scene = nativeScene(); + if (shown() != wasShown) { + reset(); updateSensors(); + } + int amplitude = shown() && layout.rumbleEnabled && !editing && !optionsOpen ? nativeRumble() : 0; + if (vibrator != null && vibrator.hasVibrator()) { + if (amplitude == 0) cancelRumble(); + else if (amplitude != lastAmplitude || SystemClock.uptimeMillis()-lastVibration >= 70) { + try { + vibrator.vibrate(VibrationEffect.createOneShot(110, + vibrator.hasAmplitudeControl() ? amplitude : VibrationEffect.DEFAULT_AMPLITUDE), rumbleAttributes); + lastAmplitude = amplitude; lastVibration = SystemClock.uptimeMillis(); + } catch (RuntimeException e) { cancelRumble(); } + } + } + postDelayed(this, 16); + } + }; + private int lookPointer = -1; + private float lookX, lookY; + private float sensitivity; + private float logicalWidth = 960, logicalHeight = 540; + private static native void nativeLook(float dx, float dy); + private static native void nativeLookReset(); + private int dragPointer = -1, dragControl = -1; + private float dragOffsetX, dragOffsetY; + private float scale = 1, offsetX, offsetY; + private int insetLeft, insetRight, insetTop, insetBottom; + private boolean visible = true; + + private static native void nativeState(int lx, int ly, int rx, int ry, + int lt, int rt, int buttons); + + /** the game's HALO_TOUCH_SCENE bits (port/linux/src/touch_input.c) */ + private static native int nativeScene(); + private static final int SCENE_KNOWN = 1, SCENE_MENUS = 2, SCENE_ON = 4, SCENE_OFF = 8; + private int scene; + private final InputManager inputManager; + private final boolean touchscreenFeature; + private boolean controllerConnected, touchscreen; + + public TouchControls(Context context) { + super(context); + sensors = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + gyroscope = sensors == null ? null : sensors.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (Build.VERSION.SDK_INT >= 31) { + VibratorManager manager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE); + vibrator = manager == null ? null : manager.getDefaultVibrator(); + } else vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); + touchscreenFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); + refreshInputDevices(); + preferences = context.getSharedPreferences("touch-layout-v1", Context.MODE_PRIVATE); + String configuration = preferences.getString("configuration", null); + if (configuration != null) { + try { + TouchLayout.Configuration saved = TouchLayout.importConfiguration(configuration); + layout = saved.layout; sensitivity = saved.sensitivity; + } catch (IllegalArgumentException e) { + Toast.makeText(context, "Saved layout could not be loaded. Using defaults.", Toast.LENGTH_LONG).show(); + } + } else { + for (int i = 0; i < layout.size(); i++) + if (i != TouchLayout.FIRE_LEFT || preferences.getBoolean("swipe-layout", false)) + layout.restore(i, preferences.getFloat("x"+i, layout.x(i)), + preferences.getFloat("y"+i, layout.y(i))); + } + sensitivity = preferences.getFloat("look-sensitivity", sensitivity > 0 ? sensitivity : 1f); + if (!Float.isFinite(sensitivity) || sensitivity < 0.25f || sensitivity > 4f) sensitivity = 1f; + setFocusable(false); + setContentDescription("Halo touch controller"); + setOnApplyWindowInsetsListener((view, insets) -> { + insetLeft = insets.getSystemWindowInsetLeft(); + insetRight = insets.getSystemWindowInsetRight(); + insetTop = insets.getSystemWindowInsetTop(); + insetBottom = insets.getSystemWindowInsetBottom(); + if (android.os.Build.VERSION.SDK_INT >= 28 && insets.getDisplayCutout() != null) { + insetLeft = Math.max(insetLeft, insets.getDisplayCutout().getSafeInsetLeft()); + insetRight = Math.max(insetRight, insets.getDisplayCutout().getSafeInsetRight()); + insetTop = Math.max(insetTop, insets.getDisplayCutout().getSafeInsetTop()); + insetBottom = Math.max(insetBottom, insets.getDisplayCutout().getSafeInsetBottom()); + } + layoutControls(); + return insets; + }); + } + + private void layoutControls() { + if (getWidth() <= 0 || getHeight() <= 0) return; + reset(); + // Keep circles proportional while allowing placement across the complete display. + float width = Math.max(1, getWidth()), height = Math.max(1, getHeight()); + scale = Math.max(0.01f, Math.min(width / 960f, height / 540f)); + offsetX = offsetY = 0; + logicalWidth = width / scale; logicalHeight = height / scale; + layout.bounds(logicalWidth, logicalHeight); + invalidate(); + } + + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { + layoutControls(); + requestApplyInsets(); + } + + public void reset() { + dragPointer = dragControl = -1; + owners.clear(); + buttonTouches.clear(); + lookPointer = -1; + gyroAim.reset(); + nativeLookReset(); + java.util.Arrays.fill(axes, 0); + publish(); + invalidate(); + } + + private void publish() { + if (editing || optionsOpen) { + nativeState(0, 0, 0, 0, 0, 0, 0); + return; + } + int bits = 0; + axes[4] = axes[5] = 0; + for (int i = 0; i < owners.size(); i++) { + int control = owners.valueAt(i); + if (control < 0 || control >= layout.size()) continue; + Button b = buttons[layout.type(control)]; + if (b.bit >= 0) bits |= 1 << b.bit; + if (b.trigger >= 0) axes[b.trigger] = 32767; + } + nativeState(axes[0], axes[1], axes[2], axes[3], axes[4], axes[5], bits); + } + + private boolean held(int control) { + return owners.indexOfValue(control) >= 0; + } + + private static boolean inside(float x, float y, float cx, float cy, float radius) { + return (x-cx)*(x-cx) + (y-cy)*(y-cy) <= radius*radius; + } + + private int hit(float x, float y) { + if (inside(x, y, optionsX(), toolbarY(), 34)) return EDIT; + if (editing && inside(x, y, optionsX()-156, toolbarY(), 34)) return EXPORT; + if (editing && inside(x, y, optionsX()-78, toolbarY(), 34)) return IMPORT; + if (inside(x, y, logicalWidth/2, toolbarY(), 28)) return TOGGLE; + if (!visible) return Integer.MIN_VALUE; + for (int i = 0; i < layout.size(); i++) { + if (!layout.shown(i) || layout.type(i) == LEFT) continue; + Button b = buttons[layout.type(i)]; + if (inside(x, y, layout.x(i), layout.y(i), layout.radius(i))) return i; + } + if (layout.shown(LEFT) && !held(LEFT) && inside(x, y, layout.x(LEFT), layout.y(LEFT), layout.radius(LEFT)*1.28f)) return LEFT; + return Integer.MIN_VALUE; + } + + private void moveStick(int control, float x, float y) { + int axis = 0; + float dx = (x - layout.x(control)) / layout.radius(control); + float dy = (y - layout.y(control)) / layout.radius(control); + float length = (float)Math.sqrt(dx*dx + dy*dy); + if (length < 0.12f) { dx = 0; dy = 0; } + else if (length > 1) { dx /= length; dy /= length; } + axes[axis] = Math.round(dx * 32767); + axes[axis+1] = Math.round(dy * 32767); + } + + /** + * whether the controls are on screen: in a game (no menu or cinematic), + * never without a touchscreen, and with input.touch_controls = "auto" + * only while no controller is connected + */ + private boolean shown() { + if ((scene & SCENE_KNOWN) == 0 || (scene & (SCENE_MENUS | SCENE_OFF)) != 0 || !touchscreen) + return false; + return (scene & SCENE_ON) != 0 || !controllerConnected; + } + + /** a game controller: a stick or a hat, not a phone's few gamepad keys */ + private static boolean isController(InputDevice device) { + if (device == null || device.isVirtual()) return false; + int sources = device.getSources(); + if ((sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) return false; + return device.getMotionRange(MotionEvent.AXIS_X, InputDevice.SOURCE_JOYSTICK) != null + || device.getMotionRange(MotionEvent.AXIS_HAT_X, InputDevice.SOURCE_JOYSTICK) != null; + } + + private void refreshInputDevices() { + boolean controller = false, touch = touchscreenFeature; + for (int id : InputDevice.getDeviceIds()) { + InputDevice device = InputDevice.getDevice(id); + if (device == null) continue; + if (isController(device)) controller = true; + if (!device.isVirtual() && (device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN) + touch = true; + } + boolean wasShown = shown(); + controllerConnected = controller; touchscreen = touch; + if (shown() != wasShown) { + reset(); updateSensors(); + } + } + + @Override public void onInputDeviceAdded(int deviceId) { refreshInputDevices(); } + @Override public void onInputDeviceRemoved(int deviceId) { refreshInputDevices(); } + @Override public void onInputDeviceChanged(int deviceId) { refreshInputDevices(); } + + /** + * whether a finger went down where Android keeps its edge gestures; in + * sticky full screen the first swipe in from an edge only shows the + * system bars, and Android also hands it over as an ordinary finger + */ + private boolean inGestureZone(float x, float y, boolean sidesOnly) { + int[] insets = ((HaloActivity)getContext()).getSystemGestureInsetsPixels(); + if (x < insets[0] || x >= getWidth()-insets[2]) return true; + return !sidesOnly && (y < insets[1] || y >= getHeight()-insets[3]); + } + + @Override public boolean onTouchEvent(MotionEvent event) { + int action = event.getActionMasked(), index = event.getActionIndex(); + int id = event.getPointerId(index); + float x = (event.getX(index)-offsetX)/scale, y = (event.getY(index)-offsetY)/scale; + if (!shown() && !editing && !optionsOpen) { + // the menus and cinematics take the fingers (SDL's surface below) + if (action == MotionEvent.ACTION_DOWN) return false; + if (owners.size() > 0 || lookPointer >= 0) { reset(); invalidate(); } + return true; + } + if (editing) return editTouch(event, action, index, id, x, y); + if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) + && inGestureZone(event.getX(index), event.getY(index), true)) { + return true; + } + if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { + int control = hit(x, y); + if (control == EDIT) { + showOptions(); performClick(); + } else if (control == TOGGLE) { + reset(); visible = !visible; performClick(); + } else if (control != Integer.MIN_VALUE) { + owners.put(id, control); + if (control >= 0 && layout.type(control) == LEFT) moveStick(control, x, y); + else buttonTouches.put(id, new float[]{event.getX(index), event.getY(index)}); + } else if (visible && lookPointer < 0 && !inGestureZone(event.getX(index), event.getY(index), false)) { + lookPointer = id; lookX = event.getX(index); lookY = event.getY(index); + owners.put(id, LOOK); + } + } else if (action == MotionEvent.ACTION_MOVE) { + for (int i = 0; i < event.getPointerCount(); i++) { + int control = owners.get(event.getPointerId(i), Integer.MIN_VALUE); + if (control >= 0 && layout.type(control) == LEFT) + moveStick(control, (event.getX(i)-offsetX)/scale, (event.getY(i)-offsetY)/scale); + int pointer = event.getPointerId(i); + float[] origin = buttonTouches.get(pointer); + if (lookPointer < 0 && origin != null && + Math.hypot(event.getX(i)-origin[0], event.getY(i)-origin[1]) > 10*scale) { + // A held action button can also aim, including either Fire button. + lookPointer = pointer; lookX = origin[0]; lookY = origin[1]; + } + if (pointer == lookPointer) { + float nx = event.getX(i), ny = event.getY(i); + nativeLook((nx-lookX)/scale*sensitivity, (ny-lookY)/scale*sensitivity); + lookX = nx; lookY = ny; + } + } + } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + int control = owners.get(id, Integer.MIN_VALUE); + if (control >= 0 && layout.type(control) == LEFT) { + int axis = 0; + axes[axis] = axes[axis+1] = 0; + } + if (id == lookPointer) lookPointer = -1; + owners.delete(id); + buttonTouches.remove(id); + } else if (action == MotionEvent.ACTION_CANCEL) { + reset(); + } + publish(); invalidate(); + return true; + } + + @Override public boolean performClick() { super.performClick(); return true; } + + private boolean editTouch(MotionEvent event, int action, int index, int id, float x, float y) { + if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { + if (dragPointer < 0) { + int control = hit(x, y); + if (control == EDIT) { + if (saveLayout()) { + reset(); editing = false; performClick(); + } + } else if (control == EXPORT || control == IMPORT) { + reset(); + ((HaloActivity)getContext()).chooseLayoutFile(control == EXPORT, exportLayout()); + } else if (control >= 0) { + dragPointer = id; dragControl = control; + dragOffsetX = x-layout.x(control); dragOffsetY = y-layout.y(control); + } + } + } else if (action == MotionEvent.ACTION_MOVE && dragPointer >= 0) { + int pointer = event.findPointerIndex(dragPointer); + if (pointer >= 0) + layout.move(dragControl, (event.getX(pointer)-offsetX)/scale-dragOffsetX, + (event.getY(pointer)-offsetY)/scale-dragOffsetY); + } else if (action == MotionEvent.ACTION_CANCEL || + ((action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) && id == dragPointer)) { + dragPointer = dragControl = -1; + } + publish(); invalidate(); + return true; + } + + private float toolbarY() { return Math.max(38, insetTop/scale+38); } + private float optionsX() { return logicalWidth-Math.max(42, insetRight/scale+42); } + + private void editorButton(Canvas canvas) { + circle(canvas, optionsX(), toolbarY(), 34, editing ? "Save" : "Options", false); + if (editing) { + circle(canvas, optionsX()-156, toolbarY(), 34, "Export", false); + circle(canvas, optionsX()-78, toolbarY(), 34, "Import", false); + paint.setTextSize(14); + canvas.drawText("Drag controls, then Save and exit", logicalWidth/2, toolbarY()+58, paint); + paint.setTextSize(9); + canvas.drawText("and exit", optionsX(), toolbarY()+17, paint); + } + } + + private void showOptions() { + optionsOpen = true; reset(); + AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("Options") + .setItems(new String[]{"General", "Edit buttons layout", "Look sensitivity"}, (d, which) -> { + if (which == 0) post(this::showGeneral); + else if (which == 1) { editing = true; visible = true; invalidate(); } + else post(this::showSensitivity); + }).setNegativeButton("Close", null).create(); + dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); }); + dialog.show(); + } + + private boolean saveLayout() { + boolean saved = preferences.edit().putString("configuration", exportLayout()) + .putFloat("look-sensitivity", sensitivity).putBoolean("swipe-layout", true).commit(); + if (!saved) Toast.makeText(getContext(), "Could not save layout. Try again.", Toast.LENGTH_LONG).show(); + return saved; + } + + public String exportLayout() { return layout.exportConfiguration(sensitivity); } + + public void importLayout(String text) { + // Parse and save the entire configuration before replacing the live controls. + TouchLayout.Configuration imported = TouchLayout.importConfiguration(text); + imported.layout.bounds(logicalWidth, logicalHeight); + String normalized = imported.layout.exportConfiguration(imported.sensitivity); + if (!preferences.edit().putString("configuration", normalized) + .putFloat("look-sensitivity", imported.sensitivity).putBoolean("swipe-layout", true).commit()) + throw new IllegalArgumentException("Could not save imported layout"); + reset(); layout = imported.layout; sensitivity = imported.sensitivity; + visible = true; updateSensors(); cancelRumble(); invalidate(); + } + + private String controlName(int type) { return type == LEFT ? "Move stick" : buttons[type].label; } + + public void startDeviceInput() { + if (deviceInputActive) return; + deviceInputActive = true; + if (inputManager != null) inputManager.registerInputDeviceListener(this, null); + refreshInputDevices(); + gyroAim.reset(); updateSensors(); post(poll); + } + + public void stopDeviceInput() { + deviceInputActive = false; removeCallbacks(poll); + if (inputManager != null) inputManager.unregisterInputDeviceListener(this); + updateSensors(); cancelRumble(); reset(); + } + + private void updateSensors() { + boolean needed = deviceInputActive && shown() && layout.gyroscopeEnabled && gyroscope != null; + if (needed && !gyroRegistered) { + gyroAim.reset(); + gyroRegistered = sensors.registerListener(this, gyroscope, SensorManager.SENSOR_DELAY_GAME); + } else if (!needed && gyroRegistered) { + sensors.unregisterListener(this); gyroRegistered = false; gyroAim.reset(); + } + } + + private void cancelRumble() { + if (lastAmplitude != 0 && vibrator != null) { + try { vibrator.cancel(); } catch (RuntimeException ignored) {} + } + lastAmplitude = 0; + } + + @Override public void onSensorChanged(SensorEvent event) { + if (!deviceInputActive || !shown() || !layout.gyroscopeEnabled || editing || optionsOpen) { + gyroAim.reset(); return; + } + int rotation = ((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay().getRotation(); + if (gyroAim.sample(event.timestamp, event.values[0], event.values[1], rotation, gyroDelta)) + // The existing direct-look path accepts logical pixels (0.0022 radians per pixel). + nativeLook(-gyroDelta[0]/0.0022f*sensitivity, -gyroDelta[1]/0.0022f*sensitivity); + } + + @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {} + + private void showGeneral() { + optionsOpen = true; reset(); cancelRumble(); + LinearLayout panel = new LinearLayout(getContext()); panel.setOrientation(LinearLayout.VERTICAL); + panel.setPadding(24, 16, 24, 16); + Switch rumble = new Switch(getContext()); + boolean hasRumble = vibrator != null && vibrator.hasVibrator(); + rumble.setText(hasRumble ? "Rumble" : "Rumble (unavailable on this phone)"); + rumble.setChecked(layout.rumbleEnabled); rumble.setEnabled(hasRumble); + rumble.setOnCheckedChangeListener((button, enabled) -> { + layout.rumbleEnabled = enabled; cancelRumble(); saveLayout(); + }); + Switch gyro = new Switch(getContext()); + gyro.setText("Gyroscope aim (Experimental)"+(gyroscope == null ? " - unavailable on this phone" : "")); + gyro.setChecked(layout.gyroscopeEnabled); gyro.setEnabled(gyroscope != null); + gyro.setOnCheckedChangeListener((button, enabled) -> { + layout.gyroscopeEnabled = enabled; reset(); updateSensors(); saveLayout(); + }); + panel.addView(rumble); panel.addView(gyro); + AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("General") + .setView(panel).setPositiveButton("Done", null).create(); + android.widget.Button manage = new android.widget.Button(getContext()); + manage.setText("Hide or add buttons"); + manage.setOnClickListener(v -> { dialog.dismiss(); post(this::showButtonManager); }); + panel.addView(manage); + android.widget.Button sizes = new android.widget.Button(getContext()); + sizes.setText("Edit buttons size"); + sizes.setOnClickListener(v -> { dialog.dismiss(); post(this::showButtonSizes); }); + panel.addView(sizes); + dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); updateSensors(); }); dialog.show(); + } + + private void showButtonSizes() { + optionsOpen = true; reset(); + LinearLayout list = new LinearLayout(getContext()); list.setOrientation(LinearLayout.VERTICAL); + for (int i = 0; i < layout.size(); i++) { + final int control = i; + LinearLayout row = new LinearLayout(getContext()); row.setPadding(16, 4, 16, 4); + TextView label = new TextView(getContext()); label.setTextColor(Color.WHITE); + label.setText(controlName(layout.type(i))+(i >= TouchLayout.BASE_COUNT ? " (copy)" : "") + +(!layout.shown(i) ? " (hidden)" : "")); + row.addView(label, new LinearLayout.LayoutParams(0, -2, 1)); + android.widget.Button minus = new android.widget.Button(getContext()); minus.setText("-"); + TextView value = new TextView(getContext()); value.setTextColor(Color.WHITE); + android.widget.Button plus = new android.widget.Button(getContext()); plus.setText("+"); + Runnable refresh = () -> { + int percent = Math.round(layout.sizeScale(control)*100); + value.setText(percent+"%"); minus.setEnabled(percent > 50); plus.setEnabled(percent < 200); + }; + minus.setOnClickListener(v -> { + layout.setSize(control, Math.max(50, Math.round(layout.sizeScale(control)*100)-10)/100f); + saveLayout(); refresh.run(); invalidate(); + }); + plus.setOnClickListener(v -> { + layout.setSize(control, Math.min(200, Math.round(layout.sizeScale(control)*100)+10)/100f); + saveLayout(); refresh.run(); invalidate(); + }); + row.addView(minus); row.addView(value); row.addView(plus); + list.addView(row); refresh.run(); + } + ScrollView scroll = new ScrollView(getContext()); scroll.addView(list); + AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("Edit buttons size") + .setView(scroll).setPositiveButton("Done", null).create(); + dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); }); dialog.show(); + } + + private void showButtonManager() { + optionsOpen = true; reset(); + LinearLayout list = new LinearLayout(getContext()); list.setOrientation(LinearLayout.VERTICAL); + Runnable[] refresh = new Runnable[1]; + refresh[0] = () -> { + list.removeAllViews(); + for (int i = 0; i < layout.size(); i++) { + final int control = i; + LinearLayout row = new LinearLayout(getContext()); row.setPadding(16, 4, 16, 4); + TextView label = new TextView(getContext()); label.setTextColor(Color.WHITE); + label.setText(controlName(layout.type(i))+(i >= TouchLayout.BASE_COUNT ? " (copy)" : "")); + row.addView(label, new LinearLayout.LayoutParams(0, -2, 1)); + android.widget.Button toggle = new android.widget.Button(getContext()); + toggle.setText(layout.shown(i) ? "Hide" : "Show"); + toggle.setOnClickListener(v -> { + reset(); layout.setShown(control, !layout.shown(control)); + saveLayout(); refresh[0].run(); invalidate(); + }); + row.addView(toggle); + if (layout.type(i) != LEFT) { + android.widget.Button copy = new android.widget.Button(getContext()); copy.setText("Duplicate"); + copy.setOnClickListener(v -> { + reset(); + if (layout.duplicate(control) < 0) + Toast.makeText(getContext(), "Maximum 64 controls. Reset to remove copies.", Toast.LENGTH_LONG).show(); + else saveLayout(); + refresh[0].run(); invalidate(); + }); + row.addView(copy); + } + list.addView(row); + } + }; + refresh[0].run(); + ScrollView scroll = new ScrollView(getContext()); scroll.addView(list); + AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("Hide or add buttons") + .setView(scroll).setPositiveButton("Done", null).setNegativeButton("Reset", null) + .setNeutralButton("Add button", null).create(); + dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); }); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v -> { + new AlertDialog.Builder(getContext()).setTitle("Reset buttons?") + .setMessage("Restore original positions, show all default controls and remove duplicates?") + .setNegativeButton("Cancel", null).setPositiveButton("Reset", (d, which) -> { + reset(); layout.resetDefaults(); visible = true; saveLayout(); + refresh[0].run(); invalidate(); + }).show(); + }); + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + String[] names = new String[TouchLayout.BASE_COUNT]; + for (int i = 0; i < names.length; i++) names[i] = controlName(i); + new AlertDialog.Builder(getContext()).setTitle("Add button").setItems(names, (d, type) -> { + reset(); + if (layout.add(type) < 0) + Toast.makeText(getContext(), type == LEFT ? "Move stick is already visible." : "Maximum 64 controls.", Toast.LENGTH_LONG).show(); + else saveLayout(); + refresh[0].run(); invalidate(); + }).setNegativeButton("Cancel", null).show(); + }); + } + + private void showSensitivity() { + optionsOpen = true; reset(); + LinearLayout panel = new LinearLayout(getContext()); panel.setOrientation(LinearLayout.VERTICAL); + panel.setPadding(32, 16, 32, 16); + TextView value = new TextView(getContext()); + SeekBar slider = new SeekBar(getContext()); slider.setMax(150); + slider.setProgress(Math.round((sensitivity-0.25f)/0.025f)); + value.setText(String.format(java.util.Locale.US, "Look sensitivity: %.2fx", sensitivity)); + slider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + public void onProgressChanged(SeekBar bar, int progress, boolean user) { + sensitivity = 0.25f+progress*0.025f; + value.setText(String.format(java.util.Locale.US, "Look sensitivity: %.2fx", sensitivity)); + saveLayout(); + } + public void onStartTrackingTouch(SeekBar bar) {} + public void onStopTrackingTouch(SeekBar bar) {} + }); + panel.addView(value); panel.addView(slider); + AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("Look sensitivity") + .setView(panel).setPositiveButton("Done", null).create(); + dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); }); dialog.show(); + } + + private void circle(Canvas canvas, float x, float y, float radius, String label, boolean active) { + circle(canvas, x, y, radius, label, active, 11); + } + + private void circle(Canvas canvas, float x, float y, float radius, String label, boolean active, float textSize) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(active ? 0x9983d9ff : 0x55304050); + canvas.drawCircle(x, y, radius, paint); + paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(2); + paint.setColor(active ? 0xffaee7ff : 0xffffffff); + canvas.drawCircle(x, y, radius, paint); + paint.setStyle(Paint.Style.FILL); paint.setColor(Color.WHITE); + paint.setTextAlign(Paint.Align.CENTER); paint.setTextSize(textSize); + canvas.drawText(label, x, y + textSize*0.36f, paint); + } + + private void stick(Canvas canvas, int axis, float x, float y, String label) { + circle(canvas, x, y, layout.radius(LEFT), label, false, 11*layout.sizeScale(LEFT)); + circle(canvas, x + axes[axis]/32767f*layout.radius(LEFT), y + axes[axis+1]/32767f*layout.radius(LEFT), + 24*layout.sizeScale(LEFT), "", held(LEFT)); + } + + @Override protected void onDraw(Canvas canvas) { + if (!shown() && !editing) return; + canvas.save(); canvas.translate(offsetX, offsetY); canvas.scale(scale, scale); + if (!editing) circle(canvas, logicalWidth/2, toolbarY(), 28, visible ? "Hide" : "Touch", false); + if (visible) { + if (layout.shown(LEFT)) stick(canvas, 0, layout.x(LEFT), layout.y(LEFT), "Move"); + for (int i = 0; i < layout.size(); i++) { + if (!layout.shown(i) || layout.type(i) == LEFT) continue; + Button b = buttons[layout.type(i)]; + circle(canvas, layout.x(i), layout.y(i), layout.radius(i), b.label, held(i) || dragControl == i, 11*layout.sizeScale(i)); + } + } + editorButton(canvas); + canvas.restore(); + } +} diff --git a/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java b/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java new file mode 100644 index 00000000..7722a3a0 --- /dev/null +++ b/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java @@ -0,0 +1,189 @@ +package com.halo.decomp; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Properties; + +/** Display-relative positions, visibility and action types for touch controls. */ +final class TouchLayout { + static final int LEFT = 16, FIRE_LEFT = 17; + private static final float[][] DEFAULTS = { + {856,447}, {913,377}, {794,377}, {850,312}, {915,239}, {802,239}, + {236,449}, {691,449}, {360,490}, {438,490}, {570,36}, {390,36}, + {100,237}, {100,335}, {51,286}, {149,286}, {115,440}, {255,239} + }; + private static final float[] RADII = { + 36,32,34,32,39,35,32,32,27,29,28,28,25,25,25,25,64,39 + }; + static final int BASE_COUNT = 18, MAX_CONTROLS = 64; + private static final class Control { + final int type; + float x, y; + boolean shown = true; + float size = 1f; + Control(int type, float x, float y) { this.type = type; this.x = x; this.y = y; } + } + private final ArrayList controls = new ArrayList<>(); + + private float width = 960, height = 540; + boolean rumbleEnabled = true, gyroscopeEnabled = false; + static final float MIN_SIZE = 0.5f, MAX_SIZE = 2f; + + void bounds(float width, float height) { + if (!Float.isFinite(width) || !Float.isFinite(height) || width <= 0 || height <= 0) + throw new IllegalArgumentException("Invalid display dimensions"); + for (Control control : controls) { + control.x *= width / this.width; + control.y *= height / this.height; + } + this.width = width; this.height = height; + for (int i = 0; i < size(); i++) move(i, x(i), y(i)); + } + float savedX(int i) { return x(i) * 960 / width; } + float savedY(int i) { return y(i) * 540 / height; } + + TouchLayout() { resetDefaults(); } + int size() { return controls.size(); } + int type(int control) { return controls.get(control).type; } + float x(int control) { return controls.get(control).x; } + float y(int control) { return controls.get(control).y; } + float radius(int control) { return RADII[type(control)] * controls.get(control).size; } + float sizeScale(int control) { return controls.get(control).size; } + void setSize(int control, float size) { + if (!Float.isFinite(size) || size < MIN_SIZE || size > MAX_SIZE) + throw new IllegalArgumentException("Button size must be between 50% and 200%"); + controls.get(control).size = size; + move(control, x(control), y(control)); + } + boolean shown(int control) { return controls.get(control).shown; } + void setShown(int control, boolean shown) { controls.get(control).shown = shown; } + + void resetDefaults() { + controls.clear(); + for (int i = 0; i < DEFAULTS.length; i++) + controls.add(new Control(i, DEFAULTS[i][0]*width/960, DEFAULTS[i][1]*height/540)); + for (int i = 0; i < size(); i++) move(i, x(i), y(i)); + } + + int duplicate(int source) { + if (size() >= MAX_CONTROLS || type(source) == LEFT) return -1; + Control added = new Control(type(source), x(source)+radius(source)*2+12, y(source)); + added.size = sizeScale(source); + controls.add(added); + int index = size()-1; + // If there is no space on the right, place the copy to the left instead. + if (added.x > width-radius(source)) added.x = x(source)-radius(source)*2-12; + move(index, added.x, added.y); + return index; + } + + int add(int type) { + if (type < 0 || type >= BASE_COUNT) return -1; + for (int i = 0; i < size(); i++) { + if (type(i) == type && !shown(i)) { setShown(i, true); return i; } + } + return duplicate(type); + } + + String exportConfiguration(float sensitivity) { + if (!validSensitivity(sensitivity)) throw new IllegalArgumentException("Invalid sensitivity"); + Properties values = new Properties(); + values.setProperty("format", "halo-touch-layout"); + values.setProperty("version", "2"); + values.setProperty("rumble", Boolean.toString(rumbleEnabled)); + values.setProperty("gyroscope", Boolean.toString(gyroscopeEnabled)); + values.setProperty("count", Integer.toString(size())); + values.setProperty("sensitivity", Float.toString(sensitivity)); + for (int i = 0; i < size(); i++) { + String key = "control."+i+"."; + values.setProperty(key+"type", Integer.toString(type(i))); + values.setProperty(key+"x", Float.toString(savedX(i))); + values.setProperty(key+"y", Float.toString(savedY(i))); + values.setProperty(key+"visible", Boolean.toString(shown(i))); + values.setProperty(key+"size", Float.toString(sizeScale(i))); + } + StringWriter text = new StringWriter(); + try { values.store(text, "Halo Android touch layout"); } + catch (IOException e) { throw new IllegalStateException(e); } + return text.toString(); + } + + static final class Configuration { + final TouchLayout layout; + final float sensitivity; + Configuration(TouchLayout layout, float sensitivity) { + this.layout = layout; this.sensitivity = sensitivity; + } + } + + private static boolean validSensitivity(float value) { + return Float.isFinite(value) && value >= 0.25f && value <= 4f; + } + + static Configuration importConfiguration(String text) { + if (text == null || text.length() > 65536) throw new IllegalArgumentException("Invalid layout file"); + Properties values = new Properties(); + try { + values.load(new StringReader(text)); + String version = values.getProperty("version"); + if (!"halo-touch-layout".equals(values.getProperty("format")) || + !("1".equals(version) || "2".equals(version))) + throw new IllegalArgumentException("Unsupported layout file"); + int count = Integer.parseInt(values.getProperty("count")); + float sensitivity = Float.parseFloat(values.getProperty("sensitivity")); + if (count < BASE_COUNT || count > MAX_CONTROLS || !validSensitivity(sensitivity)) + throw new IllegalArgumentException("Invalid layout settings"); + TouchLayout layout = new TouchLayout(); + layout.controls.clear(); + if ("2".equals(version)) { + layout.rumbleEnabled = readBoolean(values, "rumble"); + layout.gyroscopeEnabled = readBoolean(values, "gyroscope"); + } + for (int i = 0; i < count; i++) { + String key = "control."+i+"."; + int type = Integer.parseInt(values.getProperty(key+"type")); + float x = Float.parseFloat(values.getProperty(key+"x")); + float y = Float.parseFloat(values.getProperty(key+"y")); + String shown = values.getProperty(key+"visible"); + float size = "2".equals(version) ? Float.parseFloat(values.getProperty(key+"size")) : 1f; + if (type < 0 || type >= BASE_COUNT || (i < BASE_COUNT && type != i) || + (i >= BASE_COUNT && type == LEFT) || !Float.isFinite(x) || !Float.isFinite(y) || + x < 0 || x > 960 || y < 0 || y > 540 || + !("true".equals(shown) || "false".equals(shown)) || + !Float.isFinite(size) || size < MIN_SIZE || size > MAX_SIZE) + throw new IllegalArgumentException("Invalid control in layout file"); + Control control = new Control(type, x, y); + control.shown = Boolean.parseBoolean(shown); + control.size = size; + layout.controls.add(control); + } + return new Configuration(layout, sensitivity); + } catch (IOException | NullPointerException e) { + throw new IllegalArgumentException("Incomplete layout file", e); + } + } + + private static boolean readBoolean(Properties values, String key) { + String value = values.getProperty(key); + if (!("true".equals(value) || "false".equals(value))) + throw new IllegalArgumentException("Invalid "+key+" option"); + return Boolean.parseBoolean(value); + } + + void move(int control, float x, float y) { + if (!Float.isFinite(x) || !Float.isFinite(y)) return; + float radius = radius(control); + controls.get(control).x = Math.max(radius, Math.min(width-radius, x)); + controls.get(control).y = Math.max(radius, Math.min(height-radius, y)); + } + + void restore(int control, float x, float y) { + if (Float.isFinite(x) && Float.isFinite(y) && x >= 0 + && x <= 960 && y >= 0 && y <= 540) { + controls.get(control).x = x; + controls.get(control).y = y; + } + } +} diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h index ca9151c1..a1d3dac8 100644 --- a/port/android/guest/runtime/guest_host.h +++ b/port/android/guest/runtime/guest_host.h @@ -117,4 +117,16 @@ void host_android_path(int which, char *buffer, unsigned int size); in pixels of the current orientation, into insets[4]; all 0 when unknown */ void host_gesture_insets(int *insets); +/* ---------- the on-screen touch controls (host_touch.c) */ + +/* the overlay's controller: the SDL axes (left x, y, right x, y, left +trigger, right trigger) and the SDL button bits, into state[7] */ +void host_touch_read(int *state); +/* the overlay's view swipe since the last read, into delta[2] */ +void host_touch_look_read(float *delta); +/* port 0's motors, for the phone's vibration */ +void host_touch_rumble(unsigned int low, unsigned int high); +/* tells the overlay when to show: HALO_TOUCH_SCENE_* (xinput_sdl.c) */ +void host_touch_scene(int scene); + #endif diff --git a/port/android/host/host_touch.c b/port/android/host/host_touch.c new file mode 100644 index 00000000..8cf73af5 --- /dev/null +++ b/port/android/host/host_touch.c @@ -0,0 +1,173 @@ +/* +HOST_TOUCH.C + +The on-screen touch controls between the overlay and the game. The overlay +(app/.../TouchControls.java) is an Android view over SDL's surface: it +hands its stick, buttons and view swipes over JNI on the UI thread, and the +game reads them through host imports when it reads port 0's controller +(port/linux/src/xinput_sdl.c). The game hands back what the overlay needs +to know: whether a menu or a cinematic is up and the input.touch_controls +setting (host_touch_scene), and how hard port 0 rumbles. +*/ + +#include +#include +#include +#include +#include + +/* a press shorter than this still reaches the game: it reads the +controller once a frame and acts at 30 ticks a second, and a quick tap can +go down and up between two reads */ +#define TOUCH_TAP_NS 60000000LL +/* view motion and rumble older than this are dropped: left over from the +menus, a cinematic or the app in the background */ +#define TOUCH_STALE_NS 150000000LL + +/* SDL's 15 gamepad buttons, then the left and right triggers */ +#define TOUCH_INPUTS 17 + +static pthread_mutex_t touch_lock = PTHREAD_MUTEX_INITIALIZER; +/* the SDL axes (left x, y, right x, y, left trigger, right trigger), then +the SDL button bits */ +static int32_t touch_state[7]; +static int64_t touch_pressed_ns[TOUCH_INPUTS]; +static float look_delta[2]; +static int64_t look_ns; +static int rumble_amplitude; +static int64_t rumble_ns; +static volatile int touch_scene; + +static int64_t now_ns(void) +{ + struct timespec now; + + clock_gettime(CLOCK_MONOTONIC, &now); + return (int64_t)now.tv_sec * 1000000000LL + now.tv_nsec; +} + +/* the inputs of a state as bits: buttons, then the triggers */ +static uint32_t touch_inputs(const int32_t *state) +{ + return ((uint32_t)state[6] & 0x7fff) | (state[4] ? 1u << 15 : 0) | (state[5] ? 1u << 16 : 0); +} + +JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeState( + JNIEnv *env, jclass cls, jint lx, jint ly, jint rx, jint ry, + jint lt, jint rt, jint buttons) +{ + int32_t next[] = { lx, ly, rx, ry, lt, rt, buttons }; + uint32_t pressed; + int64_t now = now_ns(); + int input; + + (void)env; + (void)cls; + pthread_mutex_lock(&touch_lock); + pressed = touch_inputs(next) & ~touch_inputs(touch_state); + for (input = 0; input < TOUCH_INPUTS; input++) + { + if (pressed & (1u << input)) + touch_pressed_ns[input] = now; + } + memcpy(touch_state, next, sizeof(next)); + pthread_mutex_unlock(&touch_lock); +} + +/* the guest's: the overlay's controller state, with each press held for +at least TOUCH_TAP_NS */ +void host_touch_read(int32_t *state) +{ + int64_t now = now_ns(); + int input; + + pthread_mutex_lock(&touch_lock); + memcpy(state, touch_state, sizeof(touch_state)); + for (input = 0; input < TOUCH_INPUTS; input++) + { + if (touch_pressed_ns[input] && now - touch_pressed_ns[input] < TOUCH_TAP_NS) + { + if (input < 15) + state[6] |= 1 << input; + else if (!state[4 + input - 15]) + state[4 + input - 15] = 32767; + } + } + pthread_mutex_unlock(&touch_lock); +} + +JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeLook( + JNIEnv *env, jclass cls, jfloat dx, jfloat dy) +{ + (void)env; + (void)cls; + pthread_mutex_lock(&touch_lock); + look_delta[0] += dx; + look_delta[1] += dy; + look_ns = now_ns(); + pthread_mutex_unlock(&touch_lock); +} + +JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeLookReset(JNIEnv *env, jclass cls) +{ + (void)env; + (void)cls; + pthread_mutex_lock(&touch_lock); + look_delta[0] = look_delta[1] = 0.0f; + pthread_mutex_unlock(&touch_lock); +} + +/* the guest's: the view swipe since the last read, in the overlay's +logical pixels with its sensitivity applied */ +void host_touch_look_read(float *delta) +{ + int64_t now = now_ns(); + + pthread_mutex_lock(&touch_lock); + memcpy(delta, look_delta, sizeof(look_delta)); + if (now - look_ns > TOUCH_STALE_NS) + delta[0] = delta[1] = 0.0f; + look_delta[0] = look_delta[1] = 0.0f; + pthread_mutex_unlock(&touch_lock); +} + +/* the guest's: port 0's motors (XInputSetState), each 0..65535; the game +sends them every frame, and zero while paused or with the profile's +vibration off */ +void host_touch_rumble(unsigned int low, unsigned int high) +{ + unsigned int strength = low > high ? low : high; + + pthread_mutex_lock(&touch_lock); + rumble_amplitude = strength ? 64 + (int)(strength * 191u / 65535u) : 0; + rumble_ns = now_ns(); + pthread_mutex_unlock(&touch_lock); +} + +/* the phone's vibration strength, 0 or 64..255 */ +JNIEXPORT jint JNICALL Java_com_halo_decomp_TouchControls_nativeRumble(JNIEnv *env, jclass cls) +{ + int amplitude; + + (void)env; + (void)cls; + pthread_mutex_lock(&touch_lock); + amplitude = now_ns() - rumble_ns > TOUCH_STALE_NS ? 0 : rumble_amplitude; + pthread_mutex_unlock(&touch_lock); + return amplitude; +} + +/* the guest's, at every read of port 0: HALO_TOUCH_SCENE_* bits +(port/linux/src/xinput_sdl.c) */ +void host_touch_scene(int scene) +{ + touch_scene = scene; +} + +/* 0 until the game has read its controller once */ +JNIEXPORT jint JNICALL Java_com_halo_decomp_TouchControls_nativeScene(JNIEnv *env, jclass cls) +{ + (void)env; + (void)cls; + return touch_scene; +} diff --git a/port/android/host_imports.list b/port/android/host_imports.list index b14f8a3e..d1674a02 100644 --- a/port/android/host_imports.list +++ b/port/android/host_imports.list @@ -55,5 +55,9 @@ host_gl_fence_frame host_gl_wait_frame host_android_path host_gesture_insets +host_touch_read +host_touch_look_read +host_touch_rumble +host_touch_scene host_errno diff --git a/port/linux/game/touch_game.c b/port/linux/game/touch_game.c index eaa8d336..46ea64c9 100644 --- a/port/linux/game/touch_game.c +++ b/port/linux/game/touch_game.c @@ -20,3 +20,10 @@ int touch_game_cinematic_skippable(void) return cinematic_globals && game_in_progress() && cinematic_in_progress() && cinematic_can_be_skipped(); } + +/* asks whether a cinematic is playing, skippable or not: the player has no +controls, so the on-screen touch controls hide (touch_input.c) */ +int touch_game_cinematic_playing(void) +{ + return cinematic_globals && game_in_progress() && cinematic_in_progress(); +} diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 55865b1f..0ed73782 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -190,6 +190,11 @@ static const struct config_setting config_settings[] = "loose_sounds_reload reads the files again and loose_sounds false gives\n" "the map's sounds back." }, + { "input.touch_controls", _config_string, "\"auto\"", "HALO_TOUCH_CONTROLS", _environment_value, _platform_android, + "The on-screen touch controls in a game: \"auto\" shows them on a\n" + "touchscreen while no controller is connected, \"on\" also with a\n" + "controller, \"off\" never. A device without a touchscreen never shows\n" + "them. The menus take taps in any case." }, { "input.mouse_sensitivity", _config_real, "1.0", "HALO_MOUSE_SENSITIVITY", _environment_value, _platform_desktop, "How far the view turns for the mouse's movement." }, { "input.invert_mouse", _config_boolean, "false", "HALO_MOUSE_INVERT", _environment_set_is_true, _platform_desktop, diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index 657936b8..f87a8849 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -11,16 +11,23 @@ finger down (host_gesture_insets). #include "touch_input.h" #include "touch_menu.h" +#include "port_config.h" #include +#include #ifdef HALO_ANDROID /* port/android/guest/runtime/guest_host.h */ void host_gesture_insets(int *insets); +void host_touch_read(int *state); +void host_touch_look_read(float *delta); +void host_touch_rumble(unsigned int low, unsigned int high); +void host_touch_scene(int scene); #endif /* the game's (port/linux/game/touch_game.c) */ int touch_game_cinematic_skippable(void); +int touch_game_cinematic_playing(void); /* a tap moves at most this far; a drag of this length is one wheel step */ #define TOUCH_TAP_SLOP_DP 12.0f @@ -176,3 +183,111 @@ void touch_input_gamepad(XINPUT_GAMEPAD *pad) skip_polls--; } } + +#ifdef HALO_ANDROID +/* ---------- the on-screen touch controls + +The overlay (port/android/app/.../TouchControls.java) draws and reads its +own fingers on Android's UI thread; port/android/host/host_touch.c hands +its state over. These bits tell it when to show: */ +enum +{ + /* the game has read its controller: the other bits are known */ + _touch_scene_known = 1 << 0, + /* a menu or a cinematic is up: the overlay hides and lets the fingers + through to the menus' pointer above */ + _touch_scene_menus = 1 << 1, + /* input.touch_controls: "on" and "off" (none: "auto", shown when the + device has a touchscreen and no controller) */ + _touch_scene_on = 1 << 2, + _touch_scene_off = 1 << 3, +}; + +/* input.touch_controls as _touch_scene_on, _touch_scene_off or 0 */ +static int touch_controls_setting(void) +{ + static int setting; + static unsigned long read_at = (unsigned long)-1; + + if (read_at != config_changes()) + { + const char *value = config_string("input.touch_controls"); + + read_at = config_changes(); + setting = 0; + if (value && !strcmp(value, "on")) + setting = _touch_scene_on; + else if (value && !strcmp(value, "off")) + setting = _touch_scene_off; + } + return setting; +} + +void touch_input_controls(XINPUT_GAMEPAD *pad, int menus) +{ + /* SDL's gamepad buttons, in order (SDL_GamepadButton); the first four + are the analog A, B, X and Y */ + static const WORD digital[] = + { + 0, 0, 0, 0, XINPUT_GAMEPAD_BACK, 0, XINPUT_GAMEPAD_START, + XINPUT_GAMEPAD_LEFT_THUMB, XINPUT_GAMEPAD_RIGHT_THUMB, + 0, 0, XINPUT_GAMEPAD_DPAD_UP, XINPUT_GAMEPAD_DPAD_DOWN, + XINPUT_GAMEPAD_DPAD_LEFT, XINPUT_GAMEPAD_DPAD_RIGHT + }; + static const int analog[] = + { + XINPUT_GAMEPAD_A, XINPUT_GAMEPAD_B, XINPUT_GAMEPAD_X, XINPUT_GAMEPAD_Y + }; + SHORT *sticks[4]; + int state[7]; + int index; + + host_touch_scene(_touch_scene_known | touch_controls_setting() | + (menus || touch_game_cinematic_playing() ? _touch_scene_menus : 0)); + sticks[0] = &pad->sThumbLX; + sticks[1] = &pad->sThumbLY; + sticks[2] = &pad->sThumbRX; + sticks[3] = &pad->sThumbRY; + host_touch_read(state); + for (index = 0; index < 4; index++) + { + /* SDL's y runs down, the Xbox's up */ + int value = index == 1 || index == 3 ? -state[index] - 1 : state[index]; + + if (value < -32768) value = -32768; + if (value > 32767) value = 32767; + if (abs(value) > abs(*sticks[index])) + *sticks[index] = (SHORT)value; + if (state[6] & (1 << index)) + pad->bAnalogButtons[analog[index]] = 0xff; + } + for (index = 0; index < (int)(sizeof(digital) / sizeof(digital[0])); index++) + { + if (state[6] & (1 << index)) + pad->wButtons |= digital[index]; + } + /* the shoulders are white and black, as a DualSense's L1 and R1 */ + if (state[6] & (1 << 9)) + pad->bAnalogButtons[XINPUT_GAMEPAD_WHITE] = 0xff; + if (state[6] & (1 << 10)) + pad->bAnalogButtons[XINPUT_GAMEPAD_BLACK] = 0xff; + if (state[4]) + pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = 0xff; + if (state[5]) + pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = 0xff; +} + +void touch_input_look(float scale, float *yaw, float *pitch) +{ + float delta[2]; + + host_touch_look_read(delta); + *yaw -= delta[0] * scale; + *pitch -= delta[1] * scale; +} + +void touch_input_rumble(unsigned int left, unsigned int right) +{ + host_touch_rumble(left, right); +} +#endif diff --git a/port/linux/src/touch_input.h b/port/linux/src/touch_input.h index 2ce8efb2..e4df652a 100644 --- a/port/linux/src/touch_input.h +++ b/port/linux/src/touch_input.h @@ -35,4 +35,18 @@ void touch_input_menu_read(struct platform_ui_pointer *pointer); presses A for a few polls when a cinematic can be skipped */ void touch_input_gamepad(XINPUT_GAMEPAD *pad); +#ifdef HALO_ANDROID +/* the on-screen touch controls (port/android/app/.../TouchControls.java), +an Android view over the game: tells them whether a menu is up and +input.touch_controls, and adds their stick and buttons to port 0's state */ +void touch_input_controls(XINPUT_GAMEPAD *pad, int menus); + +/* adds the touch controls' view swipe since the last call to yaw and +pitch, in radians at `scale` radians per pixel */ +void touch_input_look(float scale, float *yaw, float *pitch); + +/* port 0's motors, for the phone's vibration while the touch controls show */ +void touch_input_rumble(unsigned int left, unsigned int right); +#endif + #endif diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index 3dd46aed..ea553c8d 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -147,10 +147,15 @@ int halo_linux_mouse_look(short gamepad_index, float *yaw, float *pitch) mouse_pending_y = 0.0f; mouse_polls_unconsumed = 0; pthread_mutex_unlock(&mouse_lock); +#ifdef HALO_ANDROID + /* the touch controls' swipe; it is not the mouse's aiming + (halo_linux_mouse_aiming), so a thumb keeps the stick's magnetism */ + touch_input_look(scale, yaw, pitch); +#endif if (x == 0.0f && y == 0.0f) - return FALSE; - *yaw = -x * scale * mouse_sensitivity(); - *pitch = (invert ? y : -y) * scale * vertical_sensitivity; + return *yaw != 0.0f || *pitch != 0.0f; + *yaw += -x * scale * mouse_sensitivity(); + *pitch += (invert ? y : -y) * scale * vertical_sensitivity; return TRUE; } @@ -916,6 +921,9 @@ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state) sdl_gamepad_state(gamepads[0], &state->Gamepad); test_input_gamepad(&state->Gamepad); touch_input_gamepad(&state->Gamepad); +#ifdef HALO_ANDROID + touch_input_controls(&state->Gamepad, input.menus); +#endif if (abs(state->Gamepad.sThumbRX) > STICK_AIMING_DEFLECTION || abs(state->Gamepad.sThumbRY) > STICK_AIMING_DEFLECTION) { @@ -949,6 +957,11 @@ DWORD WINAPI XInputSetState(HANDLE device, PXINPUT_FEEDBACK feedback) feedback->Header.dwStatus = ERROR_SUCCESS; if (port < 0) return ERROR_DEVICE_NOT_CONNECTED; +#ifdef HALO_ANDROID + /* the phone vibrates for the touch controls' player */ + if (port == 0) + touch_input_rumble(feedback->Rumble.wLeftMotorSpeed, feedback->Rumble.wRightMotorSpeed); +#endif count = sdl_gamepads(gamepads); if (port_gamepad(gamepads, count, port)) { diff --git a/tools/test_touch_layout.py b/tools/test_touch_layout.py new file mode 100644 index 00000000..761166b1 --- /dev/null +++ b/tools/test_touch_layout.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Compile and run the touch layout regression checks with JDK 17+.""" +import subprocess +from pathlib import Path + +root = Path(__file__).resolve().parent.parent +classes = root / "build/touch-layout-tests" +classes.mkdir(parents=True, exist_ok=True) +subprocess.run([ + "javac", "-d", str(classes), + str(root / "port/android/app/src/main/java/com/halo/decomp/TouchLayout.java"), + str(root / "port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java"), + str(root / "tools/tests/GyroscopeAimTest.java"), + str(root / "tools/tests/TouchLayoutTest.java") +], check=True) +subprocess.run(["java", "-cp", str(classes), "com.halo.decomp.TouchLayoutTest"], check=True) + +subprocess.run(["java", "-cp", str(classes), "com.halo.decomp.GyroscopeAimTest"], check=True) diff --git a/tools/tests/GyroscopeAimTest.java b/tools/tests/GyroscopeAimTest.java new file mode 100644 index 00000000..afecc4ae --- /dev/null +++ b/tools/tests/GyroscopeAimTest.java @@ -0,0 +1,34 @@ +package com.halo.decomp; + +public final class GyroscopeAimTest { + private static void check(boolean ok, String message) { + if (!ok) throw new AssertionError(message); + } + private static void near(float value, float expected, String message) { + check(Math.abs(value-expected) < 0.000001f, message); + } + public static void main(String[] args) { + GyroscopeAim aim = new GyroscopeAim(); float[] delta = new float[2]; + check(!aim.sample(1000000000L, 1, 0, 1, delta), "First event must only anchor time"); + check(aim.sample(1020000000L, 1, 0, 1, delta), "Landscape yaw must produce motion"); + near(delta[0], 0.02f, "Yaw integrates angular velocity in radians"); + near(delta[1], 0, "Pure yaw does not pitch"); + check(!aim.sample(1040000000L, 1, 0, 3, delta), "Changing orientation must reset the sample"); + aim.sample(1060000000L, 1, 0, 3, delta); + near(delta[0], -0.02f, "Reverse landscape must reverse device-axis mapping"); + check(!aim.sample(2000000000L, 1, 0, 3, delta), "A resume gap must not jump the camera"); + aim.reset(); + check(!aim.sample(2100000000L, 0, 1, 1, delta), "Reset releases pending gyro motion"); + aim.sample(2120000000L, 0, 1, 1, delta); + near(delta[1], -0.02f, "Landscape pitch must follow phone tilt rather than invert it"); + aim.reset(); aim.sample(2200000000L, 0, 1, 3, delta); + aim.sample(2220000000L, 0, 1, 3, delta); + near(delta[1], 0.02f, "Reverse landscape pitch must follow the reversed display axis"); + aim.reset(); aim.sample(3000000000L, 0.005f, -0.005f, 0, delta); + check(!aim.sample(3020000000L, 0.005f, -0.005f, 0, delta), "Small stationary noise must not drift"); + check(!aim.sample(3040000000L, Float.NaN, 1, 0, delta), "Invalid sensor values are rejected"); + aim.sample(4000000000L, 1, 1, 0, delta); + check(!aim.sample(3990000000L, 1, 1, 0, delta), "Out-of-order timestamps produce no motion"); + System.out.println("Gyroscope integration, orientation and lifecycle checks passed"); + } +} diff --git a/tools/tests/TouchLayoutTest.java b/tools/tests/TouchLayoutTest.java new file mode 100644 index 00000000..fba17d09 --- /dev/null +++ b/tools/tests/TouchLayoutTest.java @@ -0,0 +1,109 @@ +package com.halo.decomp; + +/** Standalone geometry/persistence regression checks; no phone required. */ +public final class TouchLayoutTest { + private static void check(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + public static void main(String[] args) { + TouchLayout layout = new TouchLayout(); + check(layout.size() == 18, "All 17 buttons and the movement stick must be editable"); + check(layout.x(TouchLayout.LEFT) == 115 && layout.y(TouchLayout.FIRE_LEFT) == 239, + "Existing layouts must start with the original controls"); + layout.move(TouchLayout.LEFT, -500, -500); + check(layout.x(TouchLayout.LEFT) >= 64 && layout.y(TouchLayout.LEFT) == 64, + "Dragging must keep the full stick inside the screen without a toolbar exclusion"); + layout.move(TouchLayout.FIRE_LEFT, 10000, 10000); + check(layout.x(TouchLayout.FIRE_LEFT)+39 <= 960 && layout.y(TouchLayout.FIRE_LEFT)+39 <= 540, + "Dragging beyond the bottom/right edge must keep the stick reachable"); + layout.move(0, 230, 320); + layout.move(5, 440, 390); + TouchLayout reopened = new TouchLayout(); + for (int i = 0; i < layout.size(); i++) reopened.restore(i, layout.x(i), layout.y(i)); + for (int i = 0; i < layout.size(); i++) { + check(layout.x(i) == reopened.x(i) && layout.y(i) == reopened.y(i), + "Saved coordinates must round-trip for control "+i); + } + float before = reopened.x(0); + reopened.restore(0, Float.NaN, 200); + reopened.restore(0, Float.POSITIVE_INFINITY, 200); + reopened.restore(0, -20, 200); + check(reopened.x(0) == before, "Invalid saved coordinates must not corrupt the layout"); + reopened.move(0, Float.NaN, 200); + check(reopened.x(0) == before, "Invalid drag coordinates must not corrupt the layout"); + // A saved top-row Pause button is valid even before it has been moved. + check(reopened.y(10) == 36, "Reopening must preserve unmoved top-row controls"); + reopened.bounds(1200, 540); + reopened.move(0, 9999, 1); + check(reopened.x(0) == 1164 && reopened.y(0) == 36, + "Controls must reach the full widescreen edge and top edge"); + TouchLayout wide = new TouchLayout(); + wide.restore(0, reopened.savedX(0), reopened.savedY(0)); + wide.bounds(1200, 540); + check(wide.x(0) == reopened.x(0), "Widescreen positions must survive reload"); + // Duplicates retain their action and are independently placed and hidden. + int copy = wide.duplicate(4); + check(copy == 18 && wide.type(copy) == 4, "Fire copies must retain the fire action"); + wide.setShown(4, false); + wide.move(copy, 300, 100); + check(!wide.shown(4) && wide.shown(copy), "Hiding an original must not hide its copy"); + check(wide.duplicate(TouchLayout.LEFT) == -1, "There can only be one movement stick"); + String exported = wide.exportConfiguration(2.25f); + TouchLayout.Configuration imported = TouchLayout.importConfiguration(exported); + imported.layout.bounds(1200, 540); + check(imported.sensitivity == 2.25f && imported.layout.size() == 19, + "Configuration must preserve sensitivity and duplicates"); + for (int i = 0; i < wide.size(); i++) { + check(imported.layout.type(i) == wide.type(i) && imported.layout.shown(i) == wide.shown(i) + && Math.abs(imported.layout.x(i)-wide.x(i)) < 0.001f + && Math.abs(imported.layout.y(i)-wide.y(i)) < 0.001f, + "Export/import must preserve each control on widescreen"); + } + check(wide.add(4) == 4 && wide.shown(4), "Add must restore a hidden button first"); + wide.setShown(TouchLayout.LEFT, false); + check(wide.add(TouchLayout.LEFT) == TouchLayout.LEFT, "Hidden move stick must be restorable"); + reject(exported.replace("version=2", "version=9")); + reject(exported.replace("control.18.type=4", "control.18.type=16")); + reject(exported.replace("control.18.x=240.0", "control.18.x=NaN")); + reject(exported.replace("count=19", "count=10000")); + reject(exported.replace("sensitivity=2.25", "sensitivity=Infinity")); + reject(exported.replace("control.0.visible=false", "control.0.visible=maybe") + .replace("control.0.visible=true", "control.0.visible=maybe")); + reject("not a layout"); + wide.setSize(copy, 1.6f); + wide.rumbleEnabled = false; wide.gyroscopeEnabled = true; + TouchLayout.Configuration sized = TouchLayout.importConfiguration(wide.exportConfiguration(2.25f)); + check(sized.layout.sizeScale(copy) == 1.6f && !sized.layout.rumbleEnabled && sized.layout.gyroscopeEnabled, + "Sizes, rumble and gyro settings must survive export/import"); + int sizedCopy = wide.duplicate(copy); + check(wide.sizeScale(sizedCopy) == 1.6f, "Duplicates inherit their source size"); + wide.move(copy, 99999, 99999); + wide.setSize(copy, 2f); + check(wide.x(copy)+wide.radius(copy) <= 1200 && wide.y(copy)+wide.radius(copy) <= 540, + "Growing a button at the edge must keep it reachable"); + String sizedText = sized.layout.exportConfiguration(2.25f); + reject(sizedText.replace("control.18.size=1.6", "control.18.size=NaN")); + reject(sizedText.replace("control.18.size=1.6", "control.18.size=3.0")); + reject(sizedText.replace("rumble=false", "rumble=invalid")); + reject(sizedText.replace("gyroscope=true", "gyroscope=invalid")); + String legacy = sizedText.replace("version=2", "version=1").replaceAll("(?m)^.*\\.size=.*\\R", "") + .replaceAll("(?m)^(rumble|gyroscope)=.*\\R", ""); + TouchLayout.Configuration old = TouchLayout.importConfiguration(legacy); + check(old.layout.sizeScale(copy) == 1 && old.layout.rumbleEnabled && !old.layout.gyroscopeEnabled, + "Legacy layouts must load with default sizes and gyro disabled"); + wide.resetDefaults(); + check(wide.size() == 18 && wide.shown(4) && wide.x(4) == 915*1200f/960, + "Reset restores defaults on the current display and removes all copies"); + check(wide.sizeScale(4) == 1f && !wide.rumbleEnabled && wide.gyroscopeEnabled, + "Button reset must restore sizes without changing General settings"); + while (wide.duplicate(4) >= 0) {} + check(wide.size() == TouchLayout.MAX_CONTROLS, "Duplicate count must be bounded"); + System.out.println("Touch layout, visibility, duplication and import/export checks passed"); + } + + private static void reject(String text) { + try { TouchLayout.importConfiguration(text); } + catch (IllegalArgumentException expected) { return; } + throw new AssertionError("Invalid configuration was accepted"); + } +} From ee1e169f9b3939ac98fe527865c362005c168dc8 Mon Sep 17 00:00:00 2001 From: thelinkin3000 Date: Wed, 7 Oct 2026 17:21:36 +0000 Subject: [PATCH 112/187] Apply a saved profile's controls to the player using it Saving the profile being edited wrote it to the disk only: a local player playing with that profile kept its old button and stick layouts, sensitivity and inversion until the profile was next chosen (the next mission, the next start). Now each local player whose active profile it is takes the saved one at once. Co-Authored-By: Claude Opus 5.5 --- source/interface/player_ui.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/source/interface/player_ui.c b/source/interface/player_ui.c index 9c92e7e5..28a9eae9 100644 --- a/source/interface/player_ui.c +++ b/source/interface/player_ui.c @@ -805,6 +805,23 @@ boolean player_ui_save_profile( player_profile_save( player_ui_globals.edit_profile_index, &player_ui_globals.edit_profile.current.player); + /* port: a local player playing with the profile gets its changes now (its button and stick + layouts, sensitivity, inversion), not when the profile is next chosen */ + { + short local_player_index; + + for (local_player_index = 0; local_player_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS; local_player_index++) + { + if (player_ui_globals.local_players[local_player_index].active_profile_index == + player_ui_globals.edit_profile_index) + { + player_ui_set_active_player_profile( + local_player_index, + player_ui_globals.edit_profile_index, + &player_ui_globals.edit_profile.current.player); + } + } + } result = TRUE; break; From 59c8d5440916f8f97895a331a7c3ae6abb219cf8 Mon Sep 17 00:00:00 2001 From: thelinkin3000 Date: Wed, 7 Oct 2026 18:05:30 +0000 Subject: [PATCH 113/187] Let any controller drive the menus while one person plays Each controller's events went only to its own player's screens, and the main menu is the first player's: a gamepad on any port but the first did nothing, and a phone can list a device of its own (its touch controls) before the gamepad. With one person playing, the first player's screens now read and take every controller's events; with two or more, each controller keeps to its own player's. Co-Authored-By: Claude Opus 5.5 --- source/interface/ui_widget.c | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 2e8a10be..ac33b50b 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -6811,6 +6811,30 @@ port/linux/game/menu_functions.c) the screens it shares with one player's campaign, New Game's levels and the difficulty, are player 1's (their rows the first controller's), and either player's controller uses them: player 1's is the one that chose co-op, player 2's the one that chose their profile */ +/* port: whether this machine has, or is adding, a second player +(port/linux/game/menu_functions.c): co-op, split screen, the lobby's */ +extern unsigned char pc_menu_split_players(void); + +/* port: with one person playing, any controller drives the first player's +menus, not only the first port's: a phone can list a device of its own (its +touch controls) before the gamepad, which then reads port 2, and a player +picks up whichever pad is at hand. Its screens read every controller's +events (ui_widgets_update), and take them (below). With two or more +players, each controller keeps to its own player's menus */ +static boolean widget_takes_any_controller( + struct widget_instance const *widget) +{ + return widget->local_player_index == 0 && !pc_menu_split_players() && + (we_are_at_the_main_menu || local_player_count() <= 1); +} + +/* port: the controller whose events a screen reads (NONE: every one's) */ +static short widget_event_controller( + struct widget_instance const *widget) +{ + return widget_takes_any_controller(widget) ? NONE : widget->local_player_index; +} + static boolean widget_takes_events_of_controller( struct widget_instance const *widget, short controller_index) @@ -6819,6 +6843,11 @@ static boolean widget_takes_events_of_controller( if (widget->local_player_index == NONE || widget->local_player_index == controller_index) return TRUE; + if (widget->local_player_index == 0 && controller_index > 0 && + controller_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS && widget_takes_any_controller(widget)) + { + return TRUE; + } if (widget->local_player_index != 0 || !we_are_at_the_main_menu || player_spawn_count < 2 || controller_index < 0 || controller_index >= MAXIMUM_NUMBER_OF_LOCAL_PLAYERS) { @@ -7694,7 +7723,7 @@ void process_ui_widgets( struct event_record event = {0}; if (widget_globals.processing_inhibited || - !get_next_event(&event, widget->local_player_index)) + !get_next_event(&event, widget_event_controller(widget))) { /* the widget still gets one empty event so that its animation, auto-close timer and fade keep running */ @@ -7725,7 +7754,7 @@ void process_ui_widgets( if (widget != widget_globals.active_widgets[widget_index]) break; } - while (get_next_event(&event, widget->local_player_index)); + while (get_next_event(&event, widget_event_controller(widget))); } widgets_processed = TRUE; if (!widget_globals.active_widgets[widget_index] && From e799d081494f12de60a45aeb1e23eb3fe7092c6e Mon Sep 17 00:00:00 2001 From: thelinkin3000 Date: Mon, 5 Oct 2026 05:49:49 +0000 Subject: [PATCH 114/187] Sleep in the frame throttle instead of spinning With display.interpolation off the game holds itself to the Xbox's 30 frames a second in main_rasterizer_throttle, which spun on the clock until the next vertical blank. On the ports each look at the clock is a system call: a tester's device spent half its game thread there. The loop sleeps a millisecond between looks now, as it already did while a map precached (Windows sets its timer to 1 ms, win32_posix.c). Co-Authored-By: Claude Opus 5.5 --- source/main/main.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/source/main/main.c b/source/main/main.c index daf9c65f..4a1f096d 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -2718,10 +2718,14 @@ void main_rasterizer_throttle( profile_idle_start(); while ((__int64)rasterizer_globals.frame_and_vertical_blank_index < target_index) { - if (precache_in_progress) - { - Sleep(1); - } + /* the native builds sleep between looks: the Xbox spun here, + and on a port each look at the clock is a system call, so a + game held at 30 frames a second (display.interpolation off) + spent half its game thread asking the time. A millisecond is + well inside a 33 ms frame (Windows sets its timer to 1 ms: + win32_posix.c) */ + (void)precache_in_progress; + Sleep(1); if (system_milliseconds() > start_milliseconds + 1000) { console_warning( From cc41b768bbd4eb3a486929a1a9407e259ed0c449 Mon Sep 17 00:00:00 2001 From: eltanschauung Date: Fri, 9 Oct 2026 08:18:29 -0400 Subject: [PATCH 115/187] Add opt-in FOV and viewmodel settings --- ...ile_edit.video_settings.fov_viewmodels.xml | 74 +++++++++++ ...r_profile_edit.video_settings.graphics.xml | 110 +++++++++++++++ ...tup.player_profile_edit.video_settings.xml | 86 +++--------- port/assets/menus/menus.json | 2 + port/linux/README.md | 17 +++ port/linux/game/render_fov.c | 125 ++++++++++++++++++ port/linux/game/render_fov.h | 10 ++ port/linux/game/viewmodel_fov.c | 79 +++++++++++ port/linux/game/viewmodel_fov.h | 6 + port/linux/game/viewmodel_visibility.c | 23 ++++ port/linux/game/viewmodel_visibility.h | 6 + port/linux/include/math.h | 6 + port/linux/src/port_config.c | 9 ++ source/interface/first_person_weapons.c | 3 + source/interface/hud_draw.c | 32 ++++- source/interface/hud_weapon.c | 5 +- source/main/main.c | 5 +- source/rasterizer/rasterizer_lights.c | 12 ++ .../rasterizer_transparent_geometry.c | 3 + .../rasterizer/xbox/rasterizer_xbox_models.c | 3 + source/render/render_particles.c | 3 + source/render/render_sprite.c | 5 + tools/port_settings.py | 48 ++++++- 23 files changed, 594 insertions(+), 78 deletions(-) create mode 100644 port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml create mode 100644 port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.graphics.xml create mode 100644 port/linux/game/render_fov.c create mode 100644 port/linux/game/render_fov.h create mode 100644 port/linux/game/viewmodel_fov.c create mode 100644 port/linux/game/viewmodel_fov.h create mode 100644 port/linux/game/viewmodel_visibility.c create mode 100644 port/linux/game/viewmodel_visibility.h diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml new file mode 100644 index 00000000..19ea9316 --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.graphics.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.graphics.xml new file mode 100644 index 00000000..f6396ebd --- /dev/null +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.graphics.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml index fb8f6484..7c845d4e 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml @@ -21,12 +21,8 @@ - - - - - - + + @@ -106,59 +102,21 @@ - - - + + + + + - + - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -179,12 +137,8 @@ - - - - - - + + @@ -196,11 +150,7 @@ - - - - - - + + diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json index ad0c56a7..b7f114c4 100644 --- a/port/assets/menus/menus.json +++ b/port/assets/menus/menus.json @@ -46,6 +46,8 @@ "ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml", "ce/main_menu.settings_select.player_setup.player_profile_edit.name_edit.xml", "ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml", + "ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.graphics.xml", "ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml", "ce/main_menu.settings_select.player_setup.player_profile_edit.xml", "ce/main_menu.solo_level_select.xml", diff --git a/port/linux/README.md b/port/linux/README.md index 95b728a2..f1bca603 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -314,6 +314,20 @@ from the next main menu. Each setting has an environment variable. The environment variable changes the setting for one start of the game. It has priority over the file. +Video Setup's **FOV AND VIEWMODELS** category offers optional world and +weapon field-of-view settings and first-person visibility. **DEFAULT** keeps +the authored world projection; **SAME** makes the weapon follow it, and +viewmodels start **ON**. OK saves and applies changes; Cancel discards them. +The menu offers 80–150 degrees in steps of five. The config file accepts +20–150 degrees, including fractional values, or zero for Default/Same. +Angles are horizontal at 16:9; other aspect ratios retain the same vertical +view. World FOV applies to on-foot first-person views, preserving authored +vehicle, death and cinematic cameras. Completed scopes keep their native +view with wider FOV; a narrower FOV also narrows the scope. Aiming reticles +scale with the world projection, while scope artwork keeps its layout. +Viewmodel FOV also applies to attached visuals. Hiding viewmodels changes +only their display; firing, animation, sound and world lights continue. + | Setting | Default | Environment variable | Function | | --- | --- | --- | --- | | `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at the mode of `display.resolution` (the nearest the display has), or at its desktop mode. `"borderless"`: a window over the whole desktop, whose mode does not change. `"windowed"`: a window of `display.window_size`. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. | @@ -328,6 +342,9 @@ the setting for one start of the game. It has priority over the file. | `debug.gpu_flush_draws` | `-1` | `HALO_GPU_FLUSH_DRAWS` | Flush the GPU's pipeline every this many draws. `-1`: every 3 on Intel graphics with Mesa's driver, which can otherwise hang in the game's long runs of small draws and reset the desktop's graphics too. `0`: never. | | `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". | | `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". | +| `display.fov` | `0.0` | `HALO_FOV` | On-foot first-person horizontal FOV at 16:9, 20–150 degrees; `0` keeps the authored view. Video Setup > FOV and Viewmodels sets it. | +| `display.viewmodel_fov` | `0.0` | `HALO_VIEWMODEL_FOV` | Independent weapon/hands horizontal FOV at 16:9, 20–150 degrees; `0` follows the world view. | +| `display.viewmodel_visible` | `true` | `HALO_VIEWMODEL_VIS` | Show first-person weapons, hands and attached visuals. This does not change gameplay or other players' models. | | `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. | | `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the resolution the game draws at, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. | | `display.shadow_resolution` | `128` | `HALO_SHADOW_RESOLUTION` | The size of the maps that the shadows of the objects are drawn in, in pixels each way: `128`, `256`, `512` or `1024` (other values go down to one of these). The game draws the shadow of each object into a map of 128x128 pixels, blurs it and projects it onto the ground. On a large screen, the edges of these shadows show steps that move when the object moves. A larger map makes the edges smooth; the blur is made wider to match, so the shadows are as soft as on the Xbox. Each doubling adds two passes of the blur. `128`: as on the Xbox. | diff --git a/port/linux/game/render_fov.c b/port/linux/game/render_fov.c new file mode 100644 index 00000000..64abccd4 --- /dev/null +++ b/port/linux/game/render_fov.c @@ -0,0 +1,125 @@ +/* FOV in horizontal degrees at 16:9. The observer, its transitions and the +network's camera values remain authored; only the local render view changes. +Default follows the original projection exactly. */ +#include "cseries.h" +#include "render_fov.h" +#include "camera/director.h" +#include "cutscene/cinematics.h" +#include "game/players.h" +#include "objects/objects.h" +#include "units/units.h" +#include "units/unit_definitions.h" +#include "items/weapons.h" +#include "render/render_cameras.h" + +#include + +double config_real(const char *name); +unsigned long config_changes(void); + +static real reticle_scales[MAXIMUM_LOCAL_PLAYERS]; + +static float render_fov_adjust(short local_player_index, float native_vertical_field_of_view) +{ + static boolean initialized; + static unsigned long read_at; + static real requested_tangent; + unsigned long changes = config_changes(); + long unit_index, weapon_index; + struct unit_datum *unit; + real unzoomed_angle, unzoomed_tangent, native_tangent, adjusted_tangent; + + if (!initialized || read_at != changes) + { + double degrees = config_real("display.fov"); + + initialized = TRUE; + read_at = changes; + requested_tangent = isfinite(degrees) && degrees >= 20.0 && degrees <= 150.0 ? + (real)(tan(degrees * 3.14159265358979323846 / 360.0) * (9.0 / 16.0)) : 0.0f; + } + if (!requested_tangent || local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || + cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) || + director_get_perspective(local_player_index) != _director_perspective_first_person || + director_inhibited_facing(local_player_index)) + { + return native_vertical_field_of_view; + } + unit_index = player_control_get_unit_index(local_player_index); + if (unit_index == NONE) + return native_vertical_field_of_view; + unit = unit_try_and_get(unit_index); + if (!unit || unit->object.parent_object_index != NONE || TEST_FLAG(unit->object.damage_flags, _object_dead_bit)) + return native_vertical_field_of_view; + + unzoomed_angle = unit_definition_get(unit->definition_index)->unit.camera_field_of_view; + if (!(unzoomed_angle >= 0.001f && unzoomed_angle <= _pi / 2.0f) || + !(native_vertical_field_of_view > 0.0f && native_vertical_field_of_view < _pi)) + { + return native_vertical_field_of_view; + } + unzoomed_tangent = 0.75f * render_camera_get_adjusted_field_of_view_tangent(unzoomed_angle); + native_tangent = tanf(native_vertical_field_of_view * 0.5f); + if (requested_tangent < unzoomed_tangent) + { + /* A narrow base view must not become wider when zoom is pressed. */ + adjusted_tangent = native_tangent * (requested_tangent / unzoomed_tangent); + } + else + { + real blend = 1.0f; + + weapon_index = unit_inventory_get_weapon(unit_index, unit->unit.current_weapon_index); + if (weapon_index != NONE) + { + real scoped_angle = weapon_get_field_of_view(weapon_index, unzoomed_angle, 0); + + if (scoped_angle > 0.0f && scoped_angle < unzoomed_angle) + { + real scoped_tangent = 0.75f * render_camera_get_adjusted_field_of_view_tangent(scoped_angle); + /* The observer's atan/tan round trip can land just above + the endpoint. Keep a completed scope's original angle exactly. */ + if (native_tangent <= scoped_tangent * 1.000001f) + return native_vertical_field_of_view; + + /* Fade the extra width along the observer's existing scope + transition. Every completed scope level keeps its stock view. */ + blend = PIN((native_tangent - scoped_tangent) / + (unzoomed_tangent - scoped_tangent), 0.0f, 1.0f); + } + } + if (blend == 0.0f) + return native_vertical_field_of_view; + adjusted_tangent = native_tangent + (requested_tangent - unzoomed_tangent) * blend; + } + return 2.0f * atanf(adjusted_tangent); +} + +float render_fov_vertical(short local_player_index, float native_vertical_field_of_view) +{ + real adjusted = render_fov_adjust(local_player_index, native_vertical_field_of_view); + + if (local_player_index >= 0 && local_player_index < MAXIMUM_LOCAL_PLAYERS) + { + real scale = 1.0f; + + if (adjusted != native_vertical_field_of_view && + native_vertical_field_of_view > 0.0f && native_vertical_field_of_view < _pi && + adjusted > 0.0f && adjusted < _pi) + { + scale = tanf(native_vertical_field_of_view * 0.5f) / tanf(adjusted * 0.5f); + if (!isfinite(scale) || scale <= 0.0f) scale = 1.0f; + } + /* Record the projection used for this view, including its native zoom + * transition. A temporary viewmodel projection must not affect the HUD. */ + reticle_scales[local_player_index] = scale; + } + return adjusted; +} + +float render_fov_reticle_scale(short local_player_index) +{ + if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || + reticle_scales[local_player_index] == 0.0f) return 1.0f; + return reticle_scales[local_player_index]; +} diff --git a/port/linux/game/render_fov.h b/port/linux/game/render_fov.h new file mode 100644 index 00000000..145b4873 --- /dev/null +++ b/port/linux/game/render_fov.h @@ -0,0 +1,10 @@ +/* A local display adjustment, after the observer and before either render +camera's projection and visibility frustum are built. */ +#ifndef RENDER_FOV_H +#define RENDER_FOV_H + +float render_fov_vertical(short local_player_index, float native_vertical_field_of_view); +/* HUD pixel ratio relative to the native projection of the last local view. */ +float render_fov_reticle_scale(short local_player_index); + +#endif diff --git a/port/linux/game/viewmodel_fov.c b/port/linux/game/viewmodel_fov.c new file mode 100644 index 00000000..d4d0a688 --- /dev/null +++ b/port/linux/game/viewmodel_fov.c @@ -0,0 +1,79 @@ +#include "cseries.h" +#include "viewmodel_fov.h" +#include "camera/director.h" +#include "cutscene/cinematics.h" +#include "game/players.h" +#include "render/render.h" +#include "render/render_cameras.h" +#include "render/render_cameras_internal.h" +#include "rasterizer/rasterizer.h" +#include "rasterizer/xbox/rasterizer_xbox_internal.h" +#include + +double config_real(const char *name); +unsigned long config_changes(void); + +static unsigned projection_depth; +static boolean projection_applied; +static struct render_camera saved_render_camera, saved_rasterizer_camera; +static struct render_frustum saved_render_frustum, saved_rasterizer_frustum; + +static real viewmodel_vertical(void) +{ + static boolean initialized; + static unsigned long read_at; + static real angle; + unsigned long changes = config_changes(); + if (!initialized || read_at != changes) + { + double degrees = config_real("display.viewmodel_fov"); + angle = isfinite(degrees) && degrees >= 20.0 && degrees <= 150.0 ? + (real)(2.0 * atan(tan(degrees * 3.14159265358979323846 / 360.0) * (9.0 / 16.0))) : 0.0f; + initialized = TRUE; + read_at = changes; + } + return angle; +} + +void viewmodel_projection_begin(void) +{ + real angle; + real_rectangle2d render_bounds, rasterizer_bounds; + if (projection_depth++ != 0) return; + projection_applied = FALSE; + angle = viewmodel_vertical(); + if (!angle || render.local_player_index < 0 || render.local_player_index >= MAXIMUM_LOCAL_PLAYERS || + global_window_parameters.rasterizer_target != 0 /* primary view */ || + cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) || + director_get_perspective(render.local_player_index) != _director_perspective_first_person || + director_inhibited_facing(render.local_player_index)) return; + + saved_render_camera = render.camera; + saved_render_frustum = render.frustum; + saved_rasterizer_camera = global_window_parameters.camera; + saved_rasterizer_frustum = global_window_parameters.frustum; + /* Retain cropped/tiled viewport bounds. CPU visibility and GPU projection + change together; pose, weapon markers and gameplay values stay put. */ + /* The builder takes normalized crop bounds. get_projection_bounds instead + returns signed view-space ray slopes, which would invert/rescale the view. */ + render_bounds = saved_render_frustum.frustum_bounds; + rasterizer_bounds = saved_rasterizer_frustum.frustum_bounds; + render.camera.vertical_field_of_view = angle; + global_window_parameters.camera.vertical_field_of_view = angle; + render_camera_build_frustum(&render.camera, &render_bounds, &render.frustum, TRUE); + render_camera_build_frustum(&global_window_parameters.camera, &rasterizer_bounds, + &global_window_parameters.frustum, TRUE); + projection_applied = TRUE; + rasterizer_set_frustum_z(0.0f, 0.0f); +} + +void viewmodel_projection_end(void) +{ + if (!projection_depth || --projection_depth || !projection_applied) return; + render.camera = saved_render_camera; + render.frustum = saved_render_frustum; + global_window_parameters.camera = saved_rasterizer_camera; + global_window_parameters.frustum = saved_rasterizer_frustum; + projection_applied = FALSE; + rasterizer_set_frustum_z(0.0f, 0.0f); +} diff --git a/port/linux/game/viewmodel_fov.h b/port/linux/game/viewmodel_fov.h new file mode 100644 index 00000000..f3fc9471 --- /dev/null +++ b/port/linux/game/viewmodel_fov.h @@ -0,0 +1,6 @@ +#ifndef VIEWMODEL_FOV_H +#define VIEWMODEL_FOV_H +/* Balanced, nestable scopes change only the current local render view. */ +void viewmodel_projection_begin(void); +void viewmodel_projection_end(void); +#endif diff --git a/port/linux/game/viewmodel_visibility.c b/port/linux/game/viewmodel_visibility.c new file mode 100644 index 00000000..09f8d860 --- /dev/null +++ b/port/linux/game/viewmodel_visibility.c @@ -0,0 +1,23 @@ +#include "viewmodel_visibility.h" + +int config_boolean(const char *name); +unsigned long config_changes(void); + +int viewmodel_is_visible(void) +{ + static int initialized, visible; + static unsigned long read_at; + unsigned long changes = config_changes(); + if (!initialized || read_at != changes) + { + visible = config_boolean("display.viewmodel_visible"); + initialized = 1; + read_at = changes; + } + return visible; +} + +int viewmodel_draws_geometry(int first_person) +{ + return !first_person || viewmodel_is_visible(); +} diff --git a/port/linux/game/viewmodel_visibility.h b/port/linux/game/viewmodel_visibility.h new file mode 100644 index 00000000..40e08b6b --- /dev/null +++ b/port/linux/game/viewmodel_visibility.h @@ -0,0 +1,6 @@ +#ifndef VIEWMODEL_VISIBILITY_H +#define VIEWMODEL_VISIBILITY_H +/* Display only: weapon updates, sounds, gameplay and world lights continue. */ +int viewmodel_is_visible(void); +int viewmodel_draws_geometry(int first_person); +#endif diff --git a/port/linux/include/math.h b/port/linux/include/math.h index 63f92edc..84d84dc3 100644 --- a/port/linux/include/math.h +++ b/port/linux/include/math.h @@ -11,6 +11,12 @@ strnlen() cannot collide with the game's own), which hides glibc's M_*. #include_next +/* glibc hides the C99 classification macros in the game's ISO C89 mode. +Use the compiler's classification, not a call to a nonexistent libm symbol. */ +#ifndef isfinite +#define isfinite(value) __builtin_isfinite(value) +#endif + #ifndef M_PI #define M_E 2.71828182845904523536 #define M_LOG2E 1.44269504088896340736 diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 5fa187c0..0283c22d 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -112,6 +112,15 @@ static const struct config_setting config_settings[] = "In first person, point the view where the player aims now instead of\n" "where the last tick left it: the view turns the frame the mouse moves,\n" "not up to two ticks (66 ms) later." }, + { "display.fov", _config_real, "0.0", "HALO_FOV", _environment_value, _platform_all, + "On-foot first-person horizontal FOV at 16:9, in degrees (20-150);\n" + "0 keeps the authored view. Other cameras keep their own FOV." }, + { "display.viewmodel_fov", _config_real, "0.0", "HALO_VIEWMODEL_FOV", _environment_value, _platform_all, + "Weapon/hands horizontal FOV at 16:9, in degrees (20-150);\n" + "0 follows the world view. Attached visuals use the same projection." }, + { "display.viewmodel_visible", _config_boolean, "true", "HALO_VIEWMODEL_VIS", _environment_value, _platform_all, + "Draw the first-person weapon, hands and attached visuals. Turning\n" + "this off does not change firing, animation, sound or world lights." }, { "display.high_res_hud", _config_boolean, "true", "HALO_HIGH_RES_HUD", _environment_value, _platform_all, "Draw the HUD (meters, counters, panels, motion sensor, reticles,\n" "waypoints, scopes) from the high-res assets (8x the maps' bitmaps);\n" diff --git a/source/interface/first_person_weapons.c b/source/interface/first_person_weapons.c index 0825d05a..de7df2fa 100644 --- a/source/interface/first_person_weapons.c +++ b/source/interface/first_person_weapons.c @@ -100,6 +100,7 @@ symbols in this file: #define REAL_MATH_EXTERNAL_REAL_LOCAL_RANDOM #define REAL_MATH_EXTERNAL_REAL_LOCAL_RANDOM_RANGE #include "interface/first_person_weapons.h" +#include "viewmodel_visibility.h" #include "cache/predicted_resources.h" #include "camera/director.h" @@ -469,6 +470,8 @@ long first_person_weapon_get_local_index( void first_person_weapon_draw( void) { + if (!viewmodel_is_visible()) + return; if (render.local_player_index!=NONE) { struct first_person_weapon *first_person_weapon= first_person_weapon_get(render.local_player_index); diff --git a/source/interface/hud_draw.c b/source/interface/hud_draw.c index ce9d0807..368b2425 100644 --- a/source/interface/hud_draw.c +++ b/source/interface/hud_draw.c @@ -95,6 +95,7 @@ symbols in this file: #include "game/players.h" #include "interface/hud_definitions.h" #include "interface/hud_draw.h" +#include "render_fov.h" #include "interface/interface.h" #include "interface/unit_hud_interface_definition.h" #include "items/weapon_definitions.h" @@ -395,7 +396,8 @@ static void hud_draw_bitmap_internal( real_rectangle2d const *bounds, real_vector2d const *xy_scale, real theta, - pixel32 color); + pixel32 color, + real reticle_scale); static void hud_draw_bitmap_with_meter( void *meter_parameters, struct bitmap_data const *bitmap, @@ -1555,7 +1557,8 @@ static void hud_draw_bitmap_internal( real_rectangle2d const *bounds, real_vector2d const *xy_scale, real theta, - pixel32 color) + pixel32 color, + real reticle_scale) { long return_eip = get_return_eip(); long stack_buffer[STACK_BUFFER_LENGTH]; @@ -1586,6 +1589,24 @@ static void hud_draw_bitmap_internal( vertices[vertex_index].color = color; } + if (reticle_scale != 1.0f) + { + real center_x = (render.camera.window_bounds.x1 + render.camera.window_bounds.x0) / 2 - + render.camera.viewport_bounds.x0; + real center_y = (render.camera.window_bounds.y1 + render.camera.window_bounds.y0) / 2 - + render.camera.viewport_bounds.y0; + + /* Transform the complete authored reticle about its aiming anchor: + * offsets and separate sprites must shrink along with their artwork. */ + for (vertex_index = 0; vertex_index < 4; vertex_index++) + { + vertices[vertex_index].position.x = center_x + + (vertices[vertex_index].position.x - center_x) * reticle_scale; + vertices[vertex_index].position.y = center_y + + (vertices[vertex_index].position.y - center_y) * reticle_scale; + } + } + csmemset(¶meters, 0, sizeof(parameters)); parameters.map_texture_scale[0].j = 1.0f; parameters.map_texture_scale[0].i = 1.0f; @@ -1669,7 +1690,9 @@ static void hud_draw_bitmap_with_meter( &bounds, &xy_scale, theta, - color); + color, + is_crosshair_bitmap && absolute_placement->corner == _hud_anchor_center ? + render_fov_reticle_scale(render.local_player_index) : 1.0f); match_assert_stack_frame("c:\\halo\\SOURCE\\interface\\hud_draw.c", 814); @@ -1831,7 +1854,8 @@ void hud_draw_bitmap_direct( &bounds, &xy_scale, theta, - color); + color, + 1.0f); match_assert_stack_frame("c:\\halo\\SOURCE\\interface\\hud_draw.c", 856); diff --git a/source/interface/hud_weapon.c b/source/interface/hud_weapon.c index 8f801a71..8dd5529f 100644 --- a/source/interface/hud_weapon.c +++ b/source/interface/hud_weapon.c @@ -1295,7 +1295,7 @@ static void crosshairs_draw( color, in_multiplayer, interface_bitmap, - TRUE); + FALSE); } else { @@ -1311,7 +1311,8 @@ static void crosshairs_draw( color, in_multiplayer, bitmap_group->type == _bitmap_group_type_interface_bitmaps, - TRUE); + /* Scope artwork keeps its authored layout. */ + state_index != _crosshair_state_zoom); } } } diff --git a/source/main/main.c b/source/main/main.c index daf9c65f..7673b1be 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -393,6 +393,7 @@ symbols in this file: #include "text/font_group.h" #include "tag_files/files.h" #include "custom_edition_cache.h" /* port: custom_edition_level_name */ +#include "render_fov.h" /* port: local world FOV, before projection and culling */ #if defined(HALO_WINDOWS) || defined(HALO_ANDROID) || defined(__linux__) #define HALO_NATIVE_BUILD_INFO 1 @@ -1152,10 +1153,10 @@ void set_window_camera_values( window->rasterizer_camera.forward = observer->forward; window->rasterizer_camera.up = observer->up; window->rasterizer_camera.vertical_field_of_view = - 2.0f * arctangent( + render_fov_vertical(window->local_player_index, 2.0f * arctangent( 0.75f * render_camera_get_adjusted_field_of_view_tangent( observer->field_of_view), - 1.0f); + 1.0f)); if (window->local_player_index != NONE && !console_is_active() && diff --git a/source/rasterizer/rasterizer_lights.c b/source/rasterizer/rasterizer_lights.c index 412d3840..4cc7abf0 100644 --- a/source/rasterizer/rasterizer_lights.c +++ b/source/rasterizer/rasterizer_lights.c @@ -81,6 +81,8 @@ symbols in this file: /* ---------- headers */ #include "cseries.h" +#include "viewmodel_fov.h" +#include "viewmodel_visibility.h" #include "errors.h" #include "bitmaps/bitmaps.h" #include "interface/hud_draw.h" @@ -545,6 +547,8 @@ void rasterizer_lens_flare_submit( 268, (parameters->compressed_window_index&_lens_flare_window_index_mask)==global_window_parameters.window_index); + if (!viewmodel_draws_geometry(parameters->compressed_window_index & _lens_flare_first_person_weapon_flag)) + return; if (rasterizer_debug_options.draw_lens_flares && !screenshot_in_progress() && global_window_parameters.rasterizer_target==_rasterizer_target_render_primary) { @@ -849,6 +853,8 @@ void rasterizer_lens_flares_submit_occlusion_tests( if ((lens_flare_parameters->compressed_window_index & _lens_flare_window_index_mask) == global_window_parameters.window_index) { + boolean first_person = (lens_flare_parameters->compressed_window_index & _lens_flare_first_person_weapon_flag) != 0; + if (first_person) viewmodel_projection_begin(); real occlusion_radius = definition->occlusion_radius; real_point3d occlusion_point; @@ -885,6 +891,8 @@ void rasterizer_lens_flares_submit_occlusion_tests( &occlusion_point, occlusion_radius, lens_flare_index); + + if (first_person) viewmodel_projection_end(); } } @@ -919,6 +927,8 @@ void rasterizer_lens_flares_draw( if ((lens_flare_parameters->compressed_window_index & _lens_flare_window_index_mask) == global_window_parameters.window_index) { + boolean first_person = (lens_flare_parameters->compressed_window_index & _lens_flare_first_person_weapon_flag) != 0; + if (first_person) viewmodel_projection_begin(); struct lens_flare_definition *definition = lens_flare_parameters->definition; if (lens_flare_parameters->internal__occlusion_pixels > 0 && @@ -1161,6 +1171,8 @@ void rasterizer_lens_flares_draw( } } } + + if (first_person) viewmodel_projection_end(); } } diff --git a/source/rasterizer/rasterizer_transparent_geometry.c b/source/rasterizer/rasterizer_transparent_geometry.c index 858fd4d1..f5534842 100644 --- a/source/rasterizer/rasterizer_transparent_geometry.c +++ b/source/rasterizer/rasterizer_transparent_geometry.c @@ -83,6 +83,7 @@ symbols in this file: /* ---------- headers */ #include "cseries.h" +#include "viewmodel_fov.h" #include "cseries/errors.h" #include "rasterizer.h" #include "rasterizer_console_vars.h" @@ -586,6 +587,7 @@ void rasterizer_transparent_geometry_draw( global_window_parameters.rasterizer_target==_rasterizer_target_render_primary); if (!first_person_flag) { + viewmodel_projection_begin(); rasterizer_set_stencil_mode(0); rasterizer_set_frustum_z( rasterizer_globals.first_person_weapon_near_clip_distance, @@ -613,6 +615,7 @@ void rasterizer_transparent_geometry_draw( rasterizer_debug_options.transparent_pixel_counter_active = FALSE; if (first_person_flag) { + viewmodel_projection_end(); rasterizer_set_frustum_z(0.0f, 0.0f); } } diff --git a/source/rasterizer/xbox/rasterizer_xbox_models.c b/source/rasterizer/xbox/rasterizer_xbox_models.c index b7eecb48..12ba8dfc 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_models.c +++ b/source/rasterizer/xbox/rasterizer_xbox_models.c @@ -111,6 +111,7 @@ symbols in this file: #include "rasterizer/rasterizer_models.h" #include "rasterizer/rasterizer_transparent_geometry.h" #include "render/render.h" +#include "viewmodel_fov.h" #include "shaders/shader_definitions.h" #include "shaders/shaders.h" @@ -615,6 +616,7 @@ void _rasterizer_model_end( !local_do_not_change_z_stencil_states) { rasterizer_set_stencil_mode(RASTERIZER_STENCIL_MODE_REJECT); + viewmodel_projection_end(); rasterizer_set_frustum_z(0.0f, 0.0f); } local_parameters = NULL; @@ -643,6 +645,7 @@ void _rasterizer_model_begin( !do_not_change_z_stencil_states) { rasterizer_set_stencil_mode(RASTERIZER_STENCIL_MODE_WRITE); + viewmodel_projection_begin(); rasterizer_set_frustum_z( rasterizer_globals.first_person_weapon_near_clip_distance, rasterizer_globals.first_person_weapon_far_clip_distance); diff --git a/source/render/render_particles.c b/source/render/render_particles.c index 244d5daf..1284b127 100644 --- a/source/render/render_particles.c +++ b/source/render/render_particles.c @@ -22,6 +22,7 @@ symbols in this file: #include "effects/particles.h" #include "game/players.h" #include "interface/first_person_weapons.h" +#include "viewmodel_visibility.h" #include "render.h" #include "render_cameras_internal.h" #include "render_particles.h" @@ -160,6 +161,8 @@ void render_particles( particle->local_player_index == local_player_index; if (render_location_visible(&particle->location) && + viewmodel_draws_geometry(owned_by_local_player && + TEST_FLAG(particle->flags, _particle_datum_dont_draw_third_person_bit)) && (!TEST_FLAG( particle->flags, _particle_datum_dont_draw_first_person_bit) || diff --git a/source/render/render_sprite.c b/source/render/render_sprite.c index 963ad649..af800c87 100644 --- a/source/render/render_sprite.c +++ b/source/render/render_sprite.c @@ -76,6 +76,7 @@ symbols in this file: /* ---------- headers */ #include "cseries/cseries.h" +#include "viewmodel_fov.h" #include "bitmaps/bitmap_group.h" #include "cseries/errors.h" #include "interface/hud_draw.h" @@ -213,6 +214,8 @@ void build_sprites_begin( !TEST_FLAG(flags, _build_sprites_valid_bit)); data->bitmap_group_index = bitmap_group_index; + if (TEST_FLAG(flags, _build_sprites_first_person_bit)) + viewmodel_projection_begin(); data->flags = flags; data->shader = shader; data->group_count = 0; @@ -282,6 +285,8 @@ void build_sprites_end( } SET_FLAG(data->flags, _build_sprites_valid_bit, FALSE); + if (TEST_FLAG(data->flags, _build_sprites_first_person_bit)) + viewmodel_projection_end(); return; } diff --git a/tools/port_settings.py b/tools/port_settings.py index da022f57..e3243556 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -176,6 +176,34 @@ SCREENS = { }, } +# Video Setup's categories open ordinary settings screens. Each has the same +# pending edits, Defaults, OK and Cancel as the other settings screens. +_video = SCREENS["video_settings"] +_graphics = {"display.high_res_hud", "display.high_res_text", "display.anti_aliasing", + "display.shadow_resolution", "display.per_pixel_lighting"} +SCREENS["video_settings/graphics"] = { + "screen": "graphics_settings_screen", "header": _video["header"], "spacing": 30, + "same_place": ["anti_aliasing_android"], + "rows": [row for row in _video["rows"] if row[1] in _graphics], +} +_video["rows"] = [row for row in _video["rows"] if row[1] not in _graphics] +_video["categories"] = [ + ("GRAPHICS:", "video_settings/graphics", "The HUD, text, anti-aliasing, shadows and\nlighting."), + ("FOV AND VIEWMODELS:", "video_settings/fov_viewmodels", + "Optional field of view and first-person weapon\ndisplay settings. Defaults keep the original view."), +] +SCREENS["video_settings/fov_viewmodels"] = { + "screen": "fov_viewmodel_settings_screen", "header": _video["header"], "spacing": 30, + "rows": [ + ("FOV:", "display.fov", [("DEFAULT", "0")] + [(str(n), str(n)) for n in range(80, 151, 5)], + "On-foot horizontal FOV at 16:9, in degrees.\nDefault keeps the authored view and scopes.", None), + ("VIEWMODEL FOV:", "display.viewmodel_fov", [("SAME", "0")] + [(str(n), str(n)) for n in range(80, 151, 5)], + "Weapon/hands horizontal FOV at 16:9, in degrees.\nSame follows the world view.", None), + ("VIEWMODELS:", "display.viewmodel_visible", ON_OFF, + "Draw first-person weapons, hands and attached\nvisuals. Gameplay and sound continue when off.", None), + ], +} + # Controls Setup: the keyboard and mouse's actions, in groups (the order of # port/linux/game/menu_functions.c's table of them) CONTROL_GROUPS = ["MOVEMENT", "WEAPONS", "ACTIONS"] @@ -330,14 +358,30 @@ def _setting_screen(folder: str, spec: dict) -> list: ("header_bounds", "7 -13 19 -7" if wide else "7 -6 19 0"), ("footer_bounds", "7 208 19 214" if wide else "7 150 19 156")], ['']) + for index, (label, category_folder, _) in enumerate(spec.get("categories", ())): + key = category_folder.rsplit("/", 1)[-1] + row = f"{base}/op_{key}" + target = f"{PE}/{category_folder}/{SCREENS[category_folder]['screen']}" + rows.append((row, None, place + 1 + index)) + extra += _widget(row, [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), + ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], + [f'', f'', + '', + f'']) + extra += _widget(f"{base}/{key}_label", + [("type", "text"), ("controller", 1), ("width", 512), ("height", 22), + ("string_list", f"{base}/labels"), ("string_index", len(spec["rows"]) + index), + ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 4)], []) extra += _button(f"{base}/button_defaults", 3, ['', '']) extra += _button(f"{base}/button_ok", 1, ['', '']) - extra += _strings(f"{base}/labels", [label for label, *_ in spec["rows"]]) + extra += _strings(f"{base}/labels", [label for label, *_ in spec["rows"]] + + [label for label, *_ in spec.get("categories", ())]) # (the help of the row whose label is string n is n + 1: the buttons' is 0) extra += _strings(f"{base}/help_strings", - [""] + [help_text.replace("\n", "\\n") for _, _, _, help_text, *_ in spec["rows"]]) + [""] + [help_text.replace("\n", "\\n") for _, _, _, help_text, *_ in spec["rows"]] + + [help_text.replace("\n", "\\n") for _, _, help_text in spec.get("categories", ())]) return _screen(folder, spec, rows, ["port settings help"], [], extra) From fb9f03cf2c9e84256c95ce85963da44a3b733c81 Mon Sep 17 00:00:00 2001 From: Paul Hontz Date: Tue, 6 Oct 2026 11:40:52 -0400 Subject: [PATCH 116/187] Split HALO_ANDROID into the guest's ABI, the ES renderer and the app HALO_ANDROID meant three things at once. The port's code now names each: HALO_ARM64_GUEST for the ILP32 AArch64 guest's code paths (the inline assembly's C, the bss pragmas, the CRT's), HALO_GLES for the OpenGL ES renderer (its GL header and function list, shaders and capabilities), and HALO_ANDROID for the app alone (its display, input, files and lifecycle). The Android build defines all three, so nothing changes for it, nor for Linux and Windows, which define none. A build for another platform can then take one part without the others: the ES renderer, or the guest's ABI, without the Android app. port/android/README.md also no longer says that the widescreen changes are in #ifdef HALO_ANDROID: none have been since 50eae19b. --- port/android/README.md | 15 ++- port/linux/include/halo_linux_prefix.h | 2 +- port/linux/include/halo_port_capacity.h | 4 +- port/linux/src/d3d8_gl.c | 122 +++++++++--------- port/linux/src/dsound_sdl.c | 2 +- port/linux/src/gl.h | 6 +- port/linux/src/menu_files.c | 2 +- port/linux/src/msvc_crt.c | 2 +- port/linux/src/nv2a_psh.c | 12 +- port/linux/src/nv2a_vsh.c | 6 +- port/linux/src/platform.h | 2 +- port/linux/src/port_config.c | 4 +- port/linux/src/sdl_platform.c | 6 +- port/linux/src/xbox_textures.c | 12 +- port/linux/src/xgpu.h | 4 +- port/linux/src/xgpu_post.c | 14 +- source/bungie_net/network/transport_address.c | 4 +- source/cseries/stack_walk_windows.c | 4 +- source/effects/material_effects.c | 4 +- source/hs/hs.c | 4 +- source/math/random_math.c | 4 +- source/memory/crc.c | 4 +- source/objects/object_types.c | 4 +- source/rasterizer/rasterizer.c | 4 +- source/text/unicode.c | 4 +- tools/android_build.py | 4 + 26 files changed, 133 insertions(+), 122 deletions(-) diff --git a/port/android/README.md b/port/android/README.md index 771958fa..7bd0b005 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -207,9 +207,7 @@ the Xbox: - Black bars and fades cover all of the screen, and so do the menus' dims and backgrounds (the pause menu's dim, dialogs, the menus' gradient). -The changes are in `#ifdef HALO_ANDROID` in `rasterizer_xbox.c`, `render.c`, -`ui_widget.c`, `cinematics.c`, `main.c` and -`rasterizer_xbox_screen_effect.c`. +All the ports draw this way. ## How the port operates @@ -314,9 +312,18 @@ floating-point contraction, as on x86. ### Game source changes +The port's code tells three things apart, each its own macro, which the +Android build defines all of (`tools/android_build.py`): + +- `HALO_ARM64_GUEST`: the guest's ABI (ILP32 AArch64 code in a 64-bit + process); +- `HALO_GLES`: the OpenGL ES renderer; +- `HALO_ANDROID`: the app (its display, input, files and lifecycle). + + The x86 inline assembly is replaced by C (refer to [port/linux/README.md](../linux/README.md#game-source-changes)). -These changes are in `#ifdef HALO_ANDROID`: +These changes are in `#ifdef HALO_ARM64_GUEST`: - Seven `#pragma bss_seg(".bss")` lines are removed. The Darwin target does not accept them. diff --git a/port/linux/include/halo_linux_prefix.h b/port/linux/include/halo_linux_prefix.h index dc366045..0667a642 100644 --- a/port/linux/include/halo_linux_prefix.h +++ b/port/linux/include/halo_linux_prefix.h @@ -10,7 +10,7 @@ byte-for-byte identical to what the matching MSVC build compiles. #ifndef __HALO_LINUX_PREFIX_H #define __HALO_LINUX_PREFIX_H -#if !defined(__i386__) && !defined(HALO_ANDROID) +#if !defined(__i386__) && !defined(HALO_ARM64_GUEST) #error the Linux port targets 32-bit x86: game data structures assume 32-bit pointers #endif diff --git a/port/linux/include/halo_port_capacity.h b/port/linux/include/halo_port_capacity.h index 0212aafc..868e2aa2 100644 --- a/port/linux/include/halo_port_capacity.h +++ b/port/linux/include/halo_port_capacity.h @@ -46,7 +46,7 @@ measurement). The desktop builds' cache is 256 MB, half their 512 MB memory window (port/linux/src/platform.h), whose pages are backed as they are used. Android's window is 128 MB, and its cache the Xbox's. */ -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST #define HALO_PORT_TEXTURE_CACHE_PAGE_COUNT 0x580 /* (0x580) */ #else #define HALO_PORT_TEXTURE_CACHE_PAGE_COUNT 0x4000 /* (0x580) */ @@ -62,7 +62,7 @@ over a map's (loose_sounds.c), are longer than the Xbox maps' and fill it (a campaign map's dialogue and music did, "SOUND CACHE BLOWN" in debug.txt). The desktop builds' cache is 16 MB; Android's window keeps the Xbox's. */ -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST #define HALO_PORT_SOUND_CACHE_SIZE 0x400000 /* (0x400000) */ #else #define HALO_PORT_SOUND_CACHE_SIZE 0x1000000 /* (0x400000) */ diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 2c708905..2eb64fdb 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -38,7 +38,7 @@ Conventions carried over from the Xbox: void d3d8_surface_initialize(D3DSurface *surface, D3DFORMAT format, unsigned long width, unsigned long height); void d3d8_surface_resize(D3DSurface *surface, D3DFORMAT format, unsigned long width, unsigned long height); -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* OpenGL ES 3 (port/android/README.md): the desktop formats, enumerants and entry points used below that ES lacks */ #define GL_BGRA GL_RGBA @@ -116,7 +116,7 @@ static const struct { { "off", _anti_aliasing_off, 0 }, { "fxaa", _anti_aliasing_fxaa, 0 }, -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* (SMAA's three passes and supersampling's four times the pixels are more than a phone's GPU has to spare: FXAA in SMAA's place, and none) */ { "smaa", _anti_aliasing_fxaa, 0 }, @@ -329,7 +329,7 @@ struct vertex_shader_object /* a shader lit for each pixel failed to compile or link: lit as the vertex shader lights it from then on */ BOOL lighting_failed; -#ifndef HALO_ANDROID +#ifndef HALO_GLES /* the vertex array its draws last used, and the streams they had (setup_streams: the layout follows from the two) */ struct vertex_array_entry *vertex_array; @@ -449,7 +449,7 @@ static struct framebuffer_entry *framebuffers; /* ---------- the device */ -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* Mobile drivers (Mali) keep every orphaned copy of a buffer until the GPU is done with it, so a large buffer orphaned each frame costs its size per frame in flight and more. Instead each frame streams into the next of a few @@ -464,7 +464,7 @@ vertices. */ #define INDEX_BUFFER_SIZE (8 * 1024 * 1024) #endif #define VISIBILITY_TEST_SLOTS 4096 -#ifdef HALO_ANDROID +#ifdef HALO_GLES #define VISIBILITY_QUERY GL_ANY_SAMPLES_PASSED #define VISIBILITY_ALL_SAMPLES 1000000 #else @@ -511,7 +511,7 @@ struct gl_device GLuint vertex_array; GLuint stream_buffer; -#ifdef HALO_ANDROID +#ifdef HALO_GLES GLuint stream_buffers[STREAM_BUFFER_RING]; GLuint index_buffers[STREAM_BUFFER_RING]; unsigned long buffer_ring; @@ -540,7 +540,7 @@ struct gl_device lights makes hundreds of tests. */ GLuint visibility_known[VISIBILITY_TEST_SLOTS]; BOOL visibility_unread[VISIBILITY_TEST_SLOTS]; -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* with atomic counters: one counter per test, used as a ring */ GLuint visibility_counters; unsigned long counter_next; @@ -641,7 +641,7 @@ xgpu_gl_state_invalidate, after which every value is set again. Unknown values are all ones, which no real value matches (floats become NaN, which compares unequal to everything). */ -#ifdef HALO_ANDROID +#ifdef HALO_GLES struct attribute_pointer { GLuint buffer; @@ -730,7 +730,7 @@ static struct GLuint array_buffer; GLuint element_array_buffer; unsigned char attribute_enabled[XGPU_VERTEX_ATTRIBUTE_COUNT]; -#ifdef HALO_ANDROID +#ifdef HALO_GLES struct attribute_pointer attribute_pointers[XGPU_VERTEX_ATTRIBUTE_COUNT]; #else GLuint vertex_array; @@ -782,7 +782,7 @@ static int texture_slot(GLenum target) return target == GL_TEXTURE_CUBE_MAP ? 1 : target == GL_TEXTURE_3D ? 2 : 0; } -#ifdef HALO_ANDROID +#ifdef HALO_GLES static void state_texture(int unit, GLenum target, GLuint texture) { int slot = texture_slot(target); @@ -826,7 +826,7 @@ static void state_element_array_buffer(GLuint buffer) } } -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* enables the attribute, reading size elements of type from buffer: each vertex is stride bytes on from the one before it, starting at buffer_offset, with the attribute relative_offset bytes into it */ @@ -953,7 +953,7 @@ static void state_attribute_value(GLuint index, const float *value) { unsigned char kind = value ? 0 : 1; -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (gl_state.attribute_enabled[index] != 0) { gl_state.attribute_enabled[index] = 0; @@ -1114,7 +1114,7 @@ GLuint xgpu_link_program(GLuint vertex_shader, GLuint fragment_shader, const cha return program; } -#ifndef HALO_ANDROID +#ifndef HALO_GLES static void GLAPIENTRY gl_debug_callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *user) { @@ -1384,7 +1384,7 @@ static void render_target_multisample(struct xgpu_render_target *target, int sam return; draw = target->depth ? framebuffer_find(0, target->multisample, TRUE) : framebuffer_find(target->multisample, 0, TRUE); -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* (ES blits into no multisampled framebuffer: it is cleared instead. A change of the setting is taken up between frames, and the frame clears the screen's targets before it draws) */ @@ -1499,7 +1499,7 @@ static void gl_initialize(void) glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); -#ifdef HALO_ANDROID +#ifdef HALO_GLES { BOOL es32 = major > 3 || (major == 3 && minor >= 2); @@ -1538,7 +1538,7 @@ static void gl_initialize(void) #endif glGenVertexArrays(1, &device.vertex_array); glBindVertexArray(device.vertex_array); -#ifdef HALO_ANDROID +#ifdef HALO_GLES { int ring; @@ -1555,7 +1555,7 @@ static void gl_initialize(void) device.index_buffer = device.index_buffers[0]; } #endif -#ifndef HALO_ANDROID +#ifndef HALO_GLES glGenBuffers(1, &device.stream_buffer); glBindBuffer(GL_ARRAY_BUFFER, device.stream_buffer); glBufferData(GL_ARRAY_BUFFER, STREAM_BUFFER_SIZE, NULL, GL_STREAM_DRAW); @@ -1566,7 +1566,7 @@ static void gl_initialize(void) glGenSamplers(D3DTSS_MAXSTAGES, device.samplers); glGenQueries(VISIBILITY_TEST_SLOTS, device.queries); glGenQueries(1, &device.scratch_query); -#ifndef HALO_ANDROID +#ifndef HALO_GLES glGenBuffers(1, &device.visibility_results_buffer); glBindBuffer(GL_QUERY_BUFFER, device.visibility_results_buffer); glBufferStorage(GL_QUERY_BUFFER, VISIBILITY_TEST_SLOTS * sizeof(GLuint), NULL, @@ -1596,7 +1596,7 @@ static void gl_initialize(void) } } #endif -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (xgpu_capabilities.atomic_counters) { int ring; @@ -2097,7 +2097,7 @@ void WINAPI D3DDevice_BeginVisibilityTest(void) return; /* the query object is chosen when the test ends; use a scratch one */ device.visibility_test_active = TRUE; -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (xgpu_capabilities.atomic_counters) { const GLuint zero = 0; @@ -2134,7 +2134,7 @@ HRESULT WINAPI D3DDevice_EndVisibilityTest(DWORD index) return S_OK; device.visibility_test_active = FALSE; index %= VISIBILITY_TEST_SLOTS; -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (xgpu_capabilities.atomic_counters) { unsigned long *count = &device.ring_test_count[device.buffer_ring]; @@ -2158,7 +2158,7 @@ HRESULT WINAPI D3DDevice_EndVisibilityTest(DWORD index) (lens flares, rasterizer_lights.c), a split-screen window's or the screen's alike */ device.query_area[index] = target_scale[0] * target_scale[1] * (float)target_samples; -#ifndef HALO_ANDROID +#ifndef HALO_GLES if (device.visibility_results) { /* the GPU copies the count into the slot at the next frame's end @@ -2182,7 +2182,7 @@ HRESULT WINAPI D3DDevice_EndVisibilityTest(DWORD index) return S_OK; } -#ifndef HALO_ANDROID +#ifndef HALO_GLES /* a count of pixels in the game's pixels */ static GLuint visibility_unscaled(GLuint samples, DWORD index) { @@ -2205,7 +2205,7 @@ HRESULT WINAPI D3DDevice_GetVisibilityTestResult(DWORD index, UINT *result, ULON *result = 0; return S_OK; } -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (xgpu_capabilities.atomic_counters) { /* the latest count the GPU has finished (counter_snapshots) */ @@ -2214,7 +2214,7 @@ HRESULT WINAPI D3DDevice_GetVisibilityTestResult(DWORD index, UINT *result, ULON return S_OK; } #endif -#ifndef HALO_ANDROID +#ifndef HALO_GLES if (device.visibility_results) { /* the latest count the GPU has written: from this test, or while @@ -2232,7 +2232,7 @@ HRESULT WINAPI D3DDevice_GetVisibilityTestResult(DWORD index, UINT *result, ULON if (available) { glGetQueryObjectuiv(device.queries[index], GL_QUERY_RESULT, &samples); -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ES only says whether any sample passed. The game divides the count by the test's area (lens flare brightness, rasterizer_lights.c): report more than any test covers, well @@ -2778,7 +2778,7 @@ static GLenum address_mode(DWORD mode) { case D3DTADDRESS_MIRROR: return GL_MIRRORED_REPEAT; case D3DTADDRESS_CLAMP: return GL_CLAMP_TO_EDGE; -#ifdef HALO_ANDROID +#ifdef HALO_GLES case D3DTADDRESS_BORDER: return xgpu_capabilities.border_clamp ? GL_CLAMP_TO_BORDER : GL_CLAMP_TO_EDGE; #else case D3DTADDRESS_BORDER: return GL_CLAMP_TO_BORDER; @@ -2824,7 +2824,7 @@ static void sampler_parameters(GLuint sampler, const DWORD *inputs) glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, (GLint)address_mode(inputs[3])); glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, (GLint)address_mode(inputs[4])); glSamplerParameteri(sampler, GL_TEXTURE_WRAP_R, (GLint)address_mode(inputs[5])); -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ES has no sampler LOD bias; the pixel shader applies it (texture_lod_bias) */ (void)lod_bias; @@ -2934,7 +2934,7 @@ struct mip_composite static struct mip_composite *mip_composites; -#ifdef HALO_ANDROID +#ifdef HALO_GLES static GLuint framebuffer_get(GLuint color, GLuint depth); /* glCopyImageSubData for ES 3.0/3.1 contexts without the extension */ @@ -3022,7 +3022,7 @@ static GLuint mip_composite_get(const struct xgpu_texture_description *descripti composite->level_sources[level] = target->texture; composite->level_written[level] = target->written; render_target_resolve(target); -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (!xgpu_capabilities.copy_image) { copy_level_by_blit(target->texture, composite->texture, (GLint)level, width, height); @@ -3114,7 +3114,7 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale gl_target == GL_TEXTURE_3D ? _xgpu_sampler_3d : _xgpu_sampler_2d; } } -#ifdef HALO_ANDROID +#ifdef HALO_GLES for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++) state_texture(stage, gl_targets[stage], gl_textures[stage]); #else @@ -3293,7 +3293,7 @@ static void apply_raster_state(BOOL has_depth) state_enable(&gl_state.cull_face, GL_CULL_FACE, rs[D3DRS_CULLMODE] != D3DCULL_NONE); if (rs[D3DRS_CULLMODE] != D3DCULL_NONE) { -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* the vertex shader flips y in clip space, which (unlike desktop GL's upper-left clip origin) also flips the winding */ GLenum front_face = rs[D3DRS_FRONTFACE] == D3DFRONT_CCW ? GL_CW : GL_CCW; @@ -3313,7 +3313,7 @@ static void apply_raster_state(BOOL has_depth) glCullFace(cull_mode); } } -#ifndef HALO_ANDROID +#ifndef HALO_GLES /* ES draws filled polygons only (wireframe is a debug mode) */ { GLenum polygon_mode = rs[D3DRS_FILLMODE] == D3DFILL_WIREFRAME ? GL_LINE : @@ -3329,7 +3329,7 @@ static void apply_raster_state(BOOL has_depth) /* D3DRS_ZBIAS is expressed in these states (D3DDevice_SetRenderState_ZBias) */ state_enable(&gl_state.offset_fill, GL_POLYGON_OFFSET_FILL, rs[D3DRS_SOLIDOFFSETENABLE] != 0); -#ifndef HALO_ANDROID +#ifndef HALO_GLES state_enable(&gl_state.offset_line, GL_POLYGON_OFFSET_LINE, rs[D3DRS_SOLIDOFFSETENABLE] != 0); #endif if (rs[D3DRS_SOLIDOFFSETENABLE]) @@ -3346,7 +3346,7 @@ static void apply_raster_state(BOOL has_depth) } } -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ES has no debug callback in 3.0; debug.gl_debug polls glGetError around each draw instead, reporting each distinct error a few times */ static void gl_check_errors(const char *where) @@ -3404,7 +3404,7 @@ apply to the others. A memory barrier is one (and only that: nothing here writes images). */ static void draw_flush(void) { -#ifndef HALO_ANDROID +#ifndef HALO_GLES if (device.flush_every && ++device.flush_draws >= device.flush_every) { device.flush_draws = 0; @@ -3491,14 +3491,14 @@ static struct program_entry *prepare_draw(BOOL immediate) D3D__RenderState[D3DRS_DESTBLEND] == D3DBLEND_SRCALPHA; key.point_threshold = key.point_threshold && key.coverage_alpha; key.alpha_test_function = D3D__RenderState[D3DRS_ALPHATESTENABLE] ? D3D__RenderState[D3DRS_ALPHAFUNC] : 0; -#ifndef HALO_ANDROID +#ifndef HALO_GLES /* (gl_SampleMask: ES has it only from 3.2) */ if (target_samples > 1 && key.alpha_test_function && !D3D__RenderState[D3DRS_ALPHABLENDENABLE]) key.alpha_test_samples = (unsigned char)target_samples; #endif key.fog_enable = D3D__RenderState[D3DRS_FOGENABLE] != 0; key.fog_table_mode = (unsigned char)D3D__RenderState[D3DRS_FOGTABLEMODE]; -#ifdef HALO_ANDROID +#ifdef HALO_GLES key.count_samples = device.visibility_test_active && xgpu_capabilities.atomic_counters; #endif @@ -3532,7 +3532,7 @@ static struct program_entry *prepare_draw(BOOL immediate) stats.draws++; draw_flush(); state_program(entry->program); -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (key.count_samples) glBindBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, device.visibility_counters, (GLintptr)(device.counter_active * sizeof(GLuint)), sizeof(GLuint)); @@ -3874,7 +3874,7 @@ static struct /* the page's memory_watch generation when it was uploaded */ unsigned long generation[MIRROR_PAGE_COUNT]; unsigned long rewritten_frame[MIRROR_PAGE_COUNT]; -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* device.frame at the segment's latest glBufferSubData */ unsigned long subdata_frame[MIRROR_SEGMENT_COUNT]; #endif @@ -3961,13 +3961,13 @@ static BOOL mirror_refresh(unsigned long first, unsigned long last) glGenBuffers(1, &mirror.buffers[segment]); glBindBuffer(GL_COPY_WRITE_BUFFER, mirror.buffers[segment]); glBufferData(GL_COPY_WRITE_BUFFER, MIRROR_SEGMENT_SIZE, NULL, GL_DYNAMIC_DRAW); -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* (no glBufferSubData yet: as if long enough ago) */ mirror.subdata_frame[segment] = device.frame - STREAM_BUFFER_RING; #endif } glBindBuffer(GL_COPY_WRITE_BUFFER, mirror.buffers[segment]); -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* Mali copies the whole buffer for a glBufferSubData that queued draws might read (see STREAM_BUFFER_RING); unused pages can be written without waiting for them. Not while such a copy may be @@ -4120,7 +4120,7 @@ static unsigned long stream_upload(const void *data, unsigned long size) stream_reserve(size); offset = device.stream_offset; state_array_buffer(device.stream_buffer); -#ifdef HALO_ANDROID +#ifdef HALO_GLES host_gl_buffer_write(GL_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)length, data); #else buffer_upload(GL_ARRAY_BUFFER, offset, length, data); @@ -4129,7 +4129,7 @@ static unsigned long stream_upload(const void *data, unsigned long size) return offset; } -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* stream_upload, with the D3DCOLOR elements of the stream turned from BGRA into the RGBA byte order ES reads */ static unsigned long stream_upload_swizzled(const struct vertex_shader_object *declaration, unsigned long stream, @@ -4187,7 +4187,7 @@ static unsigned long index_upload(const void *data, unsigned long size) device.index_offset = 0; } offset = device.index_offset; -#ifdef HALO_ANDROID +#ifdef HALO_GLES host_gl_buffer_write(GL_ELEMENT_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)length, data); #else buffer_upload(GL_ELEMENT_ARRAY_BUFFER, offset, length, data); @@ -4205,7 +4205,7 @@ static void attribute_format(const struct vertex_element *element, GLint *size, case D3DVSDT_FLOAT2: *size = 2; *type = GL_FLOAT; break; case D3DVSDT_FLOAT3: case D3DVSDT_FLOAT2H: *size = 3; *type = GL_FLOAT; break; case D3DVSDT_FLOAT4: *size = 4; *type = GL_FLOAT; break; -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ES has no BGRA attributes: stream_upload_swizzled swaps the bytes */ case D3DVSDT_D3DCOLOR: *size = 4; *type = GL_UNSIGNED_BYTE; *normalized = GL_TRUE; break; #else @@ -4230,7 +4230,7 @@ static void attribute_format(const struct vertex_element *element, GLint *size, /* upload vertices [first, first + count) of every stream the declaration uses and point the attributes at them; attribute data then starts at vertex 0 of the uploaded range */ -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ES has no BGRA attributes, so a stream with colours is swizzled as it is uploaded (stream_upload_swizzled) and cannot come from the mirror */ static BOOL stream_has_colors(const struct vertex_shader_object *declaration, unsigned long stream) @@ -4254,7 +4254,7 @@ static void setup_streams(unsigned long first, unsigned long count) BOOL placed[16] = { FALSE }; BOOL enabled[XGPU_VERTEX_ATTRIBUTE_COUNT] = { FALSE }; unsigned long index, total = 0; -#ifndef HALO_ANDROID +#ifndef HALO_GLES struct vertex_layout layout; unsigned long streams_used = 0; @@ -4276,7 +4276,7 @@ static void setup_streams(unsigned long first, unsigned long count) placed[stream] = TRUE; stream_buffers[stream] = 0; base = (unsigned long)PLATFORM_PHYSICAL_TO_VIRTUAL(device.streams[stream].data) + first * stride; -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (!stream_has_colors(declaration, stream)) #endif if (mirror_range(base, bytes, &stream_buffers[stream], &stream_offsets[stream], NULL)) @@ -4301,7 +4301,7 @@ static void setup_streams(unsigned long first, unsigned long count) const unsigned char *base = PLATFORM_PHYSICAL_TO_VIRTUAL(device.streams[stream].data); unsigned long bytes = stride ? stride * count : 64; -#ifdef HALO_ANDROID +#ifdef HALO_GLES stream_offsets[stream] = stream_upload_swizzled(declaration, stream, base + first * stride, bytes, stride); #else stream_offsets[stream] = stream_upload(base + first * stride, bytes); @@ -4309,7 +4309,7 @@ static void setup_streams(unsigned long first, unsigned long count) stream_buffers[stream] = device.stream_buffer; stats.streamed_bytes += bytes; } -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (element->type == D3DVSDT_NORMPACKED3) { state_attribute_stream(element->reg, (GLuint)stream, stream_buffers[stream], 1, GL_UNSIGNED_INT, GL_FALSE, @@ -4335,7 +4335,7 @@ static void setup_streams(unsigned long first, unsigned long count) #endif enabled[element->reg] = TRUE; } -#ifndef HALO_ANDROID +#ifndef HALO_GLES /* (the layout follows from the declaration and the streams it has, so the same two have the same vertex array: no need to look it up) */ if (!declaration->vertex_array || declaration->vertex_array_streams != streams_used) @@ -4447,7 +4447,7 @@ void WINAPI D3DDevice_DrawIndexedVertices(D3DPRIMITIVETYPE primitive_type, UINT return; /* quads are drawn as triangles, from indices made for the draw */ mirrored = primitive_type != D3DPT_QUADLIST && -#ifdef HALO_ANDROID +#ifdef HALO_GLES xgpu_capabilities.base_vertex && #endif mirror_range((unsigned long)index_data, vertex_count * sizeof(WORD), &index_buffer, &index_offset, &generation); @@ -4470,7 +4470,7 @@ void WINAPI D3DDevice_DrawIndexedVertices(D3DPRIMITIVETYPE primitive_type, UINT indices = quad_indices(index_data, vertex_count, &count); source = indices; } -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (!xgpu_capabilities.base_vertex) { /* the indices are copied anyway: rebase them */ @@ -4525,7 +4525,7 @@ void WINAPI D3DDevice_End(void) return; trace_draw("immediate", type, count, device.immediate_vertices); offset = stream_upload(device.immediate_vertices, count * stride); -#ifdef HALO_ANDROID +#ifdef HALO_GLES for (index = 0; index < XGPU_VERTEX_ATTRIBUTE_COUNT; index++) { state_attribute_stream(index, 0, device.stream_buffer, 4, GL_FLOAT, GL_FALSE, FALSE, (GLsizei)stride, @@ -4723,7 +4723,7 @@ void halo_screen_anti_alias(short x0, short y0, short x1, short y1) /* ---------- presentation */ -#ifndef HALO_ANDROID +#ifndef HALO_GLES /* the screenshot key's PNG (controls.screenshot; the Android guest has none of the SDL calls it takes) */ static void write_key_screenshot(struct render_target_entry *target) @@ -4842,7 +4842,7 @@ static void write_screenshot(struct render_target_entry *target) image viewers would show it as transparency */ for (row = 0; row < width * height; row++) { -#ifdef HALO_ANDROID +#ifdef HALO_GLES unsigned char red = pixels[row * 4]; pixels[row * 4] = pixels[row * 4 + 2]; @@ -4870,7 +4870,7 @@ static void write_screenshot(struct render_target_entry *target) free(pixels); } -#ifndef HALO_ANDROID +#ifndef HALO_GLES /* the GPU copies the counts of the previous frame's tests into their slots (given by name: Mesa's GL thread waits for everything before a glGetQueryObjectuiv, even one into a bound buffer). A copy right after each @@ -4916,12 +4916,12 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat platform_log("present back buffer %08lx texture %u", (unsigned long)device.back_buffer.Data, back_buffer->target.texture); render_target_resolve(&back_buffer->target); -#ifndef HALO_ANDROID +#ifndef HALO_GLES visibility_copy_batch(); #endif if (screenshot_every > 0 && device.frame % (unsigned long)screenshot_every == 0) write_screenshot(back_buffer); -#ifndef HALO_ANDROID +#ifndef HALO_GLES if (platform_screenshot_take_request()) write_key_screenshot(back_buffer); #endif @@ -4950,7 +4950,7 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat xgpu_gl_state_invalidate(); memory_watch_begin_frame(); xgpu_texture_cache_begin_frame(); -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (xgpu_capabilities.atomic_counters) { /* this frame's counts, for when the GPU is done with it (the diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 2611ae2b..7b712936 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -1366,7 +1366,7 @@ static void audio_start(void) spec.format = SDL_AUDIO_F32; spec.channels = OUTPUT_CHANNELS; spec.freq = OUTPUT_RATE; -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST /* frames per callback: on Android each callback is handed to a thread that can run the guest (host_sdl.c): 512 left it too little time and the menus' music broke up, which 1024 does not (about 21 ms at 48 kHz, diff --git a/port/linux/src/gl.h b/port/linux/src/gl.h index e0a58157..5c57ea51 100644 --- a/port/linux/src/gl.h +++ b/port/linux/src/gl.h @@ -16,7 +16,7 @@ Windows it is __stdcall too, and SDL would include windows.h without it) */ #ifndef _WIN32 #undef APIENTRY #endif -#ifdef HALO_ANDROID +#ifdef HALO_GLES #include #include #define GLAPIENTRY GL_APIENTRY @@ -25,7 +25,7 @@ Windows it is __stdcall too, and SDL would include windows.h without it) */ #endif #pragma pop_macro("APIENTRY") -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* OpenGL ES 3.2 (port/android/README.md); tools/android_gl_stubs.py reads this list to generate the guest's entry points */ /* ANDROID_GL_FUNCTIONS_BEGIN */ @@ -261,7 +261,7 @@ GL_FUNCTIONS(GL_DECLARE_FUNCTION) /* call sites use the ordinary names; gl_functions.c, which defines the pointers, sees the declarations without these aliases */ #ifndef GL_FUNCTIONS_DEFINE -#ifdef HALO_ANDROID +#ifdef HALO_GLES #define glGetString halo_glGetString #define glGetIntegerv halo_glGetIntegerv #define glCopyImageSubData halo_glCopyImageSubData diff --git a/port/linux/src/menu_files.c b/port/linux/src/menu_files.c index ce795529..aa3e49a8 100644 --- a/port/linux/src/menu_files.c +++ b/port/linux/src/menu_files.c @@ -99,7 +99,7 @@ static long file_add(const char *path, const unsigned char *data, unsigned long static unsigned char *file_read(const char *path, unsigned long *size) { -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST FILE *file = fopen(path, "rb"); unsigned char *data = NULL; long length; diff --git a/port/linux/src/msvc_crt.c b/port/linux/src/msvc_crt.c index afeaddac..217d04db 100644 --- a/port/linux/src/msvc_crt.c +++ b/port/linux/src/msvc_crt.c @@ -295,7 +295,7 @@ static unsigned short msvc_to_control_word(unsigned int value, unsigned short wo return word; } -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST /* AArch64: the rounding mode lives in FPCR.RMode, the sticky exception flags in FPSR. Precision control and exception unmasking have no equivalent; the rest of the MSVC control word is only remembered. */ diff --git a/port/linux/src/nv2a_psh.c b/port/linux/src/nv2a_psh.c index 211146d1..8ee338be 100644 --- a/port/linux/src/nv2a_psh.c +++ b/port/linux/src/nv2a_psh.c @@ -314,7 +314,7 @@ static void dot_input(struct xgpu_text *text, const DWORD *state, int stage) } } -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ES samplers have no LOD bias: pass D3DTSS_MIPMAPLODBIAS to the lookup */ #define SAMPLE_BIAS ", texture_lod_bias[%d]" #define SHADER_VERSION \ @@ -334,21 +334,21 @@ static void sample(struct xgpu_text *text, const struct nv2a_pixel_shader_key *k { case _xgpu_sampler_3d: xgpu_text_append(text, "texture(tex%d, (%s).xyz" SAMPLE_BIAS ")", stage, coordinates -#ifdef HALO_ANDROID +#ifdef HALO_GLES , stage #endif ); break; case _xgpu_sampler_cube: xgpu_text_append(text, "texture(tex%d, (%s).xyz" SAMPLE_BIAS ")", stage, coordinates -#ifdef HALO_ANDROID +#ifdef HALO_GLES , stage #endif ); break; default: xgpu_text_append(text, "texture(tex%d, (%s).xy * texture_scale[%d].xy" SAMPLE_BIAS ")", stage, coordinates, stage -#ifdef HALO_ANDROID +#ifdef HALO_GLES , stage #endif ); @@ -594,7 +594,7 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key) if (combiner_count > 8) combiner_count = 8; -#ifdef HALO_ANDROID +#ifdef HALO_GLES xgpu_text_append(&text, "#version %s\n", xgpu_capabilities.shading_language); if (key->count_samples) { @@ -741,7 +741,7 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key) xgpu_text_append(&text, "\tresult = vec4(t0.rgb, 1.0);\n"); if (config_boolean("debug.gpu_debug_flat")) xgpu_text_append(&text, "\tresult = xD0.a > 0.0 ? vec4(xD0.rgb, 1.0) : vec4(1.0, 0.0, 1.0, 1.0);\n"); -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (key->count_samples) xgpu_text_append(&text, "\tatomicCounterIncrement(visible_samples);\n"); #endif diff --git a/port/linux/src/nv2a_vsh.c b/port/linux/src/nv2a_vsh.c index 07669656..20df75e0 100644 --- a/port/linux/src/nv2a_vsh.c +++ b/port/linux/src/nv2a_vsh.c @@ -329,7 +329,7 @@ BOOL nv2a_vertex_shader_lighting(const DWORD *instructions, unsigned long instru /* ---------- translation */ static const char shader_prologue[] = -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* the #version line comes first, from the context's capabilities */ "precision highp float;\n" "precision highp int;\n" @@ -389,7 +389,7 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru struct xgpu_text text = { 0 }; unsigned long index; -#ifdef HALO_ANDROID +#ifdef HALO_GLES xgpu_text_append(&text, "#version %s\n", xgpu_capabilities.shading_language); #endif xgpu_text_append(&text, "%s", shader_prologue); @@ -565,7 +565,7 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru "\tif (!(abs(position.w) > 0.0))\n" "\t\tposition = vec4(0.0, 0.0, 0.0, -1.0);\n" "\tgl_Position = position;\n" -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* what glClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE) does on desktop GL: rows from the top, depth 0..1 */ "\tgl_Position.y = -gl_Position.y;\n" diff --git a/port/linux/src/platform.h b/port/linux/src/platform.h index e1bf6cfb..31a3b335 100644 --- a/port/linux/src/platform.h +++ b/port/linux/src/platform.h @@ -114,7 +114,7 @@ that window at start-up and hands out page-granular blocks from it, so the physical/virtual arithmetic the game and Direct3D rely on keeps working. */ #define PLATFORM_CONTIGUOUS_BASE 0x80000000UL -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST /* 128 MB, a development kit's: Android's guest image is linked just above the window (port/android/include/halo_android_abi.h) */ #define PLATFORM_CONTIGUOUS_SIZE 0x08000000UL diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 5fa187c0..f7335c96 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -558,7 +558,7 @@ static void config_path(char *path, size_t size) /* the whole file, NUL terminated, or NULL; free() it */ static char *config_read_file(const char *path, size_t *size) { -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST FILE *file = fopen(path, "rb"); char *text = NULL; long length; @@ -601,7 +601,7 @@ static char *config_read_file(const char *path, size_t *size) static int config_write_file(const char *path, const char *text) { -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST FILE *file = fopen(path, "wb"); int written; diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index b4b647e7..b284b088 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -765,7 +765,7 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) if (!platform_sdl_initialize()) return FALSE; -#ifdef HALO_ANDROID +#ifdef HALO_GLES SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); @@ -829,7 +829,7 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) platform_fullscreen_kind_apply(); #endif platform_gl_context = SDL_GL_CreateContext(platform_window); -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ES 3.2 where the driver has it, otherwise the renderer makes do with 3.0 plus extensions */ if (!platform_gl_context) @@ -1573,7 +1573,7 @@ void platform_pump_events(void) #endif break; case SDL_EVENT_GAMEPAD_ADDED: -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST /* (the guest reaches SDL only through host_imports.list, which has no SDL_GetGamepadName) */ SDL_OpenGamepad(event.gdevice.which); diff --git a/port/linux/src/xbox_textures.c b/port/linux/src/xbox_textures.c index 558fe055..9e3840dc 100644 --- a/port/linux/src/xbox_textures.c +++ b/port/linux/src/xbox_textures.c @@ -22,7 +22,7 @@ memory_watch.c detects that by write-protecting the pages. #include "../game/cache_file_formats.h" #include -#ifdef HALO_ANDROID +#ifdef HALO_GLES #define GL_BGRA GL_RGBA #endif #include @@ -423,7 +423,7 @@ static BOOL decode_level(const struct xgpu_texture_description *description, uns return TRUE; } -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ---------- DXT decoding, for ES drivers without S3TC (Mali) */ static unsigned long color565(unsigned long value) @@ -563,7 +563,7 @@ static GLenum compressed_format(unsigned char kind) /* debug.texture_dump_directory writes level 0 of every upload as a TGA, read back from GL */ static void texture_dump(GLenum target, const struct xgpu_texture_description *description) { -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ES cannot read textures back */ (void)target; (void)description; @@ -706,7 +706,7 @@ static void upload(GLuint texture, GLenum target, const struct xgpu_texture_desc unsigned long *converted; unsigned long face, level; -#ifdef HALO_ANDROID +#ifdef HALO_GLES decode_compressed = description->compressed && !xgpu_capabilities.s3tc; #endif converted = description->compressed && !decode_compressed ? NULL : malloc(largest * sizeof(unsigned long)); @@ -723,7 +723,7 @@ static void upload(GLuint texture, GLenum target, const struct xgpu_texture_desc { GLint channels[4] = { GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA }; -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* converted texels are BGRA in memory (32-bit ARGB words); ES takes RGBA */ if (converted) @@ -770,7 +770,7 @@ static void upload(GLuint texture, GLenum target, const struct xgpu_texture_desc } else { -#ifdef HALO_ANDROID +#ifdef HALO_GLES if (decode_compressed) dxt_decode_level(information.kind, source, (unsigned long)width, (unsigned long)height, (unsigned long)depth, converted); diff --git a/port/linux/src/xgpu.h b/port/linux/src/xgpu.h index 8d371c45..982f24b7 100644 --- a/port/linux/src/xgpu.h +++ b/port/linux/src/xgpu.h @@ -13,7 +13,7 @@ device itself (d3d8_gl.c). #include "platform.h" #include "gl.h" -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* OpenGL ES features that are optional (d3d8_gl.c gl_initialize) */ struct xgpu_capabilities { @@ -147,7 +147,7 @@ struct nv2a_pixel_shader_key char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key); -#ifdef HALO_ANDROID +#ifdef HALO_GLES /* ES samplers have no LOD bias of their own */ #define XGPU_PIXEL_UNIFORMS_ES "uniform vec4 texture_lod_bias;\n" #else diff --git a/port/linux/src/xgpu_post.c b/port/linux/src/xgpu_post.c index 03461ad2..a7bd4669 100644 --- a/port/linux/src/xgpu_post.c +++ b/port/linux/src/xgpu_post.c @@ -24,7 +24,7 @@ anti_aliasing_values). #include -#ifndef HALO_ANDROID +#ifndef HALO_GLES #include "zlib_prefixed.h" /* port/third_party/smaa, embedded by tools/embed_assets.py: the shader's @@ -74,7 +74,7 @@ static struct static void shader_header(struct xgpu_text *text) { -#ifdef HALO_ANDROID +#ifdef HALO_GLES xgpu_text_append(text, "#version %s\n" "precision highp float;\n" @@ -188,7 +188,7 @@ static const char fxaa_source[] = "\tresult = vec4(color_at(position + max(varies ? pixel_offset : 0.0, subpixel) * across), 1.0);\n" "}\n"; -#ifndef HALO_ANDROID +#ifndef HALO_GLES /* the main functions of SMAA's passes, after SMAA.hlsl; its vertex functions' offsets are found for each pixel */ static const char *const smaa_mains[] = @@ -267,7 +267,7 @@ static GLuint program_build(int which) { xgpu_text_append(&text, "%s", fxaa_source); } -#ifndef HALO_ANDROID +#ifndef HALO_GLES else if (xgpu_smaa_shader_size) { xgpu_text_append(&text, @@ -358,7 +358,7 @@ static void textures_fit(BOOL smaa, unsigned long width, unsigned long height) } } -#ifndef HALO_ANDROID +#ifndef HALO_GLES /* a lookup texture of SMAA's from its zlib stream; 0 if it does not inflate */ static GLuint lookup_texture(const unsigned int *stream, unsigned long stream_size, GLenum format, @@ -437,7 +437,7 @@ BOOL xgpu_post_prepare(BOOL smaa) return FALSE; } } -#ifndef HALO_ANDROID +#ifndef HALO_GLES if (smaa && !post.area) { post.area = lookup_texture(xgpu_smaa_area_texture, xgpu_smaa_area_texture_size, GL_RG8, GL_RG, @@ -478,7 +478,7 @@ BOOL xgpu_post_anti_alias(BOOL smaa, GLuint framebuffer, unsigned long width, un glDisable(GL_BLEND); glDisable(GL_CULL_FACE); glDisable(GL_POLYGON_OFFSET_FILL); -#ifndef HALO_ANDROID +#ifndef HALO_GLES glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); #endif glBindVertexArray(post.vertex_array); diff --git a/source/bungie_net/network/transport_address.c b/source/bungie_net/network/transport_address.c index aa142cfb..98aa8be7 100644 --- a/source/bungie_net/network/transport_address.c +++ b/source/bungie_net/network/transport_address.c @@ -104,11 +104,11 @@ symbols in this file: /* ---------- globals */ -#ifndef HALO_ANDROID /* Mach-O section names differ; the default is .bss anyway */ +#ifndef HALO_ARM64_GUEST /* Mach-O section names differ; the default is .bss anyway */ #pragma bss_seg(".bss") #endif static char transport_address_string[256]; -#ifndef HALO_ANDROID +#ifndef HALO_ARM64_GUEST #pragma bss_seg() #endif diff --git a/source/cseries/stack_walk_windows.c b/source/cseries/stack_walk_windows.c index 70c20987..00d49b0b 100644 --- a/source/cseries/stack_walk_windows.c +++ b/source/cseries/stack_walk_windows.c @@ -342,7 +342,7 @@ void stack_walk_with_context( for (frame_number = levels_dumped - 1; frame_number >= levels_to_ignore; frame_number--) { -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST /* the call site (the BL before the return address), for llvm-symbolizer --obj=build/android/halo_guest.elf */ unsigned long routine_address = routine_addresses[frame_number] - 4; @@ -762,7 +762,7 @@ static unsigned long walk_up( if (walk_up_current_frame) { -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST /* an AArch64 frame record: the caller's frame pointer, then the return address, 8 bytes each (the upper halves are zero) */ routine_address = ((unsigned long *)walk_up_current_frame)[2]; diff --git a/source/effects/material_effects.c b/source/effects/material_effects.c index 47221aa0..acbf4268 100644 --- a/source/effects/material_effects.c +++ b/source/effects/material_effects.c @@ -45,11 +45,11 @@ enum /* ---------- globals */ -#ifndef HALO_ANDROID /* Mach-O section names differ; the default is .bss anyway */ +#ifndef HALO_ARM64_GUEST /* Mach-O section names differ; the default is .bss anyway */ #pragma bss_seg(".bss") #endif boolean debug_material_effects; -#ifndef HALO_ANDROID +#ifndef HALO_ARM64_GUEST #pragma bss_seg() #endif diff --git a/source/hs/hs.c b/source/hs/hs.c index 465b42a4..ab2877c8 100644 --- a/source/hs/hs.c +++ b/source/hs/hs.c @@ -14558,7 +14558,7 @@ static void player_effect_screen_fade_in_evaluate( double value1 = arguments->value1; double value2 = arguments->value2; -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST player_effect_screen_fade_in(*(real const *)&arguments->value0, (real)value1, (real)value2, arguments->value3); #else player_effect_screen_fade_in(arguments->value0, value1, value2, arguments->value3); @@ -14581,7 +14581,7 @@ static void player_effect_screen_fade_out_evaluate( double value1 = arguments->value1; double value2 = arguments->value2; -#ifdef HALO_ANDROID +#ifdef HALO_ARM64_GUEST player_effect_screen_fade_out(*(real const *)&arguments->value0, (real)value1, (real)value2, arguments->value3); #else player_effect_screen_fade_out(arguments->value0, value1, value2, arguments->value3); diff --git a/source/math/random_math.c b/source/math/random_math.c index 4fc02dd6..4473f13c 100644 --- a/source/math/random_math.c +++ b/source/math/random_math.c @@ -92,11 +92,11 @@ direction3d_from_table( /* ---------- globals */ -#ifndef HALO_ANDROID /* Mach-O section names differ; the default is .bss anyway */ +#ifndef HALO_ARM64_GUEST /* Mach-O section names differ; the default is .bss anyway */ #pragma bss_seg(".bss") #endif static struct random_math_globals random_math_globals; -#ifndef HALO_ANDROID +#ifndef HALO_ARM64_GUEST #pragma bss_seg() #endif diff --git a/source/memory/crc.c b/source/memory/crc.c index c2f4ad14..26768af8 100644 --- a/source/memory/crc.c +++ b/source/memory/crc.c @@ -39,11 +39,11 @@ struct crc_globals /* ---------- globals */ -#ifndef HALO_ANDROID /* Mach-O section names differ; the default is .bss anyway */ +#ifndef HALO_ARM64_GUEST /* Mach-O section names differ; the default is .bss anyway */ #pragma bss_seg(".bss") #endif static struct crc_globals crc_globals; -#ifndef HALO_ANDROID +#ifndef HALO_ARM64_GUEST #pragma bss_seg() #endif diff --git a/source/objects/object_types.c b/source/objects/object_types.c index 87b1b869..d6c9a90f 100644 --- a/source/objects/object_types.c +++ b/source/objects/object_types.c @@ -473,11 +473,11 @@ struct object_type_definition *object_type_definitions[NUMBER_OF_OBJECT_TYPES] = struct object_type_definition *first_object_type_definition; /* VC7 otherwise emits this tentative definition as a common symbol. */ -#ifndef HALO_ANDROID /* Mach-O section names differ; the default is .bss anyway */ +#ifndef HALO_ARM64_GUEST /* Mach-O section names differ; the default is .bss anyway */ #pragma bss_seg(".bss") #endif static word processed_bsp_flags; -#ifndef HALO_ANDROID +#ifndef HALO_ARM64_GUEST #pragma bss_seg() #endif diff --git a/source/rasterizer/rasterizer.c b/source/rasterizer/rasterizer.c index 15ba5dd0..dee80bc6 100644 --- a/source/rasterizer/rasterizer.c +++ b/source/rasterizer/rasterizer.c @@ -686,12 +686,12 @@ struct rasterizer_debug_options rasterizer_debug_options = { 0 }, /* pad8A[2] */ }; /* No PDB name survives for this target-owned BSS symbol. */ -#ifndef HALO_ANDROID /* Mach-O section names differ; the default is .bss anyway */ +#ifndef HALO_ARM64_GUEST /* Mach-O section names differ; the default is .bss anyway */ #pragma bss_seg(".bss") #endif static long bss_004662ec; real_argb_color *global_rasterizer_model_ambient_reflection_tint; -#ifndef HALO_ANDROID +#ifndef HALO_ARM64_GUEST #pragma bss_seg() #endif diff --git a/source/text/unicode.c b/source/text/unicode.c index 21dbf388..a8ae5b7a 100644 --- a/source/text/unicode.c +++ b/source/text/unicode.c @@ -269,11 +269,11 @@ enum /* ---------- globals */ -#ifndef HALO_ANDROID /* Mach-O section names differ; the default is .bss anyway */ +#ifndef HALO_ARM64_GUEST /* Mach-O section names differ; the default is .bss anyway */ #pragma bss_seg(".bss") #endif static wchar_t bss_004c1a08[0x100]; -#ifndef HALO_ANDROID +#ifndef HALO_ARM64_GUEST #pragma bss_seg() #endif diff --git a/tools/android_build.py b/tools/android_build.py index 1e03410a..9f787a57 100644 --- a/tools/android_build.py +++ b/tools/android_build.py @@ -71,6 +71,10 @@ GUEST_ABI_FLAGS = [ "-fno-define-target-os-macros", "-D__linux__=1", "-D__unix__=1", + # the ILP32 guest's code paths, the OpenGL ES renderer's, and the app's + # (in the port's sources, each its own macro) + "-DHALO_ARM64_GUEST=1", + "-DHALO_GLES=1", "-DHALO_ANDROID=1", # ARMv8.0: nothing the emulator's binary translation or an older # device could lack (Darwin targets otherwise assume pointer From c463fabed4db6ff297927e7fb73c3c55c318ff10 Mon Sep 17 00:00:00 2001 From: saulo benigno Date: Fri, 9 Oct 2026 12:00:18 -0300 Subject: [PATCH 117/187] Add in-game Settings to single-player campaign --- port/assets/menus/README.md | 14 -- port/assets/menus/ce/in_game.xml | 63 ------ port/assets/menus/menus.json | 1 - port/linux/README.md | 5 + port/linux/game/menu_functions.c | 124 ++++++------ port/linux/game/menu_tags.c | 327 +++++++++++++++++-------------- source/interface/player_ui.c | 7 + source/interface/player_ui.h | 3 + source/interface/ui_widget.c | 133 ------------- tools/ce_menus.py | 3 +- tools/port_settings.py | 90 --------- 11 files changed, 250 insertions(+), 520 deletions(-) delete mode 100644 port/assets/menus/ce/in_game.xml diff --git a/port/assets/menus/README.md b/port/assets/menus/README.md index 65827c06..32190ad1 100644 --- a/port/assets/menus/README.md +++ b/port/assets/menus/README.md @@ -22,20 +22,6 @@ the full screen, the frame rate, the volumes, internet play, the multiplayer HUD), the profile's controller settings, and the keyboard and mouse's controls. -In a game, the pause menus are the maps' own. The widgets named `in_game/...` -(`ce/in_game.xml`, also from `tools/port_settings.py`) are built in the -campaign's maps instead of `ui.map`, with only what they lead to (the screens -they open, and their bitmaps and strings): they are drawn with the pause -menus' art, which `ui.map` has not. `in_game/settings_button` is SETTINGS, -which the campaign's full screen pause menu (with one player here) gets after -RESUME GAME (`source/interface/ui_widget.c`), on the desktop; a multiplayer -map's pause menu gets its own (`port/linux/game/menu_tags.c`'s -`pause_patch`). Its screen opens Edit Profile's Controls Setup, Gamepads, Mouse -Setup, Audio Setup and Video Setup over the pause menu's dim. They save to -`config.toml` as they do from the main menu, but Gamepads, whose changes -Edit Profile saves there, saves only its controller settings to the -player's active profile, on OK (`port active profile edit begin` and `end`). - To change the menus without building the game, put files in a `menus` folder next to `config.toml`. A file with the same path as one here replaces it. On the desktop, any other `.xml` file there is added too. A PNG that a `` diff --git a/port/assets/menus/ce/in_game.xml b/port/assets/menus/ce/in_game.xml deleted file mode 100644 index ed64de4f..00000000 --- a/port/assets/menus/ce/in_game.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json index 1364c68c..ad0c56a7 100644 --- a/port/assets/menus/menus.json +++ b/port/assets/menus/menus.json @@ -3,7 +3,6 @@ "files": [ "ce/bitmaps.xml", "ce/error.xml", - "ce/in_game.xml", "ce/main_menu.campaign_select.xml", "ce/main_menu.difficulty_select.xml", "ce/main_menu.gametype_select.xml", diff --git a/port/linux/README.md b/port/linux/README.md index 95b728a2..fa5da14b 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -289,6 +289,11 @@ stay. The Xbox's pause box is drawn taller to hold them (a redraw, what is below its list moves down. The few pictures of the settings that come from the main menu's map are not drawn there. +In a single-player campaign, the pause menu has SETTINGS too, after RESUME +GAME, its rows closer in the same box. It opens the same settings, while the +game stays paused, with only Controls, Gamepads, Mouse, Audio and Video +Setup; Gamepads' OK saves the profile at once. + The menus are XML files in `port/assets/menus` (`tools/ce_menus.py` writes them from the PC version's tags), which the game contains. To change them, put files in a `menus` folder next to `config.toml`: a file with the same diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 5b81d634..58a24668 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -10,9 +10,6 @@ calls them from PC_MENU_FUNCTION_BASE on): - the PC version's "profile set edit begin" begins editing the first player profile, as its settings screens need, and fails with none (its handlers then open the screens that make one); -- in the campaign, "port active profile edit begin" and "port active profile - edit end" edit the player's active profile for GAMEPADS (in_game.xml) and - keep the controller settings it sets; - "gamespy screen init" hides the server browser's error and filter panels, and the title of the mode "mp type set mode" did not choose (Internet or LAN); @@ -518,64 +515,6 @@ boolean pc_menu_profile_edit_begin(void) return TRUE; } -/* in a game, GAMEPADS (port/assets/menus/ce/in_game.xml) sets the player's -active profile, which no Edit Profile screen is there to save: "port active -profile edit begin" (A on its row) edits the profile, and "port active -profile edit end" (its screen gone) puts the controller settings OK wrote -into the profile as it is stored and as the game has it, nothing else (the -game's copy can be older than the file: Edit Profile saves only the file), -then saves the one and applies the other, as the game's own pause menu does -with its inverted look (player0_look_invert_pitch). FALSE if the player has -no profile. */ -static long active_profile_edited = NONE; - -static boolean active_profile_edit_begin(short local_player) -{ - long index = player_ui_get_active_player_profile_index(local_player); - - active_profile_edited = NONE; - if (index == NONE) - return FALSE; - player_ui_begin_editing_profile(index); - if (!player_ui_get_edit_player_profile()) - return FALSE; - active_profile_edited = index; - return TRUE; -} - -static boolean active_profile_edit_end(short local_player) -{ - struct player_profile *edited = player_ui_get_edit_player_profile(); - long index = active_profile_edited; - - active_profile_edited = NONE; - if (index == NONE) - return TRUE; - if (edited && player_ui_edit_profile_is_dirty()) - { - struct player_profile profile; - - if (player_profile_get(index, &profile)) - { - profile.controller_settings = edited->controller_settings; - player_profile_save(index, &profile); - if (player_ui_get_active_player_profile_index(local_player) == index) - { - player_ui_get_active_player_profile(local_player, &profile); - profile.controller_settings = edited->controller_settings; - player_ui_set_active_player_profile(local_player, index, &profile); - } - platform_log("menus: the profile's controller settings saved"); - } - else - { - platform_log("menus: could not read the profile; its controller settings not saved"); - } - } - player_ui_end_editing_profile(); - return TRUE; -} - /* ---------- the campaign */ /* the rows of its lists (main_menu/new_select/list_item_0 to 10) */ @@ -4002,6 +3941,57 @@ static boolean profile_save_changes(struct widget_instance *widget, boolean *wid return TRUE; } +/* only what changed from before to after, into the local player's active +profile, which the campaign saves its progress from */ +static void active_profile_take_changes(short local_player, long index, struct player_profile const *before, + struct player_profile const *after) +{ + struct player_profile active; + byte const *from = (byte const *)before; + byte const *to = (byte const *)after; + byte *changed = (byte *)&active; + long offset; + + player_ui_get_active_player_profile(local_player, &active); + for (offset = 0; offset < (long)sizeof(active); offset++) + { + if (to[offset] != from[offset]) + changed[offset] = to[offset]; + } + player_ui_set_active_player_profile(local_player, index, &active); +} + +/* "port profile settings save" (Gamepads' OK in a single-player campaign: +menu_tags.c's pause_settings_patch): the profile saved at once, not on +Settings' OK, its changes made the player's own, and edited again from what +was saved, for Settings to go on with */ +static boolean profile_settings_save(struct widget_instance *widget) +{ + struct player_profile *edited = player_ui_get_edit_player_profile(); + long index = player_ui_get_edit_profile_index(); + struct player_profile before, after; + short local_player; + + if (edited) + before = *edited; + settings_each(screen_of(widget), setting_changed_save); + if (!edited || !player_ui_edit_profile_is_dirty()) + return TRUE; + after = *edited; + if (!player_ui_save_profile()) + { + platform_log("menus: could not save the profile's changes"); + return campaign_fail(); + } + for (local_player = 0; local_player < MAXIMUM_LOCAL_PLAYERS; local_player++) + { + if (player_ui_get_active_player_profile_index(local_player) == index) + active_profile_take_changes(local_player, index, &before, &after); + } + player_ui_begin_editing_profile(index); + return TRUE; +} + /* "port pause end game" (the in-game pause menu's END GAME, the host's: menu_tags.c's pause_patch): the game ends as its time limit would, its players staying for the next (the carnage report, then the host's PICK GAME) */ @@ -5369,14 +5359,6 @@ boolean pc_menu_event_function_invoke( { return pc_menu_profile_edit_begin(); } - else if (!strcmp(name, "port active profile edit begin")) - { - return active_profile_edit_begin(controller_of(widget)); - } - else if (!strcmp(name, "port active profile edit end")) - { - return active_profile_edit_end(controller_of(widget)); - } /* (the press posted is the controller's that chose the button: a split screen player's LEAVE is theirs) */ else if (!strcmp(name, "mouse emit accept event")) @@ -5495,6 +5477,10 @@ boolean pc_menu_event_function_invoke( { return profile_save_changes(widget, widget_deleted); } + else if (!strcmp(name, "port profile settings save")) + { + return profile_settings_save(widget); + } else if (!strcmp(name, "direct ip connect go")) { return direct_link_from_clipboard(); diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index 349382be..301ad91e 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -29,15 +29,13 @@ A multiplayer map gets them too, for its in-game pause menu's SETTINGS pictures, sounds and screens) are none, a missing picture drawn blank, and no screen pauses the game (a network game's clock runs on). -The campaign's maps get the in-game widgets (in_game/..., which ui.map and -the multiplayer maps do not get: they are drawn with the pause menus' art, -which ui.map has not), and only what they lead to: the settings screens they -open, with their bitmaps and strings (ui_widget.c adds SETTINGS to the -campaign's pause menu). +So does a single-player campaign's map (pause_campaign_patch), whose screens +pause it as its pause menu does. */ #include "cseries.h" #include "bitmaps/bitmap_group.h" +#include "game/game.h" #include "tag_files/tag_groups.h" #include "text/text_group.h" #include "rasterizer/xbox/rasterizer_xbox_hardware_bitmaps.h" @@ -57,6 +55,7 @@ int platform_audio_devices(int recording, char (*names)[128], int maximum); /* the game's network server (this machine hosts: network_game_globals.c) */ void *global_network_game_server_get(void); +boolean network_game_is_active(void); /* cache_files.c's (port) */ void *cache_files_tag_instances(long *count); @@ -77,10 +76,11 @@ boolean pc_menu_frame_placement(struct bitmap_data const *bitmap, short *x, shor /* ---------- constants */ #define PC_MENU_TAG_PREFIX "pc\\" -#define IN_GAME_WIDGET_PREFIX "in_game/" /* the multiplayer maps' widget collection, whose pause screens pause_patch adds to */ #define MULTIPLAYER_COLLECTION "ui\\shell\\multiplayer" +/* the campaign's full screen pause screen, which pause_campaign_patch adds to */ +#define CAMPAIGN_PAUSE_SCREEN "ui\\shell\\solo_game\\pause_game\\pause_game" /* the tags pause_patch adds: two buttons and their texts */ #define PAUSE_PATCH_TAGS 4 /* what a frame of ui.map's is drawn as in a game map, which has not got it */ @@ -380,10 +380,10 @@ static char const *const port_function_names[] = /* (Settings' OK: the Xbox's fails when the profile has no changes, the settings' screens having written theirs to config.toml) */ "player profile save changes", - /* (in the campaign: GAMEPADS on the player's active profile) */ - "port active profile edit begin", "port active profile edit end", /* (the server browser's password screen) */ "port password init", "port password edit", "port password join", "port password back", + /* (Gamepads' OK in a single-player campaign: pause_settings_patch) */ + "port profile settings save", }; /* the PC version's game data functions that the Xbox's have not, from @@ -421,8 +421,6 @@ static struct struct pc_menu_setting *settings; long setting_count; boolean loaded; - /* built in a campaign map: the in-game widgets, not the main menu */ - boolean in_game; char root[300]; } menu_tags; @@ -440,14 +438,12 @@ static struct long *strings_tags; long *text_tags; long *spinner_tags; - /* which widgets, bitmaps and string lists are built */ - boolean *wanted_widgets; - boolean *wanted_bitmaps; - boolean *wanted_strings; boolean failed; - /* a multiplayer map's build (the pause menu's SETTINGS): what the map - has not got is none, not a failure */ + /* a game map's build (the pause menu's SETTINGS): what the map has not + got is none, not a failure */ boolean game_map; + /* a single-player campaign's map's, whose screens pause the game */ + boolean campaign; } build; /* ---------- private code */ @@ -567,70 +563,6 @@ static long widget_named(char const *name) return find(name, build.menus->widget_count, widget_name); } -static boolean in_game_widget(char const *name) -{ - return !strncmp(name, IN_GAME_WIDGET_PREFIX, strlen(IN_GAME_WIDGET_PREFIX)); -} - -/* one of ours by its name (not the map's, with a backslash) wanted */ -static void want_named(char const *name, long count, char const *(*name_of)(long), boolean *wanted) -{ - long index = name && !strchr(name, '\\') ? find(name, count, name_of) : NONE; - - if (index != NONE) - wanted[index] = TRUE; -} - -static void want_widget(long index); - -static void want_widget_named(char const *name) -{ - long index = name && !strchr(name, '\\') ? widget_named(name) : NONE; - - if (index != NONE) - want_widget(index); -} - -/* a widget wanted, and what it names: its bitmaps and strings, its children, -its description, the widgets its handlers open and its conditional ones */ -static void want_widget(long index) -{ - struct halo_menus const *menus = build.menus; - struct halo_menu_widget const *widget = &menus->widgets[index]; - long child, handler, conditional; - - if (build.wanted_widgets[index]) - return; - build.wanted_widgets[index] = TRUE; - want_named(widget->bitmap, menus->bitmap_count, bitmap_name, build.wanted_bitmaps); - want_named(widget->header_bitmap, menus->bitmap_count, bitmap_name, build.wanted_bitmaps); - want_named(widget->footer_bitmap, menus->bitmap_count, bitmap_name, build.wanted_bitmaps); - want_named(widget->string_list, menus->string_list_count, strings_name, build.wanted_strings); - want_widget_named(widget->description); - for (child = widget->first_child; child != HALO_MENU_NONE; child = menus->children[child].next) - { - if (menus->children[child].nested != HALO_MENU_NONE) - want_widget(menus->children[child].nested); - else - want_widget_named(menus->children[child].widget); - } - for (handler = widget->first_handler; handler != HALO_MENU_NONE; handler = menus->handlers[handler].next) - { - struct halo_menu_handler const *on = &menus->handlers[handler]; - - want_widget_named(on->open); - want_widget_named(on->replace); - want_widget_named(on->focus); - want_widget_named(on->widget); - want_widget_named(on->otherwise); - } - for (conditional = widget->first_conditional; conditional != HALO_MENU_NONE; - conditional = menus->conditionals[conditional].next) - { - want_widget_named(menus->conditionals[conditional].widget); - } -} - static long map_tag(long group_tag, char const *name, char const *file, long line) { long index = tag_loaded(group_tag, name); @@ -1166,7 +1098,7 @@ static void *widget_build(long widget_index) definition->flags = flags_parse(source->flags, widget_flag_names, NUMBEROF(widget_flag_names), source->file, source->line); /* (in a network game the clock runs on) */ - if (build.game_map) + if (build.game_map && !build.campaign) definition->flags &= ~FLAG(1); /* pause_game */ definition->milliseconds_to_auto_close = source->auto_close; definition->auto_close_fade_time = source->auto_close_fade; @@ -1777,71 +1709,188 @@ static void pause_patch(struct cache_file_tag_instance *instances) platform_log("menus: the pause menu has SETTINGS%s", host ? " and END GAME" : ""); } +/* ---------- the single-player campaign's pause menu + +Its list gets the same SETTINGS (pause_list_patch) after RESUME GAME, a copy +of REVERT TO SAVED; its box, beside the mission objectives', keeps its size, +the rows closer. Settings there has only the items that work in a game +(pause_settings_patch), and Gamepads' OK saves the profile itself. */ + +#define PAUSE_SETTINGS_LIST "main_menu/settings_select/player_setup/player_profile_edit/profile_edit_select_list" +#define PAUSE_SETTINGS_GAMEPADS_OK "main_menu/settings_select/player_setup/player_profile_edit/gamepad_setup/button_ok" + +/* Change Name's keyboard and Change Color's pictures are ui.map's, Network +Setup's settings are not for a game under way, and About is nothing to it */ +static char const *const pause_settings_hidden_items[] = +{ + "main_menu/settings_select/player_setup/player_profile_edit/name_profile_item", + "main_menu/settings_select/player_setup/player_profile_edit/network_settings_profile_item", + "main_menu/settings_select/player_setup/player_profile_edit/color_profile_item", + "main_menu/settings_select/player_setup/player_profile_edit/about_item", +}; + +/* the list's rows spaced evenly over the span its rows had */ +static void pause_list_fit(struct ui_widget_definition *list, short span) +{ + struct ui_widget_child_reference *children = list->child_widgets.address; + long count = list->child_widgets.count, child; + + for (child = 1; child < count; child++) + children[child].vertical_offset = (short)(children[0].vertical_offset + child * span / (count - 1)); +} + +static boolean pause_settings_item_hidden(long tag_index) +{ + short index; + + for (index = 0; index < (short)NUMBEROF(pause_settings_hidden_items); index++) + { + long widget = widget_named(pause_settings_hidden_items[index]); + + if (widget != NONE && build.widget_tags[widget] == tag_index) + return TRUE; + } + return FALSE; +} + +/* the hidden items stay in the list, empty and taking no events (which it +passes over), as an item's description is the one at its place in the list; +the others move up into the first places */ +static void pause_settings_patch(void) +{ + long list_widget = widget_named(PAUSE_SETTINGS_LIST); + long ok_widget = widget_named(PAUSE_SETTINGS_GAMEPADS_OK); + long settings_save = function_index("port settings save", "pause", 0); + struct ui_widget_definition *list, *ok; + struct ui_widget_child_reference *children; + struct ui_widget_event_handler_reference *handlers; + short places[16]; + long child, handler, place_count = 0, place = 0; + + if (list_widget == NONE || ok_widget == NONE) + return; + list = tag_get(UI_WIDGET_DEFINITION_TAG, build.widget_tags[list_widget]); + children = list->child_widgets.address; + for (child = 0; child < list->child_widgets.count && place_count < (long)NUMBEROF(places); child++) + { + struct ui_widget_definition *item = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index); + + if (item->type == _widget_type_text_box) + places[place_count++] = (short)(item->bounds.y0 + children[child].vertical_offset); + } + for (child = 0; child < list->child_widgets.count && place < place_count; child++) + { + struct ui_widget_definition *item = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index); + + if (item->type != _widget_type_text_box) + continue; + if (pause_settings_item_hidden(children[child].widget_tag.index)) + { + item->event_handlers.count = 0; + reference_clear(&item->background_bitmap, BITMAP_GROUP_TAG); + reference_clear(&item->text_label_string_list, UNICODE_STRING_LIST_TAG); + /* (nowhere for the mouse to focus it) */ + item->bounds.y1 = item->bounds.y0; + item->bounds.x1 = item->bounds.x0; + } + else + { + children[child].vertical_offset = (short)(places[place++] - item->bounds.y0); + } + } + ok = tag_get(UI_WIDGET_DEFINITION_TAG, build.widget_tags[ok_widget]); + handlers = ok->event_handlers.address; + for (handler = 0; handler < ok->event_handlers.count; handler++) + { + if (handlers[handler].function == settings_save) + handlers[handler].function = (short)function_index("port profile settings save", "pause", 0); + } +} + +static void pause_campaign_patch(struct cache_file_tag_instance *instances) +{ + long screen = tag_loaded(UI_WIDGET_DEFINITION_TAG, CAMPAIGN_PAUSE_SCREEN); + struct ui_widget_definition *definition; + struct ui_widget_child_reference *children; + long child; + + if (screen == NONE) + return; + definition = tag_get(UI_WIDGET_DEFINITION_TAG, screen); + children = definition->child_widgets.address; + for (child = 0; child < definition->child_widgets.count; child++) + { + struct ui_widget_definition *list; + struct ui_widget_child_reference const *rows; + short span; + + if (children[child].widget_tag.index == NONE) + continue; + list = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index); + if (list->type != _widget_type_column_list || list->child_widgets.count < 2) + continue; + rows = list->child_widgets.address; + span = (short)(rows[list->child_widgets.count - 1].vertical_offset - rows[0].vertical_offset); + if (pause_list_patch(instances, list, 1, FALSE)) + { + pause_list_fit(list, span); + pause_settings_patch(); + platform_log("menus: the pause menu has SETTINGS"); + } + return; + } +} + +/* the campaign played here by one player: not network co-op, nor split +screen */ +static boolean single_player_campaign_map(void) +{ + return tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE && + tag_loaded(UI_WIDGET_DEFINITION_TAG, CAMPAIGN_PAUSE_SCREEN) != NONE && + game_connection() == _game_connection_local && !network_game_is_active() && !game_is_cooperative(); +} + void menu_tags_loaded( char const *map_name) { struct halo_menus const *menus; struct cache_file_tag_instance *instances; long widget_count, own_lists = 0, total, index; - long widgets_built = 0, strings_built = 0, bitmaps_built = 0; - /* (ui.map; a multiplayer map: its pause menu's SETTINGS, pause_patch; - any other map is the campaign's: the in-game widgets) */ - boolean game_map = strcmp(map_name, "ui") != 0 && tag_loaded('Soul', MULTIPLAYER_COLLECTION) != NONE; - boolean in_game = strcmp(map_name, "ui") != 0 && !game_map; - if (strcmp(config_string("display.menus"), "pc")) + boolean game_map = strcmp(map_name, "ui") != 0; + boolean campaign = game_map && single_player_campaign_map(); + + /* (ui.map, and a multiplayer map: its pause menu's SETTINGS; a + single-player campaign's too) */ + if ((game_map && tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE && !campaign) || + strcmp(config_string("display.menus"), "pc")) + { return; + } menus = halo_menus_load(); if (!menus) return; memset(&build, 0, sizeof(build)); build.menus = menus; build.game_map = game_map; + build.campaign = campaign; widget_count = menus->widget_count; build.widget_tags = malloc((widget_count + 1) * sizeof(long)); build.text_tags = malloc((widget_count + 1) * sizeof(long)); build.spinner_tags = malloc((widget_count + 1) * sizeof(long)); build.bitmap_tags = malloc((menus->bitmap_count + 1) * sizeof(long)); build.strings_tags = malloc((menus->string_list_count + 1) * sizeof(long)); - build.wanted_widgets = malloc((widget_count + 1) * sizeof(boolean)); - build.wanted_bitmaps = malloc((menus->bitmap_count + 1) * sizeof(boolean)); - build.wanted_strings = malloc((menus->string_list_count + 1) * sizeof(boolean)); - if (!build.widget_tags || !build.text_tags || !build.spinner_tags || !build.bitmap_tags || - !build.strings_tags || !build.wanted_widgets || !build.wanted_bitmaps || !build.wanted_strings) + if (!build.widget_tags || !build.text_tags || !build.spinner_tags || !build.bitmap_tags || !build.strings_tags) goto failed; - /* ui.map and a multiplayer map: all but the in-game widgets; a campaign - map: those, and what they lead to */ - for (index = 0; index < widget_count; index++) - build.wanted_widgets[index] = !in_game && !in_game_widget(menus->widgets[index].name); - for (index = 0; index < menus->bitmap_count; index++) - build.wanted_bitmaps[index] = !in_game; - for (index = 0; index < menus->string_list_count; index++) - build.wanted_strings[index] = !in_game; - for (index = 0; in_game && index < widget_count; index++) - { - if (in_game_widget(menus->widgets[index].name)) - want_widget(index); - } for (index = 0; index < widget_count; index++) { - if (build.wanted_widgets[index]) - { - widgets_built++; - own_lists += (menus->widgets[index].text != NULL) + (menus->widgets[index].strings != NULL); - } + own_lists += (menus->widgets[index].text != NULL) + (menus->widgets[index].strings != NULL); /* (names are unique) */ if (widget_named(menus->widgets[index].name) != index) problem(menus->widgets[index].file, menus->widgets[index].line, "two widgets are named", menus->widgets[index].name); } - for (index = 0; index < menus->string_list_count; index++) - strings_built += build.wanted_strings[index] != FALSE; - for (index = 0; index < menus->bitmap_count; index++) - bitmaps_built += build.wanted_bitmaps[index] != FALSE; - /* (a campaign map with no in-game widgets: nothing to add) */ - if (in_game && !widgets_built && !build.failed) - goto done; - total = widgets_built + own_lists + strings_built + bitmaps_built + + total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count + (game_map ? PAUSE_PATCH_TAGS : 0); instances = build.failed ? NULL : instances_grow(total, &build.first_index, &build.first_salt); if (!instances) @@ -1849,22 +1898,18 @@ void menu_tags_loaded( /* each new tag's index */ for (index = 0; index < widget_count; index++) { - boolean wanted = build.wanted_widgets[index]; - - build.widget_tags[index] = wanted ? next_tag() : NONE; - build.text_tags[index] = wanted && menus->widgets[index].text ? next_tag() : NONE; - build.spinner_tags[index] = wanted && menus->widgets[index].strings ? next_tag() : NONE; + build.widget_tags[index] = next_tag(); + build.text_tags[index] = menus->widgets[index].text ? next_tag() : NONE; + build.spinner_tags[index] = menus->widgets[index].strings ? next_tag() : NONE; } for (index = 0; index < menus->string_list_count; index++) - build.strings_tags[index] = build.wanted_strings[index] ? next_tag() : NONE; + build.strings_tags[index] = next_tag(); for (index = 0; index < menus->bitmap_count; index++) - build.bitmap_tags[index] = build.wanted_bitmaps[index] ? next_tag() : NONE; + build.bitmap_tags[index] = next_tag(); /* the tags: the bitmaps and strings first, which the widgets name; the widgets' names in the table before any is built, which they find there */ for (index = 0; index < menus->bitmap_count && !build.failed; index++) { - if (!build.wanted_bitmaps[index]) - continue; instance_set(instances, BITMAP_GROUP_TAG, build.bitmap_tags[index], menus->bitmaps[index].name, "", bitmap_build(&menus->bitmaps[index], build.bitmap_tags[index])); } @@ -1872,8 +1917,6 @@ void menu_tags_loaded( { struct halo_menu_strings const *list = &menus->string_lists[index]; - if (!build.wanted_strings[index]) - continue; instance_set(instances, UNICODE_STRING_LIST_TAG, build.strings_tags[index], list->name, "", string_list_build(menus->strings + list->first, list->count)); } @@ -1881,8 +1924,6 @@ void menu_tags_loaded( { struct halo_menu_widget const *widget = &menus->widgets[index]; - if (!build.wanted_widgets[index]) - continue; if (widget->text) { instance_set(instances, UNICODE_STRING_LIST_TAG, build.text_tags[index], widget->name, " text", @@ -1909,24 +1950,19 @@ void menu_tags_loaded( } cache_files_set_tag_instances(instances, build.first_index + total); for (index = 0; index < widget_count && !build.failed; index++) - { - if (build.wanted_widgets[index]) - instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index); - } + instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index); if (build.failed) goto failed; if (game_map) { pause_patch(instances); + if (campaign) + pause_campaign_patch(instances); if (build.failed) goto failed; /* (those it made) */ cache_files_set_tag_instances(instances, build.first_index + build.next); } - else if (in_game) - { - /* (no main menu here) */ - } else if (widget_named(menus->root) != NONE) { snprintf(menu_tags.root, sizeof(menu_tags.root), "%s%s", PC_MENU_TAG_PREFIX, menus->root); @@ -1941,14 +1977,12 @@ void menu_tags_loaded( platform_log("menus: there is no widget named %s, the main menu", menus->root); } menu_tags.loaded = TRUE; - menu_tags.in_game = in_game; platform_log("menus: %ld widgets, %ld string lists and %ld bitmaps added to the map's %ld tags", - widgets_built, own_lists + strings_built, bitmaps_built, build.first_index); + widget_count, own_lists + menus->string_list_count, menus->bitmap_count, build.first_index); goto done; failed: - platform_log(in_game ? "menus: not added; no SETTINGS in the pause menu" : - "menus: not added; using the game's own menus"); + platform_log("menus: not added; using the game's own menus"); menu_tags_release(); done: @@ -1957,9 +1991,6 @@ done: free(build.spinner_tags); free(build.strings_tags); free(build.bitmap_tags); - free(build.wanted_widgets); - free(build.wanted_bitmaps); - free(build.wanted_strings); memset(&build, 0, sizeof(build)); } @@ -1977,7 +2008,7 @@ char const *pc_menus_root_name( settings' screens) */ char const *open = config_string("debug.menu_open"); - if (menu_tags.loaded && !menu_tags.in_game && *open && build.menus == NULL) + if (menu_tags.loaded && *open && build.menus == NULL) { static char name[300]; long index; @@ -2068,7 +2099,7 @@ char const *pc_menus_screen( }; short index; - if (!menu_tags.loaded || menu_tags.in_game) + if (!menu_tags.loaded) return name; for (index = 0; index < NUMBEROF(screens); index++) { diff --git a/source/interface/player_ui.c b/source/interface/player_ui.c index 9c92e7e5..a33529b1 100644 --- a/source/interface/player_ui.c +++ b/source/interface/player_ui.c @@ -367,6 +367,13 @@ struct player_profile *player_ui_get_edit_player_profile( return result; } +/* port */ +long player_ui_get_edit_profile_index( + void) +{ + return player_ui_globals.edit_profile_index; +} + struct game_variant *player_ui_get_edit_playlist_profile( void) { diff --git a/source/interface/player_ui.h b/source/interface/player_ui.h index 59a51f6c..83280455 100644 --- a/source/interface/player_ui.h +++ b/source/interface/player_ui.h @@ -43,6 +43,9 @@ long player_ui_get_active_player_profile_index( short local_player_index); struct player_profile *player_ui_get_edit_player_profile( void); +/* port: the saved game file being edited (NONE for none) */ +long player_ui_get_edit_profile_index( + void); struct game_variant *player_ui_get_edit_playlist_profile( void); /* port: the PC options (game_engine.h) of the gametype being edited, and of diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index bf91970d..2e8a10be 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -3881,135 +3881,6 @@ void ui_widget_port_go_back( widget_instance_go_back_to_previous(widget); } -/* ---------- SETTINGS in the pause menus (port) - -The campaign's full screen pause menu (with one player here) gets SETTINGS -after RESUME GAME: the menus' own row for it -(port/assets/menus/ce/in_game.xml), which menu_tags.c builds in the -campaign's maps when the menus are there (a multiplayer map's pause menu has -menu_tags.c's own: pause_patch). It opens its screen as any widget opens -another, so B comes back here, to SETTINGS. The rows are found by their tags' -names, not their places, and spaced to fit the box (ui_pause_menu_rows_fit): -a row added after SETTINGS is spaced with the others. */ - -#define UI_PAUSE_MENU_TAG "ui\\shell\\solo_game\\pause_game\\pause_game" -#define UI_PAUSE_MENU_SETTINGS_TAG "pc\\in_game\\settings_button" - -enum -{ - /* the body of the pause menus' box between the lines of its art - (ui\shell\bitmaps\pausebox2_*), and how far inside it the rows stay */ - UI_PAUSE_BOX_BODY_TOP = 2, - UI_PAUSE_BOX_BODY_BOTTOM = 130, - UI_PAUSE_BOX_ROWS_MARGIN = 4 -}; - -/* whether the widget is drawn from a tag whose name ends with the leaf */ -static boolean ui_pause_menu_tag_is( - struct widget_instance *widget, - char const *leaf) -{ - char const *name = tag_get_name(widget->definition_tag_index); - char const *last = name ? strrchr(name, '\\') : NULL; - - return last && !csstrcmp(last + 1, leaf); -} - -/* the rows spaced as the map spaces them, around the middle where it has -them, if they fit in the box's body; else the widest spacing that fits */ -static void ui_pause_menu_rows_fit( - struct widget_instance *list, - struct widget_instance *box) -{ - struct ui_widget_definition *definition = ui_widget_definition_get(list->definition_tag_index); - struct ui_widget_child_reference *references = - (struct ui_widget_child_reference *)definition->child_widgets.address; - struct ui_widget_definition *row_definition; - struct widget_instance *row; - short row_count = 0; - short row_height, pitch, middle_twice, body_top, body_bottom, span, top; - - for (row = list->child; row; row = row->next) - row_count++; - if (definition->child_widgets.count < 2 || row_count < 2) - return; - row_definition = ui_widget_definition_get(list->child->definition_tag_index); - row_height = row_definition->bounds.y1 - row_definition->bounds.y0; - pitch = references[1].vertical_offset - references[0].vertical_offset; - /* (on the screen, as the list's place and the box's are) */ - middle_twice = 2 * list->vertical_offset + references[0].vertical_offset + - references[definition->child_widgets.count - 1].vertical_offset + row_height; - body_top = box->vertical_offset + UI_PAUSE_BOX_BODY_TOP + UI_PAUSE_BOX_ROWS_MARGIN; - body_bottom = box->vertical_offset + UI_PAUSE_BOX_BODY_BOTTOM - UI_PAUSE_BOX_ROWS_MARGIN; - if ((row_count - 1) * pitch + row_height > body_bottom - body_top) - pitch = (body_bottom - body_top - row_height) / (row_count - 1); - span = (row_count - 1) * pitch + row_height; - top = PIN((middle_twice - span) / 2, body_top, body_bottom - span); - row_count = 0; - for (row = list->child; row; row = row->next) - row->vertical_offset = top - list->vertical_offset + row_count++ * pitch; - - return; -} - -static void ui_pause_menu_loaded( - struct widget_instance *root) -{ - char const *name = tag_get_name(root->definition_tag_index); - long settings_tag_index; - struct widget_instance *box = NULL; - struct widget_instance *list = NULL; - struct widget_instance *resume = NULL; - struct widget_instance *child; - struct widget_instance *settings; - - /* (and with one player here: the campaign's falls back on its full - screen pause menu for more, ui_check_for_pause_game) */ - if (!name || csstrcmp(name, UI_PAUSE_MENU_TAG) || local_player_count() > 1) - return; - settings_tag_index = tag_loaded(UI_WIDGET_DEFINITION_TAG, UI_PAUSE_MENU_SETTINGS_TAG); - if (settings_tag_index == NONE) - return; - for (child = root->child; child; child = child->next) - { - if (ui_pause_menu_tag_is(child, "pause_dialog_bkd")) - { - box = child; - } - else if (child->type == _ui_widget_type_column_list && !resume) - { - for (resume = child->child; resume; resume = resume->next) - { - if (ui_pause_menu_tag_is(resume, "resume_game_button")) - break; - } - if (resume) - list = child; - } - } - if (!box || !list) - return; - settings = ui_widget_load_by_name_or_tag( - NULL, - settings_tag_index, - list, - list->local_player_index, - NONE, - NONE, - NONE); - if (!settings) - return; - settings->horizontal_offset = resume->horizontal_offset; - settings->previous = resume; - settings->next = resume->next; - if (resume->next) - resume->next->previous = settings; - resume->next = settings; - ui_pause_menu_rows_fit(list, box); - - return; -} - struct widget_instance *ui_widget_load_by_name_or_tag( char const *name, long tag_index, @@ -4104,10 +3975,6 @@ struct widget_instance *ui_widget_load_by_name_or_tag( tag_index, local_player_index, widget_stack); - /* port: SETTINGS in the pause menus, unless a created handler - closed the widget */ - if (!parent && widget_globals.active_widgets[widget_stack] == widget) - ui_pause_menu_loaded(widget); } else { diff --git a/tools/ce_menus.py b/tools/ce_menus.py index 58dd1c3f..b3f6742b 100644 --- a/tools/ce_menus.py +++ b/tools/ce_menus.py @@ -600,8 +600,7 @@ def main() -> None: lines += widget_xml(tag, widgets[tag], tags, functions, inputs, fonts, {}) lines.append("") (CE / f"{folder.replace('/', '.')}.xml").write_text("\n".join(lines) + "\n") - for name, lines in [*port_settings.settings_files().items(), *port_settings.multiplayer_files().items(), - *port_settings.in_game_files().items()]: + for name, lines in [*port_settings.settings_files().items(), *port_settings.multiplayer_files().items()]: (CE / name).write_text("\n".join(lines)) # their strings lines = ['', diff --git a/tools/port_settings.py b/tools/port_settings.py index ab3ec56b..da022f57 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -1309,93 +1309,3 @@ def multiplayer_files() -> dict: REPLACED_FOLDERS = [f"{MT}/server_settings"] - - -# ---------- in the campaign: SETTINGS in the pause menu - -# The game's pause menus are the maps' own, not these files', and ui.map has -# none of their art, so these widgets (in_game/...) are built in the -# campaign's maps instead, with the screens they open -# (port/linux/game/menu_tags.c); ui_widget.c adds SETTINGS to the campaign's -# pause menu after RESUME GAME (a multiplayer map's has menu_tags.c's own). Its screen -# is the multiplayer pause menu's box and key, from the maps' own tags (every -# campaign and multiplayer map has them), with rows as the pause menus have -# them, named and ordered as Edit Profile has them. Each opens Edit Profile's -# screen over the pause menu's dim (behind them, the main menu has its dark -# backdrop, a game the game), and the screen saves config.toml's settings as -# it does there; GAMEPADS, on OK, only the controller settings of the -# player's active profile ("port active profile edit begin" and "end", -# port/linux/game/menu_functions.c). -# The desktop builds' only, for now. -IG = "in_game" -PAUSE_DIM = "ui\\shell\\bitmaps\\semi_transparent_grey" -PAUSE_ROW = [("type", "text"), ("width", 202), ("height", 27), ("bitmap", "ui\\shell\\bitmaps\\menu_bkds"), - ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("align", "center"), ("text_y", 3)] -DESKTOP = [("platform", "desktop")] -# Edit Profile's setups: their rows' names (its profile_edit_options), and -# the screens they open -IN_GAME_SETUPS = [("controls_setup", 1, f"{PE}/controls_setup/controls_settings_screen"), - ("gamepads", 2, f"{PE}/gamepad_setup/gamepad_setup_screen"), - ("mouse_setup", 3, f"{PE}/mouse_settings/mouse_settings_screen"), - ("audio_setup", 4, f"{PE}/audio_settings/audio_settings_screen"), - ("video_setup", 5, f"{PE}/video_settings/video_settings_screen")] -# the box where the multiplayer pause menu has it; the body of its art -# (pausebox2_*) between its rows 2 and 130, the key's band below; the rows -# that far inside it (ui_widget.c's UI_PAUSE_BOX_ROWS_MARGIN), spaced as the -# multiplayer pause menu's at most -BOX_Y, BOX_BODY_TOP, BOX_BODY_BOTTOM, ROWS_MARGIN, ROW_HEIGHT, ROW_SPACING = 164, 2, 130, 4, 27, 35 - - -def _pause_row(name: str, label: list, handlers: list) -> list: - return _widget(name, label + PAUSE_ROW + DESKTOP, handlers) - - -def _opens(screen: str, run: str = None) -> list: - action = (f'run="{run}" ' if run else "") + f'open="{screen}"' - return [f'', f''] - - -def _rows_place(count: int) -> tuple: - """the rows' top and spacing: in the middle of the box's body""" - body = BOX_BODY_BOTTOM - BOX_BODY_TOP - spacing = min(ROW_SPACING, (body - 2 * ROWS_MARGIN - ROW_HEIGHT) // (count - 1)) - span = (count - 1) * spacing + ROW_HEIGHT - return BOX_Y + BOX_BODY_TOP + (body - span) // 2, spacing - - -def in_game_files() -> dict: - """SETTINGS in the pause menus, its screen, and the setups' screens""" - top, spacing = _rows_place(len(IN_GAME_SETUPS)) - lines = _pause_row(f"{IG}/settings_button", [("text", "SETTINGS")], _opens(f"{IG}/settings_screen")) - lines += _widget(f"{IG}/settings_screen", - [("width", 640), ("height", 480), ("flags", "pass_unhandled_to_focused_child pause_game"), - ("bitmap", PAUSE_DIM)] + DESKTOP, - ['', '', - f'', - f'', - '']) - lines += _widget(f"{IG}/settings_box", [("width", 226), ("height", 154)] + DESKTOP, - ['', - '', - '']) - lines += _widget(f"{IG}/settings_list", - [("type", "column_list"), ("width", 202), - ("height", (len(IN_GAME_SETUPS) - 1) * spacing + ROW_HEIGHT), - ("flags", "pass_unhandled_to_focused_child up_down_tabs_items")] + DESKTOP, - [f'' - for index, (key, _, _) in enumerate(IN_GAME_SETUPS)]) - for key, string_index, screen in IN_GAME_SETUPS: - # (GAMEPADS sets the profile's controller settings: the player's - # active profile, being edited while its screen is up) - gamepads = key == "gamepads" - lines += _pause_row(f"{IG}/{key}_button", - [("string_list", f"{PE}/profile_edit_options"), ("string_index", string_index)], - _opens(f"{IG}/{key}_screen", "port active profile edit begin" if gamepads else None)) - lines += _widget(f"{IG}/{key}_screen", - [("width", 640), ("height", 480), ("flags", "pass_unhandled_to_focused_child"), - ("bitmap", PAUSE_DIM)] + DESKTOP, - ([''] if gamepads else []) + - [f'']) - return {"in_game.xml": ['', - "", - "", *lines, "", ""]} From d084721fcf697a7685f8faf8bba37ecee238218c Mon Sep 17 00:00:00 2001 From: justinfilip Date: Fri, 9 Oct 2026 10:59:12 -0500 Subject: [PATCH 118/187] Keep an item's resting surface past 32767. The number stays in the same 16-bit field, and a match can end on a map whose collision surfaces run past that. --- source/items/items.c | 68 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/source/items/items.c b/source/items/items.c index 3e2c7c0c..c32d4efb 100644 --- a/source/items/items.c +++ b/source/items/items.c @@ -102,11 +102,52 @@ enum /* ---------- prototypes */ +/* the surface an item rests on is a signed short in the datum. a collision +surface past 32767 is negative there (39748 is kept as its low 16 bits). +read back unsigned, it is that surface. none stays none, and 65535 is +none's pattern, so a surface that high is not recorded */ +static long item_rested_surface_index( + struct item_datum const *item); +static struct collision_surface const *item_rested_surface( + struct item_datum const *item); +static void item_set_rested_surface( + struct item_datum *item, + long surface_index); + /* ---------- globals */ static struct profile_section item_update_section = { "item_update", NONE, TRUE }; static real const item_maximum_impact_velocity = 0.1f; +static long item_rested_surface_index( + struct item_datum const *item) +{ + if (item->item.rested_surface_index == NONE) + return NONE; + return (unsigned short)item->item.rested_surface_index; +} + +static struct collision_surface const *item_rested_surface( + struct item_datum const *item) +{ + struct collision_bsp const *bsp = global_collision_bsp_get(); + long surface_index = item_rested_surface_index(item); + + if (!bsp || surface_index < 0 || surface_index >= bsp->surfaces.count) + return NULL; + return TAG_BLOCK_GET_ELEMENT(&bsp->surfaces, surface_index, struct collision_surface); +} + +static void item_set_rested_surface( + struct item_datum *item, + long surface_index) +{ + if (surface_index < 0 || surface_index >= 65535) + item->item.rested_surface_index = NONE; + else + item->item.rested_surface_index = (short)surface_index; +} + /* ---------- public code */ void items_initialize( @@ -356,10 +397,9 @@ void item_accelerate( real distance_above; collision_bsp = global_collision_bsp_get(); - surface = TAG_BLOCK_GET_ELEMENT( - &collision_bsp->surfaces, - item->item.rested_surface_index, - struct collision_surface); + surface = item_rested_surface(item); + if (surface) + { bsp3d_get_plane_from_designator( &collision_bsp->bsp3d, surface->plane_designator, @@ -373,6 +413,7 @@ void item_accelerate( distance_above, &new_position); object_translate(item_index, &new_position, NULL); + } } item->object.flags &= ~FLAG(_object_at_rest_bit); @@ -679,8 +720,7 @@ boolean item_update( { case _collision_result_structure: SET_FLAG(item->item.flags, _item_on_structure_bit, TRUE); - item->item.rested_surface_index = - (short)collision.surface_index; + item_set_rested_surface(item, collision.surface_index); item->item.bsp_index = global_structure_bsp_index_get(); break; @@ -766,18 +806,18 @@ boolean item_update( 1); if (TEST_FLAG(item->item.flags, _item_on_structure_bit) && - item->item.rested_surface_index != NONE && + item_rested_surface_index(item) != NONE && item->item.bsp_index == global_structure_bsp_index_get()) { - struct collision_bsp *collision_bsp; struct collision_surface const *surface; - collision_bsp = global_collision_bsp_get(); - surface = TAG_BLOCK_GET_ELEMENT( - &collision_bsp->surfaces, - item->item.rested_surface_index, - struct collision_surface); - if (TEST_FLAG(surface->flags, _collision_surface_breakable_bit) && + surface = item_rested_surface(item); + if (!surface) + { + SET_FLAG(item->item.flags, _item_on_structure_bit, FALSE); + item->item.rested_surface_index = NONE; + } + else if (TEST_FLAG(surface->flags, _collision_surface_breakable_bit) && !breakable_surface_extant( surface->breakable_surface_index)) { From b980207235184bb1f40569c836a77ef93e33e112 Mon Sep 17 00:00:00 2001 From: justinfilip Date: Fri, 9 Oct 2026 11:38:27 -0500 Subject: [PATCH 119/187] Let a frame draw 32768 structure triangles. Retail stops the list at 16384. The count is a long so it can hold 32768, and the dynamic triangle buffer keeps room for the rest of the frame. --- source/rasterizer/rasterizer.h | 5 ++++- source/render/render.h | 7 +++++-- source/structures/structure_render.c | 24 ++++++++++++------------ source/structures/structure_visibility.c | 15 +++++++++++++-- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/source/rasterizer/rasterizer.h b/source/rasterizer/rasterizer.h index 5e34ba24..2d006764 100644 --- a/source/rasterizer/rasterizer.h +++ b/source/rasterizer/rasterizer.h @@ -33,7 +33,10 @@ enum RASTERIZER_MAXIMUM_DEBUG_VERTICES = 393216, RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS = 384, RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS2 = 32, - RASTERIZER_MAXIMUM_DYNAMIC_TRIANGLES = 32768, + /* the structure list can ask for 32768 triangles. a request that would + fill this buffer is refused, so the room above 32768 is what a frame + still has for lights, shadows, and contrails */ + RASTERIZER_MAXIMUM_DYNAMIC_TRIANGLES = 49152, RASTERIZER_MAXIMUM_DYNAMIC_TRIANGLE_BUFFERS = 1024, /* four per particle (build_sprites_begin), for the native builds' larger particle pool (halo_port_capacity.h) */ diff --git a/source/render/render.h b/source/render/render.h index 592f1a71..c3c2b3f5 100644 --- a/source/render/render.h +++ b/source/render/render.h @@ -20,7 +20,9 @@ enum { MAXIMUM_RENDERED_DISTANT_LIGHTS = 2, MAXIMUM_RENDERED_POINT_LIGHTS = 2, - MAXIMUM_RENDERED_ENVIRONMENT_SURFACES = 16384, + /* retail stops a frame at 16384 structure triangles. 32768 is the + HAC2 and MCC count */ + MAXIMUM_RENDERED_ENVIRONMENT_SURFACES = 32768, MAXIMUM_RENDERED_CLUSTERS = 128, MAXIMUM_SURFACES_PER_STRUCTURE = 0x20000, MAXIMUM_RENDERED_LIGHTS = 128, @@ -94,7 +96,8 @@ struct render_globals struct rendered_cluster rendered_clusters[MAXIMUM_RENDERED_CLUSTERS]; short rendered_cluster_count; unsigned long environment_surface_flags[MAXIMUM_SURFACES_PER_STRUCTURE]; - short environment_surface_count; + /* a short wraps at 32768, so the raised triangle count lives in a long */ + long environment_surface_count; long environment_surface_indices[MAXIMUM_RENDERED_ENVIRONMENT_SURFACES]; }; diff --git a/source/structures/structure_render.c b/source/structures/structure_render.c index 0db163b8..8e9605b3 100644 --- a/source/structures/structure_render.c +++ b/source/structures/structure_render.c @@ -152,21 +152,21 @@ static boolean compare_surface_indices( long surface_index1, long surface_index2); static void structure_render_dynamic_triangles_from_bitvector( - short surface_count, + long surface_count, long *surface_indices, unsigned long *surface_flags, short *triangles); static void structure_render_dynamic_triangles_from_indices( - short surface_count, + long surface_count, long *surface_indices, short *triangles); static long structure_render_build_dynamic_triangles( long *surface_indices, - short surface_count, + long surface_count, unsigned long *surface_flags); static void structure_render_pass( long *surface_indices, - short surface_count, + long surface_count, long dynamic_triangles_index, void (*begin_lightmap)(struct bitmap_data const *lightmap_bitmap), void (*draw_triangles)( @@ -264,13 +264,13 @@ static boolean compare_surface_indices( } static void structure_render_dynamic_triangles_from_bitvector( - short surface_count, + long surface_count, long *surface_indices, unsigned long *surface_flags, short *triangles) { struct structure_bsp *structure_bsp = global_structure_bsp_get(); - short surface_index_index = 0; + long surface_index_index = 0; long surface_index = 0; if (structure_bsp->surfaces.count > 0) @@ -327,12 +327,12 @@ static void structure_render_dynamic_triangles_from_bitvector( } static void structure_render_dynamic_triangles_from_indices( - short surface_count, + long surface_count, long *surface_indices, short *triangles) { struct structure_bsp *structure_bsp = global_structure_bsp_get(); - short surface_index_index; + long surface_index_index; qsort_4byte(surface_indices, surface_count, compare_surface_indices); @@ -358,7 +358,7 @@ static void structure_render_dynamic_triangles_from_indices( static long structure_render_build_dynamic_triangles( long *surface_indices, - short surface_count, + long surface_count, unsigned long *surface_flags) { struct structure_bsp *structure_bsp = global_structure_bsp_get(); @@ -407,7 +407,7 @@ static long structure_render_build_dynamic_triangles( static void structure_render_pass( long *surface_indices, - short surface_count, + long surface_count, long dynamic_triangles_index, void (*begin_lightmap)(struct bitmap_data const *lightmap_bitmap), void (*draw_triangles)( @@ -489,7 +489,7 @@ static void structure_render_pass( struct structure_material *material; struct shader *shader; long *chunk_start; - short chunk_count; + long chunk_count; if (surface_index_cursor >= surface_index_end) { @@ -516,7 +516,7 @@ static void structure_render_pass( while (surface_index_cursor < surface_index_end && *surface_index_cursor < material->first_surface_index + material->surface_count); - chunk_count = (short)(surface_index_cursor - chunk_start); + chunk_count = surface_index_cursor - chunk_start; if (breakable_surface_extant(material->breakable_surface_index)) { diff --git a/source/structures/structure_visibility.c b/source/structures/structure_visibility.c index 2666e998..4fa14099 100644 --- a/source/structures/structure_visibility.c +++ b/source/structures/structure_visibility.c @@ -339,6 +339,7 @@ static boolean warned_about_portal_hull_vertices; static boolean warned_about_invalid_structure_bsp; static boolean warned_about_rendered_clusters; static boolean warned_about_structure_bsp_depth; +static boolean warned_about_environment_surfaces; static struct structure_visibility_globals structure_visibility_globals; static struct profile_section render_structure_visibility_portal_traversal = { "render_structure_visibility_portal_traversal", NONE, TRUE }; @@ -469,8 +470,10 @@ static void structure_visibility_traverse_subclusters( } surface_index_buffer = TAG_BLOCK_GET_ELEMENT(&subcluster->surface_indices, 0, long); + /* a short index stops, and then never finishes, once a subcluster + passes 32767 surfaces */ for (surface_list_index = 0; - (short)surface_list_index < subcluster->surface_indices.count; + surface_list_index < subcluster->surface_indices.count; surface_index_buffer++, surface_list_index++) { /* port: a surface index (from the map) that is no surface is @@ -1245,7 +1248,7 @@ static short structure_visibility_build_surfaces_traverse_clusters( long surface_list_index; for (surface_list_index = 0; - (short)surface_list_index < subcluster->surface_indices.count; + surface_list_index < subcluster->surface_indices.count; surface_index_buffer++, surface_list_index++) { /* port: (as in structure_visibility_traverse_subclusters) */ @@ -1999,5 +2002,13 @@ void structure_visibility_compute( structure_visibility_traverse_surface_lists(structure); } + if (render.environment_surface_count >= MAXIMUM_RENDERED_ENVIRONMENT_SURFACES && + !warned_about_environment_surfaces) + { + error(_error_silent, "a frame stopped at %d structure triangles", + MAXIMUM_RENDERED_ENVIRONMENT_SURFACES); + warned_about_environment_surfaces = TRUE; + } + return; } From acae172b57973739ce6db75aa289d69e5bd4832f Mon Sep 17 00:00:00 2001 From: justinfilip Date: Fri, 9 Oct 2026 12:43:22 -0500 Subject: [PATCH 120/187] Keep the weapon at its original view when the world view is wide. SAME was drawing the arms and gun with that wide view. Video Setup also packs each platform's own rows, so a hidden setting does not leave a gap. --- ...ile_edit.video_settings.fov_viewmodels.xml | 2 +- ...tup.player_profile_edit.video_settings.xml | 23 +++++++++++-------- port/linux/README.md | 14 +++++------ port/linux/game/render_fov.c | 11 +++++++++ port/linux/game/render_fov.h | 2 ++ port/linux/game/viewmodel_fov.c | 5 ++++ port/linux/src/port_config.c | 4 ++-- tools/port_settings.py | 22 +++++++++++++----- 8 files changed, 58 insertions(+), 25 deletions(-) diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml index 19ea9316..26986d63 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml @@ -68,7 +68,7 @@ - + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml index 7c845d4e..7d4d8d36 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml @@ -14,15 +14,19 @@ - - - - - - - - - + + + + + + + + + + + + + @@ -154,3 +158,4 @@ + diff --git a/port/linux/README.md b/port/linux/README.md index f1bca603..8a448957 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -315,11 +315,11 @@ Each setting has an environment variable. The environment variable changes the setting for one start of the game. It has priority over the file. Video Setup's **FOV AND VIEWMODELS** category offers optional world and -weapon field-of-view settings and first-person visibility. **DEFAULT** keeps -the authored world projection; **SAME** makes the weapon follow it, and -viewmodels start **ON**. OK saves and applies changes; Cancel discards them. -The menu offers 80–150 degrees in steps of five. The config file accepts -20–150 degrees, including fractional values, or zero for Default/Same. +weapon field of view settings and first person visibility. **DEFAULT** keeps +the authored world projection. **SAME** keeps the original weapon view, and +viewmodels start **ON**. OK saves and applies changes. Cancel discards them. +The menu offers 80 to 150 degrees in steps of five. The config file accepts +20 to 150 degrees, including fractional values, or zero for Default and Same. Angles are horizontal at 16:9; other aspect ratios retain the same vertical view. World FOV applies to on-foot first-person views, preserving authored vehicle, death and cinematic cameras. Completed scopes keep their native @@ -342,8 +342,8 @@ only their display; firing, animation, sound and world lights continue. | `debug.gpu_flush_draws` | `-1` | `HALO_GPU_FLUSH_DRAWS` | Flush the GPU's pipeline every this many draws. `-1`: every 3 on Intel graphics with Mesa's driver, which can otherwise hang in the game's long runs of small draws and reset the desktop's graphics too. `0`: never. | | `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". | | `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". | -| `display.fov` | `0.0` | `HALO_FOV` | On-foot first-person horizontal FOV at 16:9, 20–150 degrees; `0` keeps the authored view. Video Setup > FOV and Viewmodels sets it. | -| `display.viewmodel_fov` | `0.0` | `HALO_VIEWMODEL_FOV` | Independent weapon/hands horizontal FOV at 16:9, 20–150 degrees; `0` follows the world view. | +| `display.fov` | `0.0` | `HALO_FOV` | On-foot first-person horizontal FOV at 16:9, from 20 to 150 degrees. `0` keeps the authored view. Video Setup's FOV and Viewmodels screen sets it. | +| `display.viewmodel_fov` | `0.0` | `HALO_VIEWMODEL_FOV` | Independent weapon and hands horizontal FOV at 16:9, 20 to 150 degrees. `0` keeps the original weapon view. | | `display.viewmodel_visible` | `true` | `HALO_VIEWMODEL_VIS` | Show first-person weapons, hands and attached visuals. This does not change gameplay or other players' models. | | `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. | | `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the resolution the game draws at, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. | diff --git a/port/linux/game/render_fov.c b/port/linux/game/render_fov.c index 64abccd4..cfb65b6e 100644 --- a/port/linux/game/render_fov.c +++ b/port/linux/game/render_fov.c @@ -18,6 +18,7 @@ double config_real(const char *name); unsigned long config_changes(void); static real reticle_scales[MAXIMUM_LOCAL_PLAYERS]; +static real authored_vertical[MAXIMUM_LOCAL_PLAYERS]; static float render_fov_adjust(short local_player_index, float native_vertical_field_of_view) { @@ -113,10 +114,20 @@ float render_fov_vertical(short local_player_index, float native_vertical_field_ /* Record the projection used for this view, including its native zoom * transition. A temporary viewmodel projection must not affect the HUD. */ reticle_scales[local_player_index] = scale; + /* A widened world view. The weapon can return to this frame's own angle. */ + authored_vertical[local_player_index] = + adjusted != native_vertical_field_of_view ? native_vertical_field_of_view : 0.0f; } return adjusted; } +float render_fov_authored_vertical(short local_player_index) +{ + if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS) + return 0.0f; + return authored_vertical[local_player_index]; +} + float render_fov_reticle_scale(short local_player_index) { if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || diff --git a/port/linux/game/render_fov.h b/port/linux/game/render_fov.h index 145b4873..024a98b8 100644 --- a/port/linux/game/render_fov.h +++ b/port/linux/game/render_fov.h @@ -6,5 +6,7 @@ camera's projection and visibility frustum are built. */ float render_fov_vertical(short local_player_index, float native_vertical_field_of_view); /* HUD pixel ratio relative to the native projection of the last local view. */ float render_fov_reticle_scale(short local_player_index); +/* The view's own vertical angle when the world FOV replaced it, else 0. */ +float render_fov_authored_vertical(short local_player_index); #endif diff --git a/port/linux/game/viewmodel_fov.c b/port/linux/game/viewmodel_fov.c index d4d0a688..d815bf01 100644 --- a/port/linux/game/viewmodel_fov.c +++ b/port/linux/game/viewmodel_fov.c @@ -1,5 +1,6 @@ #include "cseries.h" #include "viewmodel_fov.h" +#include "render_fov.h" #include "camera/director.h" #include "cutscene/cinematics.h" #include "game/players.h" @@ -42,6 +43,10 @@ void viewmodel_projection_begin(void) if (projection_depth++ != 0) return; projection_applied = FALSE; angle = viewmodel_vertical(); + /* SAME keeps the weapon at this frame's own angle. A wide world view + stretches arms and a gun that sit against the camera. */ + if (!angle) + angle = render_fov_authored_vertical(render.local_player_index); if (!angle || render.local_player_index < 0 || render.local_player_index >= MAXIMUM_LOCAL_PLAYERS || global_window_parameters.rasterizer_target != 0 /* primary view */ || cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) || diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 0283c22d..c6d80449 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -116,8 +116,8 @@ static const struct config_setting config_settings[] = "On-foot first-person horizontal FOV at 16:9, in degrees (20-150);\n" "0 keeps the authored view. Other cameras keep their own FOV." }, { "display.viewmodel_fov", _config_real, "0.0", "HALO_VIEWMODEL_FOV", _environment_value, _platform_all, - "Weapon/hands horizontal FOV at 16:9, in degrees (20-150);\n" - "0 follows the world view. Attached visuals use the same projection." }, + "Weapon and hands horizontal FOV at 16:9, in degrees (20 to 150);\n" + "0 keeps the original weapon view. Attached visuals use the same projection." }, { "display.viewmodel_visible", _config_boolean, "true", "HALO_VIEWMODEL_VIS", _environment_value, _platform_all, "Draw the first-person weapon, hands and attached visuals. Turning\n" "this off does not change firing, animation, sound or world lights." }, diff --git a/tools/port_settings.py b/tools/port_settings.py index e3243556..53bddec6 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -30,10 +30,11 @@ SCREENS = { "video_settings": { "screen": "video_settings_screen", "header": ("header_profile_video_settings", f"{PE}/video_settings/header_profile_video_settings"), - # (closer than the other screens' rows, and the help lower, for all - # twelve places to fit above it) - "spacing": 24, + # the other screens' 30px step. each platform packs its own rows, so + # a row hidden on this machine does not leave a gap + "spacing": 30, "help_top": 364, + "platform_places": True, # (rows in the place of the row before them: Window Size in # Resolution's, port/linux/game/menu_functions.c showing the one the # display mode chosen uses; Android's anti-aliasing in the desktop's) @@ -198,7 +199,7 @@ SCREENS["video_settings/fov_viewmodels"] = { ("FOV:", "display.fov", [("DEFAULT", "0")] + [(str(n), str(n)) for n in range(80, 151, 5)], "On-foot horizontal FOV at 16:9, in degrees.\nDefault keeps the authored view and scopes.", None), ("VIEWMODEL FOV:", "display.viewmodel_fov", [("SAME", "0")] + [(str(n), str(n)) for n in range(80, 151, 5)], - "Weapon/hands horizontal FOV at 16:9, in degrees.\nSame follows the world view.", None), + "Weapon and hands horizontal FOV at 16:9, in degrees.\nSame keeps the original weapon view.", None), ("VIEWMODELS:", "display.viewmodel_visible", ON_OFF, "Draw first-person weapons, hands and attached\nvisuals. Gameplay and sound continue when off.", None), ], @@ -325,9 +326,10 @@ def _setting_screen(folder: str, spec: dict) -> list: for index, (label, setting, choices, _, platform, *named) in enumerate(spec["rows"]): key = named[0] if named else setting.split(".", 1)[1] row = f"{base}/op_{key}" + shares = setting in spec.get("same_place", ()) or key in spec.get("same_place", ()) if spec.get("platform_places"): for name in places: - if platform in (None, name): + if platform in (None, name) and not shares: places[name] += 1 if platform or places["desktop"] == places["android"]: rows.append((row, platform, places[platform or "desktop"])) @@ -362,7 +364,15 @@ def _setting_screen(folder: str, spec: dict) -> list: key = category_folder.rsplit("/", 1)[-1] row = f"{base}/op_{key}" target = f"{PE}/{category_folder}/{SCREENS[category_folder]['screen']}" - rows.append((row, None, place + 1 + index)) + if spec.get("platform_places"): + for name in places: + places[name] += 1 + if places["desktop"] == places["android"]: + rows.append((row, None, places["desktop"])) + else: + rows += [(row, name, places[name]) for name in ("desktop", "android")] + else: + rows.append((row, None, place + 1 + index)) extra += _widget(row, [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF")], [f'', f'', From 141e5984f5cff319300188864beb726e8cc2231c Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:13:17 +1300 Subject: [PATCH 121/187] Touch aiming with the stick's aim assist and invert The touch controls' swipe was added after the aim assist, through the mouse's hook: no friction over a target and no adhesion to a moving one, which a controller has. It is now a turn made the stick's rate before the magnetism (halo_linux_touch_look, one port: hook in player_control.c), so the zoom, the stun and the magnetism act on it as on the stick's, and it counts as aiming for them; it follows the profile's invert look as the stick does (input_abstraction_port_look_inverted). input.touch_aim_assist (default true) turns the aim assist off. The mouse's hook is as before the touch controls. --- port/android/README.md | 8 ++++++-- port/linux/src/port_config.c | 4 ++++ port/linux/src/touch_input.c | 13 ++++++++++++ port/linux/src/touch_input.h | 3 +++ port/linux/src/xinput_sdl.c | 29 ++++++++++++++++++-------- source/game/player_control.c | 35 +++++++++++++++++++++++++++++--- source/input/input_abstraction.c | 16 +++++++++++++++ source/input/input_abstraction.h | 3 +++ 8 files changed, 98 insertions(+), 13 deletions(-) diff --git a/port/android/README.md b/port/android/README.md index ec113df0..e79824b7 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -162,8 +162,11 @@ controller for player 1: A finger that holds a button can also swipe to look, so you can fire and aim with one thumb. A short tap reaches the game even when it is shorter than one frame. Swipes that start in the edge-gesture zones of Android do -not turn the view. The view does not use the mouse settings: the swipe -keeps the aim assist of the controller. +not turn the view. The swipe aims as the controller's stick does, not as +a mouse: the aim slows over a target and follows a moving one, as with a +controller, and it follows the profile's "invert look" (the setting +`input.touch_aim_assist` turns the aim assist off). "Look sensitivity" sets +how far a swipe turns; the mouse settings do not apply. The touch controls show only in a game. In the menus and during cinematics they hide, and the touchscreen operates the menus as described @@ -206,6 +209,7 @@ These settings are only for Android: | Setting | Function | | --- | --- | +| `input.touch_aim_assist` | `true` (the default): the touch controls' swipe aiming gets the aim assist of a controller (the aim slows over a target and follows a moving one). `false`: none, as with a mouse; the bullets' own autoaim stays. | | `input.touch_controls` | The touch controls in a game. `"auto"` (the default): shown on a touchscreen while no controller is connected. `"on"`: also shown with a controller. `"off"`: never shown. A device without a touchscreen never shows them. The menus take taps with each value. | | `display.screen_width` | The number of columns of the 480-line picture. `0` (the default): the shape of the display (1068 on a 20:9 phone). `640`: the 4:3 shape of the Xbox. | | `debug.sample_seconds` | Refer to "Find problems". | diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 0ed73782..3f1c22c3 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -195,6 +195,10 @@ static const struct config_setting config_settings[] = "touchscreen while no controller is connected, \"on\" also with a\n" "controller, \"off\" never. A device without a touchscreen never shows\n" "them. The menus take taps in any case." }, + { "input.touch_aim_assist", _config_boolean, "true", "HALO_TOUCH_AIM_ASSIST", _environment_value, _platform_android, + "The touch controls' swipe aiming gets a controller's aim assist: the\n" + "aim slows over a target and follows a moving one. false: none, as a\n" + "mouse (the bullets' own autoaim stays)." }, { "input.mouse_sensitivity", _config_real, "1.0", "HALO_MOUSE_SENSITIVITY", _environment_value, _platform_desktop, "How far the view turns for the mouse's movement." }, { "input.invert_mouse", _config_boolean, "false", "HALO_MOUSE_INVERT", _environment_set_is_true, _platform_desktop, diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index f87a8849..85633698 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -286,6 +286,19 @@ void touch_input_look(float scale, float *yaw, float *pitch) *pitch -= delta[1] * scale; } +int touch_input_aim_assist(void) +{ + static int assisted; + static unsigned long read_at = (unsigned long)-1; + + if (read_at != config_changes()) + { + read_at = config_changes(); + assisted = config_boolean("input.touch_aim_assist") != 0; + } + return assisted; +} + void touch_input_rumble(unsigned int left, unsigned int right) { host_touch_rumble(left, right); diff --git a/port/linux/src/touch_input.h b/port/linux/src/touch_input.h index e4df652a..68356139 100644 --- a/port/linux/src/touch_input.h +++ b/port/linux/src/touch_input.h @@ -45,6 +45,9 @@ void touch_input_controls(XINPUT_GAMEPAD *pad, int menus); pitch, in radians at `scale` radians per pixel */ void touch_input_look(float scale, float *yaw, float *pitch); +/* whether the swipe gets the stick's aim assist (input.touch_aim_assist) */ +int touch_input_aim_assist(void); + /* port 0's motors, for the phone's vibration while the touch controls show */ void touch_input_rumble(unsigned int left, unsigned int right); #endif diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index ea553c8d..6f689711 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -147,18 +147,31 @@ int halo_linux_mouse_look(short gamepad_index, float *yaw, float *pitch) mouse_pending_y = 0.0f; mouse_polls_unconsumed = 0; pthread_mutex_unlock(&mouse_lock); -#ifdef HALO_ANDROID - /* the touch controls' swipe; it is not the mouse's aiming - (halo_linux_mouse_aiming), so a thumb keeps the stick's magnetism */ - touch_input_look(scale, yaw, pitch); -#endif if (x == 0.0f && y == 0.0f) - return *yaw != 0.0f || *pitch != 0.0f; - *yaw += -x * scale * mouse_sensitivity(); - *pitch += (invert ? y : -y) * scale * vertical_sensitivity; + return FALSE; + *yaw = -x * scale * mouse_sensitivity(); + *pitch = (invert ? y : -y) * scale * vertical_sensitivity; return TRUE; } +/* the touch controls' swipe since the last call, in radians at the mouse's +rate per pixel (the view applies its own sensitivity), and whether it gets +the stick's aim assist (input.touch_aim_assist): player_control.c makes it +the stick's rate before the magnetism. Nothing but on Android. */ +int halo_linux_touch_look(short gamepad_index, float *yaw, float *pitch, int *assisted) +{ + *yaw = 0.0f; + *pitch = 0.0f; + *assisted = FALSE; + if (gamepad_index != 0) + return FALSE; +#ifdef HALO_ANDROID + touch_input_look(0.0022f, yaw, pitch); + *assisted = touch_input_aim_assist(); +#endif + return *yaw != 0.0f || *pitch != 0.0f; +} + /* whether the player on the gamepad aims with the mouse (it moved after the right stick last did) and input.mouse_aim_assist is off: then the view's magnetism leaves them be (player_control.c); the bullets' autoaim stays */ diff --git a/source/game/player_control.c b/source/game/player_control.c index ffb2d925..de13ef5c 100644 --- a/source/game/player_control.c +++ b/source/game/player_control.c @@ -1143,6 +1143,10 @@ static void get_local_player_input_blob( real yaw_spin_scale; real pitch_spin_scale; real_euler_angles2d look_delta; + /* port: the touch controls' swipe this frame, and whether it + gets the stick's aim assist */ + boolean touching = FALSE; + int touch_assisted = FALSE; if (input_state->buttons[_button_scope_zoom] && controls_enable_doubled_spin) @@ -1176,6 +1180,29 @@ static void get_local_player_input_blob( constants->look_function.count, constants->look_function.address, clamped_pitch) * pitch_spin_scale * look_pitch_rate; + { + /* port: the touch controls' swipe (port/linux/src/xinput_sdl.c), + a turn this frame, made a rate as the stick's look is: the + zoom, the stun and the magnetism below act on it as on the + stick's, so that it slows over a target and follows a moving + one (input.touch_aim_assist); inverted as the stick is */ + extern int halo_linux_touch_look(short gamepad_index, real *yaw, real *pitch, int *assisted); + real touch_yaw; + real touch_pitch; + real touch_scale = time_delta_sec * TICKS_PER_SECOND; + + if (halo_linux_touch_look(gamepad_index, &touch_yaw, &touch_pitch, &touch_assisted) && + touch_scale > _real_epsilon) + { + touching = TRUE; + if (input_abstraction_port_look_inverted(gamepad_index)) + { + touch_pitch = -touch_pitch; + } + look_delta.yaw += touch_yaw / touch_scale; + look_delta.pitch += touch_pitch / touch_scale; + } + } if (player->unit_index != NONE && control->zoom_level != NONE) { @@ -1233,16 +1260,18 @@ static void get_local_player_input_blob( &target_angular_position, &target_angular_velocity); { - /* no magnetism for the mouse (port/linux/src/xinput_sdl.c) */ + /* no magnetism for the mouse (port/linux/src/xinput_sdl.c), + nor for the touch controls' swipe without its aim assist */ extern int halo_linux_mouse_aiming(short gamepad_index); - if (halo_linux_mouse_aiming(gamepad_index)) + if (halo_linux_mouse_aiming(gamepad_index) || (touching && !touch_assisted)) { control->magnetism_level = 0.f; } } if (player_magnetism_flag && control->magnetism_level > 0.f && - (fabs(clamped_yaw) > _real_epsilon || + (touching || + fabs(clamped_yaw) > _real_epsilon || fabs(clamped_pitch) > _real_epsilon || fabs(input->throttle.i) > _real_epsilon || fabs(input->throttle.j) > _real_epsilon)) diff --git a/source/input/input_abstraction.c b/source/input/input_abstraction.c index 3d418072..195f52fe 100644 --- a/source/input/input_abstraction.c +++ b/source/input/input_abstraction.c @@ -323,6 +323,22 @@ real input_abstraction_port_primary_trigger( return gamepad->buttons[button_index] ? 1.f : 0.f; } +/* port: whether the controller's player looks inverted, as its right stick +does (input_abstraction_update): its profile's invert, or flying an aircraft +with its profile's aircraft invert; the touch controls' swipe follows it +(player_control.c) */ +boolean input_abstraction_port_look_inverted( + short controller_index) +{ + struct game_input_preferences const *preferences; + + if (controller_index < 0 || controller_index >= MAXIMUM_GAMEPADS) + return FALSE; + preferences = &input_abstraction_globals.player_control_preferences[controller_index]; + return preferences->invert_look || + (preferences->invert_look_aircraft_control && local_player_is_piloting_aircraft(controller_index)); +} + /* ---------- public code */ void input_abstraction_initialize( diff --git a/source/input/input_abstraction.h b/source/input/input_abstraction.h index 0724fce2..4b439a67 100644 --- a/source/input/input_abstraction.h +++ b/source/input/input_abstraction.h @@ -62,6 +62,9 @@ boolean input_abstraction_port_action_only( short controller_index); real input_abstraction_port_primary_trigger( short controller_index); +/* port: the player's look inverted, as its stick is (the touch controls) */ +boolean input_abstraction_port_look_inverted( + short controller_index); /* ---------- globals */ From fc6100447903fba3175c7e355a0db1cf4773b3cd Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:10:43 +1300 Subject: [PATCH 122/187] Touch controls: the profile's buttons and sticks, finger sizes, opacity The stick moves the player through the game's input state (keyboard_controls_update), as the keys do, so it moves with a southpaw or legacy stick layout too, where it was the left stick. The buttons are named by the profile's button layout, which the game sends the overlay as it changes (host_touch_bindings): with Swap triggers the right trigger's button says Grenade. No control is smaller than a finger (48 dp across). The overlay's opacity can be set; a floating move stick (off by default) goes where the thumb lands in the lower left. Hide is remembered, and Hide and Options act as the finger lifts on them, so a swipe that starts on them aims rather than opens the options mid-fight. A label too long for its button is drawn smaller. TouchControls, TouchLayout, GyroscopeAim and HaloActivity's touch layout files in the repository's style; the layout file is version 3 (opacity, floating stick), and versions 1 and 2 still load. CI runs the layout tests. --- .github/workflows/build.yml | 4 + port/android/README.md | 28 +- .../java/com/halo/decomp/GyroscopeAim.java | 72 +- .../java/com/halo/decomp/HaloActivity.java | 154 +- .../java/com/halo/decomp/TouchControls.java | 1528 +++++++++++------ .../java/com/halo/decomp/TouchLayout.java | 322 +++- port/android/guest/runtime/guest_host.h | 3 + port/android/host/host_touch.c | 33 + port/android/host_imports.list | 1 + port/linux/game/touch_game.c | 17 + port/linux/src/touch_input.c | 43 +- port/linux/src/touch_input.h | 4 + port/linux/src/xinput_sdl.c | 16 + source/input/input_abstraction.c | 41 + source/input/input_abstraction.h | 6 +- tools/tests/TouchLayoutTest.java | 38 +- 16 files changed, 1619 insertions(+), 691 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 072053af..7554303c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -157,6 +157,10 @@ jobs: HALO_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} run: python3 tools/ci_build.py android release + # the touch controls' layout and gyroscope maths (the runner's JDK) + - name: Test the touch layout + run: python3 tools/test_touch_layout.py + - name: Compile cache run: ccache --show-stats diff --git a/port/android/README.md b/port/android/README.md index e79824b7..e1003119 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -145,7 +145,7 @@ on-screen keyboard, tap a key to press it, "B =BACK" to cancel and ### Touch controls In a game, the app shows touch controls over the picture. They are a -controller for player 1: +controller for player 1. With the default buttons of the profile: | Control | Function in the game | | --- | --- | @@ -159,8 +159,13 @@ controller for player 1: | Pause, Back | start, back | | Up, Down, Left, Right | D-pad | +The names on the buttons follow the profile's "Button layout" (Settings > +Gamepads): with "Swap triggers", the right trigger's button says "Grenade". +The stick moves the player with every "Stick layout", southpaw too. + A finger that holds a button can also swipe to look, so you can fire and -aim with one thumb. A short tap reaches the game even when it is shorter +aim with one thumb. No control is smaller than a finger (48 dp across), +whatever the size of the screen. A short tap reaches the game even when it is shorter than one frame. Swipes that start in the edge-gesture zones of Android do not turn the view. The swipe aims as the controller's stick does, not as a mouse: the aim slows over a target and follows a moving one, as with a @@ -177,17 +182,23 @@ to "Settings"). The buttons at the top of the screen: -- "Hide" removes the controls until you push "Touch". +- "Hide" removes the controls until you push "Touch"; the app remembers + it. - "Options" opens these items: - "General": the phone's vibration (on by default; it follows the vibration setting of the game's profile), aiming with the gyroscope - (off by default), "Hide or add buttons" (hide a button, add a copy of - a button) and "Edit buttons size". + (off by default), a floating move stick (off by default: the stick goes + where your thumb lands in the lower left of the screen), the opacity of + the controls, "Hide or add buttons" (hide a button, add a copy of a + button) and "Edit buttons size". - "Edit buttons layout": drag the controls to new positions, then push - "Save and exit". "Export" and "Import" write and read a layout file - with the file picker of the system. + "Save". "Export" and "Import" write and read a layout file with the + file picker of the system. - "Look sensitivity". +"Hide" and "Options" act when the finger lifts on them: a swipe that +starts on them turns the view instead. + The app keeps the layout in its own preferences, not in `config.toml`. Removing the app's data or the app removes the layout. @@ -417,7 +428,8 @@ assembly of the port is necessary: available, the app shows a message. - Touch operates the menus and skips cinematics. In the game, the touch controls appear when no controller is connected; their size follows the - height of the screen, so they are larger on a tablet than on a phone. + height of the screen (larger on a tablet than on a phone), never smaller + than a finger. - Kernels with 16 KB pages (a developer option of Android 15) do not operate. The Xbox memory uses 4 KB pages. - The x86 Android emulator runs the app through its ARM translation. The diff --git a/port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java b/port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java index 18ac94c3..9c2bdb72 100644 --- a/port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java +++ b/port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java @@ -1,35 +1,71 @@ package com.halo.decomp; -/** Integrates angular velocity in display coordinates; output is in radians. */ +/** + * Gyroscope aiming (TouchControls): integrates the sensor's angular velocity, + * turned into the display's coordinates for its rotation, into radians of + * view turn between samples. + */ final class GyroscopeAim { + /** below this (radians a second) a reading is the sensor's noise at rest, which must not drift the view */ + private static final float NOISE = 0.01f; + /** a gap longer than this (seconds) starts over rather than turn at once */ + private static final float LONGEST_GAP = 0.1f; + private long previousTime; private int previousRotation = -1; - private float previousX, previousY; + private float previousX; + private float previousY; - void reset() { previousTime = 0; previousRotation = -1; } + void reset() { + previousTime = 0; + previousRotation = -1; + } + /** + * takes one sample (its time in nanoseconds, the angular velocity about + * the device's x and y axes, the display's rotation 0..3) and puts the turn + * since the last into delta {yaw, pitch}; false while there is none + */ boolean sample(long time, float x, float y, int rotation, float[] delta) { - delta[0] = delta[1] = 0; + delta[0] = 0; + delta[1] = 0; if (!Float.isFinite(x) || !Float.isFinite(y) || rotation < 0 || rotation > 3) { - reset(); return false; + reset(); + return false; } - float screenX = x, screenY = y; + float screenX = x; + float screenY = y; switch (rotation) { - case 1: screenX = y; screenY = -x; break; - case 2: screenX = -x; screenY = -y; break; - case 3: screenX = -y; screenY = x; break; + case 1: + screenX = y; + screenY = -x; + break; + case 2: + screenX = -x; + screenY = -y; + break; + case 3: + screenX = -y; + screenY = x; + break; + default: + break; } - // Small stationary sensor noise must not gradually drift the camera. - if (Math.abs(screenX) < 0.01f) screenX = 0; - if (Math.abs(screenY) < 0.01f) screenY = 0; - float dt = (time-previousTime)*1e-9f; - boolean valid = previousTime != 0 && rotation == previousRotation && dt > 0 && dt <= 0.1f; + if (Math.abs(screenX) < NOISE) + screenX = 0; + if (Math.abs(screenY) < NOISE) + screenY = 0; + float seconds = (time - previousTime) * 1e-9f; + boolean valid = previousTime != 0 && rotation == previousRotation && seconds > 0 && seconds <= LONGEST_GAP; if (valid) { - delta[0] = -(screenY+previousY)*0.5f*dt; - delta[1] = -(screenX+previousX)*0.5f*dt; + // the trapezoid rule over the two samples + delta[0] = -(screenY + previousY) * 0.5f * seconds; + delta[1] = -(screenX + previousX) * 0.5f * seconds; } - previousTime = time; previousRotation = rotation; - previousX = screenX; previousY = screenY; + previousTime = time; + previousRotation = rotation; + previousX = screenX; + previousY = screenY; return valid && (delta[0] != 0 || delta[1] != 0); } } diff --git a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java index 2cce0b25..84cc3023 100644 --- a/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java +++ b/port/android/app/src/main/java/com/halo/decomp/HaloActivity.java @@ -3,21 +3,23 @@ package com.halo.decomp; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; +import android.graphics.Insets; import android.net.Uri; import android.net.wifi.WifiManager; +import android.os.Build; import android.os.Bundle; +import android.view.Display; +import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowManager; import android.widget.Toast; + import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import android.graphics.Insets; -import android.os.Build; -import android.view.Display; -import android.view.View; -import android.view.WindowInsets; -import android.view.WindowManager; import org.libsdl.app.SDLActivity; @@ -64,11 +66,14 @@ public class HaloActivity extends SDLActivity { /** the system's file picker, for the touch layout; no storage permission */ public void chooseLayoutFile(boolean export, String configuration) { - new AlertDialog.Builder(this).setTitle(export ? "Export layout" : "Import layout") - .setMessage(export ? "Choose the folder and file name for your touch layout." - : "Choose an exported touch layout. It replaces your buttons, sensitivity and General settings.") + String message = export + ? "Choose the folder and the name of the touch layout file." + : "Choose a touch layout file. It replaces your buttons, the look sensitivity and the General settings."; + new AlertDialog.Builder(this) + .setTitle(export ? "Export the layout" : "Import a layout") + .setMessage(message) .setNegativeButton("Cancel", null) - .setPositiveButton(export ? "Choose location" : "Choose file", (dialog, which) -> { + .setPositiveButton(export ? "Choose the place" : "Choose the file", (dialog, which) -> { Intent intent = new Intent(export ? Intent.ACTION_CREATE_DOCUMENT : Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(export ? "text/plain" : "*/*"); @@ -76,68 +81,97 @@ public class HaloActivity extends SDLActivity { pendingLayoutExport = configuration; intent.putExtra(Intent.EXTRA_TITLE, "halo-touch-layout.halolayout"); } - try { startActivityForResult(intent, export ? EXPORT_LAYOUT : IMPORT_LAYOUT); } - catch (android.content.ActivityNotFoundException e) { + try { + startActivityForResult(intent, export ? EXPORT_LAYOUT : IMPORT_LAYOUT); + } catch (android.content.ActivityNotFoundException e) { pendingLayoutExport = null; - Toast.makeText(this, "No document picker is available.", Toast.LENGTH_LONG).show(); + Toast.makeText(this, "This device has no file picker.", Toast.LENGTH_LONG).show(); } - }).show(); + }) + .show(); } - @Override protected void onSaveInstanceState(Bundle state) { + @Override + protected void onSaveInstanceState(Bundle state) { super.onSaveInstanceState(state); state.putString("pending-layout-export", pendingLayoutExport); } - @Override protected void onActivityResult(int request, int result, Intent data) { + @Override + protected void onActivityResult(int request, int result, Intent data) { if (request != EXPORT_LAYOUT && request != IMPORT_LAYOUT) { - super.onActivityResult(request, result, data); return; + super.onActivityResult(request, result, data); + return; } - String exported = pendingLayoutExport; pendingLayoutExport = null; - if (result != RESULT_OK || data == null || data.getData() == null) return; + String exported = pendingLayoutExport; + pendingLayoutExport = null; + if (result != RESULT_OK || data == null || data.getData() == null) + return; Uri document = data.getData(); + // (the file is read and written away from the UI thread) new Thread(() -> { try { - if (request == EXPORT_LAYOUT) { - if (exported == null) throw new java.io.IOException("Layout snapshot is unavailable"); - try (OutputStream output = getContentResolver().openOutputStream(document, "wt")) { - if (output == null) throw new java.io.IOException("Cannot open destination"); - output.write(exported.getBytes(StandardCharsets.UTF_8)); - } - runOnUiThread(() -> Toast.makeText(this, "Layout exported.", Toast.LENGTH_SHORT).show()); - } else { - ByteArrayOutputStream contents = new ByteArrayOutputStream(); - try (InputStream input = getContentResolver().openInputStream(document)) { - if (input == null) throw new java.io.IOException("Cannot open layout file"); - byte[] buffer = new byte[4096]; int size; - while ((size = input.read(buffer)) != -1) { - if (contents.size()+size > 65536) throw new java.io.IOException("Layout file is too large"); - contents.write(buffer, 0, size); - } - } - String configuration = new String(contents.toByteArray(), StandardCharsets.UTF_8); - // checked away from the UI thread, then applied to the overlay at once - TouchLayout.importConfiguration(configuration); - runOnUiThread(() -> { - if (isFinishing() || isDestroyed() || touchControls == null) return; - try { - touchControls.importLayout(configuration); - Toast.makeText(this, "Layout imported and saved.", Toast.LENGTH_SHORT).show(); - } catch (IllegalArgumentException e) { layoutFileError(e); } - }); - } - } catch (Exception e) { runOnUiThread(() -> layoutFileError(e)); } + if (request == EXPORT_LAYOUT) + writeLayoutFile(document, exported); + else + readLayoutFile(document); + } catch (Exception e) { + runOnUiThread(() -> layoutFileError(e)); + } }, "halo-touch-layout-file").start(); } - private void layoutFileError(Exception error) { - if (isFinishing() || isDestroyed()) return; - new AlertDialog.Builder(this).setTitle("Layout file") - .setMessage("Could not complete the operation: "+error.getMessage()) - .setPositiveButton("OK", null).show(); + private void writeLayoutFile(Uri document, String configuration) throws IOException { + if (configuration == null) + throw new IOException("the layout to export is gone"); + try (OutputStream output = getContentResolver().openOutputStream(document, "wt")) { + if (output == null) + throw new IOException("the file cannot be opened"); + output.write(configuration.getBytes(StandardCharsets.UTF_8)); + } + runOnUiThread(() -> Toast.makeText(this, "The layout is exported.", Toast.LENGTH_SHORT).show()); } - @Override protected void onResume() { + private void readLayoutFile(Uri document) throws IOException { + ByteArrayOutputStream contents = new ByteArrayOutputStream(); + try (InputStream input = getContentResolver().openInputStream(document)) { + if (input == null) + throw new IOException("the file cannot be opened"); + byte[] buffer = new byte[4096]; + int size; + while ((size = input.read(buffer)) != -1) { + if (contents.size() + size > 65536) + throw new IOException("the file is too large"); + contents.write(buffer, 0, size); + } + } + String configuration = new String(contents.toByteArray(), StandardCharsets.UTF_8); + // checked here, then applied to the overlay at once + TouchLayout.importConfiguration(configuration); + runOnUiThread(() -> { + if (isFinishing() || isDestroyed() || touchControls == null) + return; + try { + touchControls.importLayout(configuration); + Toast.makeText(this, "The layout is imported and saved.", Toast.LENGTH_SHORT).show(); + } catch (IllegalArgumentException e) { + layoutFileError(e); + } + }); + } + + private void layoutFileError(Exception error) { + if (isFinishing() || isDestroyed()) + return; + new AlertDialog.Builder(this) + .setTitle("Touch layout file") + .setMessage("The operation did not complete: " + error.getMessage()) + .setPositiveButton("OK", null) + .show(); + } + + @Override + protected void onResume() { super.onResume(); if (touchControls != null && getWindow().getDecorView().hasWindowFocus()) touchControls.startDeviceInput(); @@ -145,22 +179,26 @@ public class HaloActivity extends SDLActivity { @Override protected void onPause() { - if (touchControls != null) touchControls.stopDeviceInput(); + if (touchControls != null) + touchControls.stopDeviceInput(); super.onPause(); } @Override public void onWindowFocusChanged(boolean hasFocus) { if (touchControls != null) { - if (hasFocus) touchControls.startDeviceInput(); - else touchControls.stopDeviceInput(); + if (hasFocus) + touchControls.startDeviceInput(); + else + touchControls.stopDeviceInput(); } super.onWindowFocusChanged(hasFocus); } @Override protected void onDestroy() { - if (touchControls != null) touchControls.stopDeviceInput(); + if (touchControls != null) + touchControls.stopDeviceInput(); if (multicastLock != null && multicastLock.isHeld()) multicastLock.release(); multicastLock = null; diff --git a/port/android/app/src/main/java/com/halo/decomp/TouchControls.java b/port/android/app/src/main/java/com/halo/decomp/TouchControls.java index 5513d2ec..e2e808e5 100644 --- a/port/android/app/src/main/java/com/halo/decomp/TouchControls.java +++ b/port/android/app/src/main/java/com/halo/decomp/TouchControls.java @@ -1,41 +1,45 @@ package com.halo.decomp; import android.app.AlertDialog; -import android.media.AudioAttributes; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.hardware.input.InputManager; +import android.media.AudioAttributes; import android.os.Build; import android.os.SystemClock; import android.os.VibrationEffect; import android.os.Vibrator; import android.os.VibratorManager; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.View; import android.view.WindowManager; -import android.widget.Switch; -import android.content.Context; +import android.widget.Button; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.SeekBar; +import android.widget.Switch; import android.widget.TextView; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.hardware.input.InputManager; -import android.view.InputDevice; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.util.SparseIntArray; -import android.util.SparseArray; -import android.view.MotionEvent; -import android.view.View; import android.widget.Toast; +import java.util.Arrays; +import java.util.Locale; + /** * The on-screen touch controls: a multitouch overlay over SDL's surface, * with a stick, the controller's buttons, swipe (and optionally gyroscope) - * aiming and full-display layout editing. Its state goes to the game - * through port/android/host/host_touch.c, as port 0's controller. + * aiming and an editor for the layout (TouchLayout). Its state goes to the + * game through port/android/host/host_touch.c, as port 0's controller. * * It shows only in a game, never over the menus or a cinematic (which take * taps themselves, port/linux/src/touch_input.c), and with @@ -43,101 +47,183 @@ import android.widget.Toast; * is connected. While hidden it lets every finger through to the game. */ public final class TouchControls extends View implements SensorEventListener, InputManager.InputDeviceListener { - private static final int LEFT = TouchLayout.LEFT, LOOK = -5; - private static final int TOGGLE = -3, EDIT = -4, EXPORT = -6, IMPORT = -7; - private static final class Button { - final String label; - final float radius; - final int bit, trigger; - Button(String label, float radius, int bit, int trigger) { - this.label = label; this.radius = radius; - this.bit = bit; this.trigger = trigger; + /** what a finger holds besides a control: the view swipe */ + private static final int LOOK = -5; + private static final int NOTHING = Integer.MIN_VALUE; + /** the toolbar's items (at the top of the screen) */ + private static final int TOOLBAR_TOGGLE = -3; + private static final int TOOLBAR_OPTIONS = -4; + private static final int TOOLBAR_EXPORT = -6; + private static final int TOOLBAR_IMPORT = -7; + private static final float TOOLBAR_RADIUS = 28; + private static final float OPTIONS_RADIUS = 34; + + /** the logical grid the controls are laid out on, scaled to the display's height */ + private static final float GRID_WIDTH = 960; + private static final float GRID_HEIGHT = 540; + /** a control is never smaller than a finger: 24 dp of radius, 48 across */ + private static final float MINIMUM_RADIUS_DP = 24; + /** a finger moves this far (logical units) before a press becomes a swipe */ + private static final float SLOP = 10; + /** the move stick's dead zone and its touch area beyond its circle */ + private static final float STICK_DEAD_ZONE = 0.12f; + private static final float STICK_REACH = 1.28f; + private static final float STICK_KNOB_RADIUS = 24; + /** a floating stick goes where a thumb lands in this part of the display (from the lower left) */ + private static final float FLOATING_ZONE_WIDTH = 0.45f; + private static final float FLOATING_ZONE_TOP = 0.35f; + /** radians per logical unit of swipe (the mouse's rate: xinput_sdl.c) */ + private static final float LOOK_RADIANS_PER_UNIT = 0.0022f; + /** a label is at most this many radii wide */ + private static final float LABEL_WIDTH = 1.7f; + private static final long POLL_MS = 16; + private static final long VIBRATION_MS = 110; + private static final long VIBRATION_RENEW_MS = 70; + + /** the game's HALO_TOUCH_SCENE bits (port/linux/src/touch_input.c) */ + private static final int SCENE_KNOWN = 1; + private static final int SCENE_MENUS = 2; + private static final int SCENE_ON = 4; + private static final int SCENE_OFF = 8; + + private static final String PREFERENCES = "touch-layout-v1"; + private static final String KEY_CONFIGURATION = "configuration"; + private static final String KEY_HIDDEN = "hidden"; + + /** + * a kind of control: the SDL button bit or trigger axis it sends (-1 for + * none), and the game's controller button it is (port/linux/game/ + * touch_game.c numbers them: input.h's _gamepad_*), which names it + */ + private static final class Kind { + final int bit; + final int trigger; + final int gamepadButton; + final String buttonName; + final boolean faceButton; + + Kind(int bit, int trigger, int gamepadButton, String buttonName, boolean faceButton) { + this.bit = bit; + this.trigger = trigger; + this.gamepadButton = gamepadButton; + this.buttonName = buttonName; + this.faceButton = faceButton; } } - // Button bits follow SDL_GamepadButton; triggers are SDL axes 4 and 5. - private final Button[] buttons = { - new Button("A / Jump", 36, 0, -1), - new Button("B / Melee", 32, 1, -1), - new Button("X / Reload", 34, 2, -1), - new Button("Y / Weapon", 32, 3, -1), - new Button("Fire", 39, -1, 5), - new Button("Grenade", 35, -1, 4), - new Button("Crouch", 32, 7, -1), - new Button("Zoom", 32, 8, -1), - new Button("Light", 27, 9, -1), - new Button("Gren. type", 29, 10, -1), - new Button("Pause", 28, 6, -1), - new Button("Back", 28, 4, -1), - new Button("Up", 25, 11, -1), - new Button("Down", 25, 12, -1), - new Button("Left", 25, 13, -1), - new Button("Right", 25, 14, -1), - new Button("", 64, -1, -1), // movement stick keeps its saved index - new Button("Fire", 39, -1, 5) + + /** by TouchLayout's kinds; bits follow SDL_GamepadButton, triggers are SDL axes 4 and 5 */ + private static final Kind[] KINDS = { + new Kind(0, -1, 0, "A", true), + new Kind(1, -1, 1, "B", true), + new Kind(2, -1, 2, "X", true), + new Kind(3, -1, 3, "Y", true), + new Kind(-1, 5, 7, "RT", false), + new Kind(-1, 4, 6, "LT", false), + new Kind(7, -1, 14, "LS", false), + new Kind(8, -1, 15, "RS", false), + new Kind(9, -1, 5, "White", false), + new Kind(10, -1, 4, "Black", false), + new Kind(6, -1, 12, "Start", false), + new Kind(4, -1, 13, "Back", false), + new Kind(11, -1, 8, "Up", false), + new Kind(12, -1, 9, "Down", false), + new Kind(13, -1, 10, "Left", false), + new Kind(14, -1, 11, "Right", false), + new Kind(-1, -1, -1, "Move", false), + new Kind(-1, 5, 7, "RT", false), }; + + /** the game's controls (input_abstraction.c's _game_control_*), as the buttons name them */ + private static final String[] GAME_CONTROLS = { + "Jump", "Gren. type", "Reload", "Weapon", "Melee", "Light", + "Grenade", "Fire", "Pause", "Back", "Crouch", "Zoom", + }; + /** the default profile's mapping, until the game sends the player's own */ + private static final int[] DEFAULT_BINDINGS = { + 0, 4, 2, 3, 1, 5, 6, 7, -1, -1, -1, -1, 8, 9, 10, 11, + }; + + private static native void nativeState(int lx, int ly, int rx, int ry, int lt, int rt, int buttons); + private static native void nativeLook(float dx, float dy); + private static native void nativeLookReset(); + private static native int nativeRumble(); + private static native int nativeScene(); + private static native int nativeBindings(int[] controls); + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final SparseIntArray owners = new SparseIntArray(); - private final SparseArray buttonTouches = new SparseArray<>(); - private final int[] axes = new int[6]; - private TouchLayout layout = new TouchLayout(); private final SharedPreferences preferences; - private boolean editing, optionsOpen; private final SensorManager sensors; private final Sensor gyroscope; private final Vibrator vibrator; + private final InputManager inputManager; + private final boolean touchscreenFeature; private final GyroscopeAim gyroAim = new GyroscopeAim(); private final float[] gyroDelta = new float[2]; - private boolean deviceInputActive, gyroRegistered; + private final AudioAttributes rumbleAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_GAME) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(); + + private TouchLayout layout = new TouchLayout(); + private float sensitivity = 1f; + /** the controls removed by the toolbar's Hide, until its Touch */ + private boolean hidden; + private boolean editing; + private boolean optionsOpen; + + /** what each finger holds: a control, LOOK, or (in toolbarFingers) a toolbar item */ + private final SparseIntArray owners = new SparseIntArray(); + /** where each finger on a button went down, in pixels: dragged, it swipes */ + private final SparseArray buttonTouches = new SparseArray<>(); + /** each finger on a toolbar item: {x, y, item}; the item acts when it lifts there */ + private final SparseArray toolbarFingers = new SparseArray<>(); + private final int[] axes = new int[6]; + private int lookPointer = -1; + private float lookX; + private float lookY; + /** a floating stick's centre while a thumb holds it (logical units) */ + private boolean stickFloating; + private float stickX; + private float stickY; + private int dragPointer = -1; + private int dragControl = -1; + private float dragOffsetX; + private float dragOffsetY; + + /** pixels per logical unit, and the display in logical units */ + private float scale = 1; + private float logicalWidth = GRID_WIDTH; + private float logicalHeight = GRID_HEIGHT; + private int insetRight; + private int insetTop; + + private int scene; + private boolean controllerConnected; + private boolean touchscreen; + private boolean deviceInputActive; + private boolean gyroRegistered; private int lastAmplitude; private long lastVibration; - private final AudioAttributes rumbleAttributes = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_GAME).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build(); - private static native int nativeRumble(); - /** reads what the game says every frame: when to show, and the rumble */ + private final int[] bindings = DEFAULT_BINDINGS.clone(); + private int bindingsSerial; + + /** reads what the game says every frame: when to show, the buttons' names, and the rumble */ private final Runnable poll = new Runnable() { + @Override public void run() { - if (!deviceInputActive) return; + if (!deviceInputActive) + return; boolean wasShown = shown(); scene = nativeScene(); if (shown() != wasShown) { - reset(); updateSensors(); + reset(); + updateSensors(); } - int amplitude = shown() && layout.rumbleEnabled && !editing && !optionsOpen ? nativeRumble() : 0; - if (vibrator != null && vibrator.hasVibrator()) { - if (amplitude == 0) cancelRumble(); - else if (amplitude != lastAmplitude || SystemClock.uptimeMillis()-lastVibration >= 70) { - try { - vibrator.vibrate(VibrationEffect.createOneShot(110, - vibrator.hasAmplitudeControl() ? amplitude : VibrationEffect.DEFAULT_AMPLITUDE), rumbleAttributes); - lastAmplitude = amplitude; lastVibration = SystemClock.uptimeMillis(); - } catch (RuntimeException e) { cancelRumble(); } - } - } - postDelayed(this, 16); + readBindings(); + updateRumble(); + postDelayed(this, POLL_MS); } }; - private int lookPointer = -1; - private float lookX, lookY; - private float sensitivity; - private float logicalWidth = 960, logicalHeight = 540; - private static native void nativeLook(float dx, float dy); - private static native void nativeLookReset(); - private int dragPointer = -1, dragControl = -1; - private float dragOffsetX, dragOffsetY; - private float scale = 1, offsetX, offsetY; - private int insetLeft, insetRight, insetTop, insetBottom; - private boolean visible = true; - - private static native void nativeState(int lx, int ly, int rx, int ry, - int lt, int rt, int buttons); - - /** the game's HALO_TOUCH_SCENE bits (port/linux/src/touch_input.c) */ - private static native int nativeScene(); - private static final int SCENE_KNOWN = 1, SCENE_MENUS = 2, SCENE_ON = 4, SCENE_OFF = 8; - private int scene; - private final InputManager inputManager; - private final boolean touchscreenFeature; - private boolean controllerConnected, touchscreen; public TouchControls(Context context) { super(context); @@ -146,125 +232,113 @@ public final class TouchControls extends View implements SensorEventListener, In if (Build.VERSION.SDK_INT >= 31) { VibratorManager manager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE); vibrator = manager == null ? null : manager.getDefaultVibrator(); - } else vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + } else { + vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + } inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); touchscreenFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); + preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + loadLayout(context); + hidden = preferences.getBoolean(KEY_HIDDEN, false); refreshInputDevices(); - preferences = context.getSharedPreferences("touch-layout-v1", Context.MODE_PRIVATE); - String configuration = preferences.getString("configuration", null); - if (configuration != null) { - try { - TouchLayout.Configuration saved = TouchLayout.importConfiguration(configuration); - layout = saved.layout; sensitivity = saved.sensitivity; - } catch (IllegalArgumentException e) { - Toast.makeText(context, "Saved layout could not be loaded. Using defaults.", Toast.LENGTH_LONG).show(); - } - } else { - for (int i = 0; i < layout.size(); i++) - if (i != TouchLayout.FIRE_LEFT || preferences.getBoolean("swipe-layout", false)) - layout.restore(i, preferences.getFloat("x"+i, layout.x(i)), - preferences.getFloat("y"+i, layout.y(i))); - } - sensitivity = preferences.getFloat("look-sensitivity", sensitivity > 0 ? sensitivity : 1f); - if (!Float.isFinite(sensitivity) || sensitivity < 0.25f || sensitivity > 4f) sensitivity = 1f; setFocusable(false); setContentDescription("Halo touch controller"); setOnApplyWindowInsetsListener((view, insets) -> { - insetLeft = insets.getSystemWindowInsetLeft(); insetRight = insets.getSystemWindowInsetRight(); insetTop = insets.getSystemWindowInsetTop(); - insetBottom = insets.getSystemWindowInsetBottom(); - if (android.os.Build.VERSION.SDK_INT >= 28 && insets.getDisplayCutout() != null) { - insetLeft = Math.max(insetLeft, insets.getDisplayCutout().getSafeInsetLeft()); + if (insets.getDisplayCutout() != null) { insetRight = Math.max(insetRight, insets.getDisplayCutout().getSafeInsetRight()); insetTop = Math.max(insetTop, insets.getDisplayCutout().getSafeInsetTop()); - insetBottom = Math.max(insetBottom, insets.getDisplayCutout().getSafeInsetBottom()); } layoutControls(); return insets; }); } - private void layoutControls() { - if (getWidth() <= 0 || getHeight() <= 0) return; - reset(); - // Keep circles proportional while allowing placement across the complete display. - float width = Math.max(1, getWidth()), height = Math.max(1, getHeight()); - scale = Math.max(0.01f, Math.min(width / 960f, height / 540f)); - offsetX = offsetY = 0; - logicalWidth = width / scale; logicalHeight = height / scale; - layout.bounds(logicalWidth, logicalHeight); - invalidate(); + private void loadLayout(Context context) { + String configuration = preferences.getString(KEY_CONFIGURATION, null); + if (configuration == null) + return; + try { + TouchLayout.Configuration saved = TouchLayout.importConfiguration(configuration); + layout = saved.layout; + sensitivity = saved.sensitivity; + } catch (IllegalArgumentException e) { + Toast.makeText(context, "The saved touch layout could not be read. Using the default one.", + Toast.LENGTH_LONG).show(); + } } - @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { + // ---------- the activity's lifecycle (HaloActivity) + + public void startDeviceInput() { + if (deviceInputActive) + return; + deviceInputActive = true; + if (inputManager != null) + inputManager.registerInputDeviceListener(this, null); + refreshInputDevices(); + gyroAim.reset(); + updateSensors(); + post(poll); + } + + public void stopDeviceInput() { + deviceInputActive = false; + removeCallbacks(poll); + if (inputManager != null) + inputManager.unregisterInputDeviceListener(this); + updateSensors(); + cancelRumble(); + reset(); + } + + // ---------- the display + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { layoutControls(); requestApplyInsets(); } - public void reset() { - dragPointer = dragControl = -1; - owners.clear(); - buttonTouches.clear(); - lookPointer = -1; - gyroAim.reset(); - nativeLookReset(); - java.util.Arrays.fill(axes, 0); - publish(); + /** scales the grid to the display (its height, keeping the circles round) */ + private void layoutControls() { + if (getWidth() <= 0 || getHeight() <= 0) + return; + reset(); + float width = getWidth(); + float height = getHeight(); + scale = Math.max(0.01f, Math.min(width / GRID_WIDTH, height / GRID_HEIGHT)); + logicalWidth = width / scale; + logicalHeight = height / scale; + layout.bounds(logicalWidth, logicalHeight); + layout.setMinimumRadius(minimumRadius()); invalidate(); } - private void publish() { - if (editing || optionsOpen) { - nativeState(0, 0, 0, 0, 0, 0, 0); - return; - } - int bits = 0; - axes[4] = axes[5] = 0; - for (int i = 0; i < owners.size(); i++) { - int control = owners.valueAt(i); - if (control < 0 || control >= layout.size()) continue; - Button b = buttons[layout.type(control)]; - if (b.bit >= 0) bits |= 1 << b.bit; - if (b.trigger >= 0) axes[b.trigger] = 32767; - } - nativeState(axes[0], axes[1], axes[2], axes[3], axes[4], axes[5], bits); + /** a finger's width, in logical units */ + private float minimumRadius() { + return MINIMUM_RADIUS_DP * getResources().getDisplayMetrics().density / scale; } - private boolean held(int control) { - return owners.indexOfValue(control) >= 0; + private float logical(float pixels) { + return pixels / scale; } - private static boolean inside(float x, float y, float cx, float cy, float radius) { - return (x-cx)*(x-cx) + (y-cy)*(y-cy) <= radius*radius; + private float toolbarY() { + return Math.max(38, insetTop / scale + 38); } - private int hit(float x, float y) { - if (inside(x, y, optionsX(), toolbarY(), 34)) return EDIT; - if (editing && inside(x, y, optionsX()-156, toolbarY(), 34)) return EXPORT; - if (editing && inside(x, y, optionsX()-78, toolbarY(), 34)) return IMPORT; - if (inside(x, y, logicalWidth/2, toolbarY(), 28)) return TOGGLE; - if (!visible) return Integer.MIN_VALUE; - for (int i = 0; i < layout.size(); i++) { - if (!layout.shown(i) || layout.type(i) == LEFT) continue; - Button b = buttons[layout.type(i)]; - if (inside(x, y, layout.x(i), layout.y(i), layout.radius(i))) return i; - } - if (layout.shown(LEFT) && !held(LEFT) && inside(x, y, layout.x(LEFT), layout.y(LEFT), layout.radius(LEFT)*1.28f)) return LEFT; - return Integer.MIN_VALUE; + private float optionsX() { + return logicalWidth - Math.max(42, insetRight / scale + 42); } - private void moveStick(int control, float x, float y) { - int axis = 0; - float dx = (x - layout.x(control)) / layout.radius(control); - float dy = (y - layout.y(control)) / layout.radius(control); - float length = (float)Math.sqrt(dx*dx + dy*dy); - if (length < 0.12f) { dx = 0; dy = 0; } - else if (length > 1) { dx /= length; dy /= length; } - axes[axis] = Math.round(dx * 32767); - axes[axis+1] = Math.round(dy * 32767); + private float toolbarRadius(float radius) { + return Math.max(radius, minimumRadius()); } + // ---------- when the controls show + /** * whether the controls are on screen: in a game (no menu or cinematic), * never without a touchscreen, and with input.touch_controls = "auto" @@ -278,32 +352,165 @@ public final class TouchControls extends View implements SensorEventListener, In /** a game controller: a stick or a hat, not a phone's few gamepad keys */ private static boolean isController(InputDevice device) { - if (device == null || device.isVirtual()) return false; - int sources = device.getSources(); - if ((sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) return false; + if (device == null || device.isVirtual()) + return false; + if ((device.getSources() & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) + return false; return device.getMotionRange(MotionEvent.AXIS_X, InputDevice.SOURCE_JOYSTICK) != null || device.getMotionRange(MotionEvent.AXIS_HAT_X, InputDevice.SOURCE_JOYSTICK) != null; } private void refreshInputDevices() { - boolean controller = false, touch = touchscreenFeature; + boolean controller = false; + boolean touch = touchscreenFeature; for (int id : InputDevice.getDeviceIds()) { InputDevice device = InputDevice.getDevice(id); - if (device == null) continue; - if (isController(device)) controller = true; - if (!device.isVirtual() && (device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN) + if (device == null) + continue; + if (isController(device)) + controller = true; + boolean touchSource = (device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN; + if (!device.isVirtual() && touchSource) touch = true; } boolean wasShown = shown(); - controllerConnected = controller; touchscreen = touch; + controllerConnected = controller; + touchscreen = touch; if (shown() != wasShown) { - reset(); updateSensors(); + reset(); + updateSensors(); } } - @Override public void onInputDeviceAdded(int deviceId) { refreshInputDevices(); } - @Override public void onInputDeviceRemoved(int deviceId) { refreshInputDevices(); } - @Override public void onInputDeviceChanged(int deviceId) { refreshInputDevices(); } + @Override + public void onInputDeviceAdded(int deviceId) { + refreshInputDevices(); + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + refreshInputDevices(); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + refreshInputDevices(); + } + + private void setHidden(boolean hidden) { + reset(); + this.hidden = hidden; + preferences.edit().putBoolean(KEY_HIDDEN, hidden).apply(); + } + + // ---------- the controller state the game reads + + /** lets go of every finger and sends a released controller */ + public void reset() { + dragPointer = -1; + dragControl = -1; + owners.clear(); + buttonTouches.clear(); + toolbarFingers.clear(); + lookPointer = -1; + stickFloating = false; + gyroAim.reset(); + nativeLookReset(); + Arrays.fill(axes, 0); + publish(); + invalidate(); + } + + private void publish() { + if (editing || optionsOpen) { + nativeState(0, 0, 0, 0, 0, 0, 0); + return; + } + int bits = 0; + axes[4] = 0; + axes[5] = 0; + for (int i = 0; i < owners.size(); i++) { + int control = owners.valueAt(i); + if (control < 0 || control >= layout.size()) + continue; + Kind kind = KINDS[layout.type(control)]; + if (kind.bit >= 0) + bits |= 1 << kind.bit; + if (kind.trigger >= 0) + axes[kind.trigger] = 32767; + } + nativeState(axes[0], axes[1], axes[2], axes[3], axes[4], axes[5], bits); + } + + private boolean held(int control) { + return owners.indexOfValue(control) >= 0; + } + + private float stickCenterX() { + return stickFloating ? stickX : layout.x(TouchLayout.LEFT); + } + + private float stickCenterY() { + return stickFloating ? stickY : layout.y(TouchLayout.LEFT); + } + + /** the floating stick, centred where a thumb landed, kept on the display */ + private void floatStick(float x, float y) { + float radius = layout.radius(TouchLayout.LEFT); + stickFloating = true; + stickX = Math.max(radius, Math.min(logicalWidth - radius, x)); + stickY = Math.max(radius, Math.min(logicalHeight - radius, y)); + } + + private boolean inFloatingZone(float x, float y) { + return x < logicalWidth * FLOATING_ZONE_WIDTH && y > logicalHeight * FLOATING_ZONE_TOP; + } + + private void moveStick(int control, float x, float y) { + float dx = (x - stickCenterX()) / layout.radius(control); + float dy = (y - stickCenterY()) / layout.radius(control); + float length = (float) Math.sqrt(dx * dx + dy * dy); + if (length < STICK_DEAD_ZONE) { + dx = 0; + dy = 0; + } else if (length > 1) { + dx /= length; + dy /= length; + } + axes[0] = Math.round(dx * 32767); + axes[1] = Math.round(dy * 32767); + } + + // ---------- fingers + + private static boolean inside(float x, float y, float cx, float cy, float radius) { + return (x - cx) * (x - cx) + (y - cy) * (y - cy) <= radius * radius; + } + + /** what is under a point (logical units): a toolbar item, a control, or NOTHING */ + private int hit(float x, float y) { + if (inside(x, y, optionsX(), toolbarY(), toolbarRadius(OPTIONS_RADIUS))) + return TOOLBAR_OPTIONS; + if (editing && inside(x, y, optionsX() - 156, toolbarY(), toolbarRadius(OPTIONS_RADIUS))) + return TOOLBAR_EXPORT; + if (editing && inside(x, y, optionsX() - 78, toolbarY(), toolbarRadius(OPTIONS_RADIUS))) + return TOOLBAR_IMPORT; + if (!editing && inside(x, y, logicalWidth / 2, toolbarY(), toolbarRadius(TOOLBAR_RADIUS))) + return TOOLBAR_TOGGLE; + if (hidden && !editing) + return NOTHING; + for (int i = 0; i < layout.size(); i++) { + if (layout.shown(i) && layout.type(i) != TouchLayout.LEFT + && inside(x, y, layout.x(i), layout.y(i), layout.radius(i))) + return i; + } + int stick = TouchLayout.LEFT; + float reach = editing ? 1 : STICK_REACH; + if (layout.shown(stick) && !held(stick) + && inside(x, y, layout.x(stick), layout.y(stick), layout.radius(stick) * reach)) + return stick; + return NOTHING; + } /** * whether a finger went down where Android keeps its edge gestures; in @@ -311,383 +518,704 @@ public final class TouchControls extends View implements SensorEventListener, In * system bars, and Android also hands it over as an ordinary finger */ private boolean inGestureZone(float x, float y, boolean sidesOnly) { - int[] insets = ((HaloActivity)getContext()).getSystemGestureInsetsPixels(); - if (x < insets[0] || x >= getWidth()-insets[2]) return true; - return !sidesOnly && (y < insets[1] || y >= getHeight()-insets[3]); + int[] insets = ((HaloActivity) getContext()).getSystemGestureInsetsPixels(); + if (x < insets[0] || x >= getWidth() - insets[2]) + return true; + return !sidesOnly && (y < insets[1] || y >= getHeight() - insets[3]); } - @Override public boolean onTouchEvent(MotionEvent event) { - int action = event.getActionMasked(), index = event.getActionIndex(); - int id = event.getPointerId(index); - float x = (event.getX(index)-offsetX)/scale, y = (event.getY(index)-offsetY)/scale; + @Override + public boolean onTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); if (!shown() && !editing && !optionsOpen) { // the menus and cinematics take the fingers (SDL's surface below) - if (action == MotionEvent.ACTION_DOWN) return false; - if (owners.size() > 0 || lookPointer >= 0) { reset(); invalidate(); } + if (action == MotionEvent.ACTION_DOWN) + return false; + if (owners.size() > 0 || lookPointer >= 0) + reset(); return true; } - if (editing) return editTouch(event, action, index, id, x, y); - if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) - && inGestureZone(event.getX(index), event.getY(index), true)) { - return true; - } - if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { - int control = hit(x, y); - if (control == EDIT) { - showOptions(); performClick(); - } else if (control == TOGGLE) { - reset(); visible = !visible; performClick(); - } else if (control != Integer.MIN_VALUE) { - owners.put(id, control); - if (control >= 0 && layout.type(control) == LEFT) moveStick(control, x, y); - else buttonTouches.put(id, new float[]{event.getX(index), event.getY(index)}); - } else if (visible && lookPointer < 0 && !inGestureZone(event.getX(index), event.getY(index), false)) { - lookPointer = id; lookX = event.getX(index); lookY = event.getY(index); - owners.put(id, LOOK); - } - } else if (action == MotionEvent.ACTION_MOVE) { - for (int i = 0; i < event.getPointerCount(); i++) { - int control = owners.get(event.getPointerId(i), Integer.MIN_VALUE); - if (control >= 0 && layout.type(control) == LEFT) - moveStick(control, (event.getX(i)-offsetX)/scale, (event.getY(i)-offsetY)/scale); - int pointer = event.getPointerId(i); - float[] origin = buttonTouches.get(pointer); - if (lookPointer < 0 && origin != null && - Math.hypot(event.getX(i)-origin[0], event.getY(i)-origin[1]) > 10*scale) { - // A held action button can also aim, including either Fire button. - lookPointer = pointer; lookX = origin[0]; lookY = origin[1]; - } - if (pointer == lookPointer) { - float nx = event.getX(i), ny = event.getY(i); - nativeLook((nx-lookX)/scale*sensitivity, (ny-lookY)/scale*sensitivity); - lookX = nx; lookY = ny; - } - } - } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { - int control = owners.get(id, Integer.MIN_VALUE); - if (control >= 0 && layout.type(control) == LEFT) { - int axis = 0; - axes[axis] = axes[axis+1] = 0; - } - if (id == lookPointer) lookPointer = -1; - owners.delete(id); - buttonTouches.remove(id); - } else if (action == MotionEvent.ACTION_CANCEL) { - reset(); - } - publish(); invalidate(); + if (editing) + editTouch(event); + else + gameTouch(event); + publish(); + invalidate(); return true; } - @Override public boolean performClick() { super.performClick(); return true; } + @Override + public boolean performClick() { + super.performClick(); + return true; + } - private boolean editTouch(MotionEvent event, int action, int index, int id, float x, float y) { + private void gameTouch(MotionEvent event) { + int index = event.getActionIndex(); + int id = event.getPointerId(index); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + fingerDown(id, event.getX(index), event.getY(index)); + break; + case MotionEvent.ACTION_MOVE: + for (int i = 0; i < event.getPointerCount(); i++) + fingerMoved(event.getPointerId(i), event.getX(i), event.getY(i)); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + fingerUp(id, event.getX(index), event.getY(index)); + break; + case MotionEvent.ACTION_CANCEL: + reset(); + break; + default: + break; + } + } + + /** a finger down in a game, at a point in pixels */ + private void fingerDown(int id, float px, float py) { + if (inGestureZone(px, py, true)) + return; + float x = logical(px); + float y = logical(py); + int target = hit(x, y); + int stick = TouchLayout.LEFT; + if (target == NOTHING && layout.floatingStick && !hidden && layout.shown(stick) && !held(stick) + && inFloatingZone(x, y)) { + floatStick(x, y); + target = stick; + } + if (target == TOOLBAR_OPTIONS || target == TOOLBAR_TOGGLE) { + toolbarFingers.put(id, new float[] {px, py, target}); + } else if (target != NOTHING) { + owners.put(id, target); + if (layout.type(target) == TouchLayout.LEFT) + moveStick(target, x, y); + else + buttonTouches.put(id, new float[] {px, py}); + } else if (!hidden && lookPointer < 0 && !inGestureZone(px, py, false)) { + startLook(id, px, py); + } + } + + private void startLook(int id, float px, float py) { + lookPointer = id; + lookX = px; + lookY = py; + if (owners.get(id, NOTHING) == NOTHING) + owners.put(id, LOOK); + } + + private boolean movedPastSlop(float[] origin, float px, float py) { + return Math.hypot(px - origin[0], py - origin[1]) > SLOP * scale; + } + + private void fingerMoved(int id, float px, float py) { + int control = owners.get(id, NOTHING); + if (control >= 0 && layout.type(control) == TouchLayout.LEFT) + moveStick(control, logical(px), logical(py)); + float[] toolbar = toolbarFingers.get(id); + if (toolbar != null && movedPastSlop(toolbar, px, py)) { + // a swipe that began on the toolbar aims; the item does not act + toolbarFingers.remove(id); + if (!hidden && lookPointer < 0) + startLook(id, toolbar[0], toolbar[1]); + } + float[] origin = buttonTouches.get(id); + if (lookPointer < 0 && origin != null && movedPastSlop(origin, px, py)) { + // a held button (either Fire too) also aims + lookPointer = id; + lookX = origin[0]; + lookY = origin[1]; + } + if (id == lookPointer) { + nativeLook(logical(px - lookX) * sensitivity, logical(py - lookY) * sensitivity); + lookX = px; + lookY = py; + } + } + + private void fingerUp(int id, float px, float py) { + float[] toolbar = toolbarFingers.get(id); + if (toolbar != null) { + toolbarFingers.remove(id); + int item = (int) toolbar[2]; + if (hit(logical(px), logical(py)) == item) + toolbarAction(item); + return; + } + int control = owners.get(id, NOTHING); + if (control >= 0 && layout.type(control) == TouchLayout.LEFT) { + axes[0] = 0; + axes[1] = 0; + stickFloating = false; + } + if (id == lookPointer) + lookPointer = -1; + owners.delete(id); + buttonTouches.remove(id); + } + + private void toolbarAction(int item) { + if (item == TOOLBAR_OPTIONS) + showOptions(); + else if (item == TOOLBAR_TOGGLE) + setHidden(!hidden); + performClick(); + } + + /** a finger in the layout editor: drags controls; Save, Export and Import act at once */ + private void editTouch(MotionEvent event) { + int action = event.getActionMasked(); + int index = event.getActionIndex(); + int id = event.getPointerId(index); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { - if (dragPointer < 0) { - int control = hit(x, y); - if (control == EDIT) { - if (saveLayout()) { - reset(); editing = false; performClick(); - } - } else if (control == EXPORT || control == IMPORT) { + if (dragPointer >= 0) + return; + float x = logical(event.getX(index)); + float y = logical(event.getY(index)); + int target = hit(x, y); + if (target == TOOLBAR_OPTIONS) { + if (saveLayout()) { reset(); - ((HaloActivity)getContext()).chooseLayoutFile(control == EXPORT, exportLayout()); - } else if (control >= 0) { - dragPointer = id; dragControl = control; - dragOffsetX = x-layout.x(control); dragOffsetY = y-layout.y(control); + editing = false; + performClick(); } + } else if (target == TOOLBAR_EXPORT || target == TOOLBAR_IMPORT) { + reset(); + ((HaloActivity) getContext()).chooseLayoutFile(target == TOOLBAR_EXPORT, exportLayout()); + } else if (target >= 0) { + dragPointer = id; + dragControl = target; + dragOffsetX = x - layout.x(target); + dragOffsetY = y - layout.y(target); } } else if (action == MotionEvent.ACTION_MOVE && dragPointer >= 0) { int pointer = event.findPointerIndex(dragPointer); - if (pointer >= 0) - layout.move(dragControl, (event.getX(pointer)-offsetX)/scale-dragOffsetX, - (event.getY(pointer)-offsetY)/scale-dragOffsetY); - } else if (action == MotionEvent.ACTION_CANCEL || - ((action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) && id == dragPointer)) { - dragPointer = dragControl = -1; - } - publish(); invalidate(); - return true; - } - - private float toolbarY() { return Math.max(38, insetTop/scale+38); } - private float optionsX() { return logicalWidth-Math.max(42, insetRight/scale+42); } - - private void editorButton(Canvas canvas) { - circle(canvas, optionsX(), toolbarY(), 34, editing ? "Save" : "Options", false); - if (editing) { - circle(canvas, optionsX()-156, toolbarY(), 34, "Export", false); - circle(canvas, optionsX()-78, toolbarY(), 34, "Import", false); - paint.setTextSize(14); - canvas.drawText("Drag controls, then Save and exit", logicalWidth/2, toolbarY()+58, paint); - paint.setTextSize(9); - canvas.drawText("and exit", optionsX(), toolbarY()+17, paint); + if (pointer >= 0) { + layout.move(dragControl, logical(event.getX(pointer)) - dragOffsetX, + logical(event.getY(pointer)) - dragOffsetY); + } + } else if (action == MotionEvent.ACTION_CANCEL + || ((action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) && id == dragPointer)) { + dragPointer = -1; + dragControl = -1; } } - private void showOptions() { - optionsOpen = true; reset(); - AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("Options") - .setItems(new String[]{"General", "Edit buttons layout", "Look sensitivity"}, (d, which) -> { - if (which == 0) post(this::showGeneral); - else if (which == 1) { editing = true; visible = true; invalidate(); } - else post(this::showSensitivity); - }).setNegativeButton("Close", null).create(); - dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); }); - dialog.show(); + // ---------- the buttons' names, from the player's profile + + private void readBindings() { + int[] controls = new int[bindings.length]; + int serial = nativeBindings(controls); + if (serial == 0 || serial == bindingsSerial) + return; + bindingsSerial = serial; + if (!Arrays.equals(controls, bindings)) { + System.arraycopy(controls, 0, bindings, 0, bindings.length); + invalidate(); + } } + /** a control's name: its button and what the profile has it do ("A / Jump", "Fire", "RT") */ + private String label(int type) { + Kind kind = KINDS[type]; + if (kind.gamepadButton < 0) + return kind.buttonName; + int control = bindings[kind.gamepadButton]; + if (control < 0 || control >= GAME_CONTROLS.length) + return kind.buttonName; + return kind.faceButton ? kind.buttonName + " / " + GAME_CONTROLS[control] : GAME_CONTROLS[control]; + } + + private String controlName(int type) { + return type == TouchLayout.LEFT ? "Move stick" : label(type); + } + + // ---------- the layout, saved and in files + private boolean saveLayout() { - boolean saved = preferences.edit().putString("configuration", exportLayout()) - .putFloat("look-sensitivity", sensitivity).putBoolean("swipe-layout", true).commit(); - if (!saved) Toast.makeText(getContext(), "Could not save layout. Try again.", Toast.LENGTH_LONG).show(); + boolean saved = preferences.edit().putString(KEY_CONFIGURATION, exportLayout()).commit(); + if (!saved) + Toast.makeText(getContext(), "The touch layout could not be saved. Try again.", Toast.LENGTH_LONG).show(); return saved; } - public String exportLayout() { return layout.exportConfiguration(sensitivity); } + public String exportLayout() { + return layout.exportConfiguration(sensitivity); + } + /** replaces the layout with a layout file's (saved first: it throws if the file is not valid) */ public void importLayout(String text) { - // Parse and save the entire configuration before replacing the live controls. TouchLayout.Configuration imported = TouchLayout.importConfiguration(text); imported.layout.bounds(logicalWidth, logicalHeight); + imported.layout.setMinimumRadius(minimumRadius()); String normalized = imported.layout.exportConfiguration(imported.sensitivity); - if (!preferences.edit().putString("configuration", normalized) - .putFloat("look-sensitivity", imported.sensitivity).putBoolean("swipe-layout", true).commit()) - throw new IllegalArgumentException("Could not save imported layout"); - reset(); layout = imported.layout; sensitivity = imported.sensitivity; - visible = true; updateSensors(); cancelRumble(); invalidate(); + if (!preferences.edit().putString(KEY_CONFIGURATION, normalized).commit()) + throw new IllegalArgumentException("Could not save the imported layout"); + reset(); + layout = imported.layout; + sensitivity = imported.sensitivity; + setHidden(false); + updateSensors(); + cancelRumble(); + invalidate(); } - private String controlName(int type) { return type == LEFT ? "Move stick" : buttons[type].label; } + // ---------- the options - public void startDeviceInput() { - if (deviceInputActive) return; - deviceInputActive = true; - if (inputManager != null) inputManager.registerInputDeviceListener(this, null); - refreshInputDevices(); - gyroAim.reset(); updateSensors(); post(poll); + private void openDialog(AlertDialog dialog) { + dialog.setOnDismissListener(d -> { + optionsOpen = false; + reset(); + updateSensors(); + }); + optionsOpen = true; + reset(); + dialog.show(); } - public void stopDeviceInput() { - deviceInputActive = false; removeCallbacks(poll); - if (inputManager != null) inputManager.unregisterInputDeviceListener(this); - updateSensors(); cancelRumble(); reset(); + private void showOptions() { + String[] items = {"General", "Edit buttons layout", "Look sensitivity"}; + AlertDialog dialog = new AlertDialog.Builder(getContext()) + .setTitle("Options") + .setItems(items, (d, which) -> { + if (which == 0) { + post(this::showGeneral); + } else if (which == 1) { + editing = true; + setHidden(false); + } else { + post(this::showSensitivity); + } + }) + .setNegativeButton("Close", null) + .create(); + openDialog(dialog); } + private TextView text(String value) { + TextView view = new TextView(getContext()); + view.setText(value); + return view; + } + + private Button button(String label, View.OnClickListener listener) { + Button button = new Button(getContext()); + button.setText(label); + button.setOnClickListener(listener); + return button; + } + + private void showGeneral() { + cancelRumble(); + LinearLayout panel = new LinearLayout(getContext()); + panel.setOrientation(LinearLayout.VERTICAL); + panel.setPadding(24, 16, 24, 16); + + boolean hasRumble = vibrator != null && vibrator.hasVibrator(); + Switch rumble = new Switch(getContext()); + rumble.setText(hasRumble ? "Vibration" : "Vibration (not on this phone)"); + rumble.setChecked(layout.rumbleEnabled); + rumble.setEnabled(hasRumble); + rumble.setOnCheckedChangeListener((view, enabled) -> { + layout.rumbleEnabled = enabled; + cancelRumble(); + saveLayout(); + }); + panel.addView(rumble); + + Switch gyro = new Switch(getContext()); + gyro.setText(gyroscope != null ? "Gyroscope aiming (experimental)" : "Gyroscope aiming (not on this phone)"); + gyro.setChecked(layout.gyroscopeEnabled); + gyro.setEnabled(gyroscope != null); + gyro.setOnCheckedChangeListener((view, enabled) -> { + layout.gyroscopeEnabled = enabled; + gyroAim.reset(); + saveLayout(); + }); + panel.addView(gyro); + + Switch floating = new Switch(getContext()); + floating.setText("Floating move stick: it goes where your thumb lands in the lower left"); + floating.setChecked(layout.floatingStick); + floating.setOnCheckedChangeListener((view, enabled) -> { + layout.floatingStick = enabled; + saveLayout(); + }); + panel.addView(floating); + + TextView opacityLabel = text(""); + SeekBar opacity = new SeekBar(getContext()); + int steps = Math.round((TouchLayout.MAX_OPACITY - TouchLayout.MIN_OPACITY) * 20); + opacity.setMax(steps); + opacity.setProgress(Math.round((layout.opacity - TouchLayout.MIN_OPACITY) * 20)); + Runnable showOpacity = () -> opacityLabel.setText(String.format(Locale.US, "Opacity: %d%%", + Math.round(layout.opacity * 100))); + showOpacity.run(); + opacity.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar bar, int progress, boolean user) { + layout.setOpacity(Math.min(TouchLayout.MAX_OPACITY, TouchLayout.MIN_OPACITY + progress / 20f)); + showOpacity.run(); + invalidate(); + } + + @Override + public void onStartTrackingTouch(SeekBar bar) { + } + + @Override + public void onStopTrackingTouch(SeekBar bar) { + saveLayout(); + } + }); + panel.addView(opacityLabel); + panel.addView(opacity); + + AlertDialog dialog = new AlertDialog.Builder(getContext()) + .setTitle("General") + .setView(panel) + .setPositiveButton("Done", null) + .create(); + panel.addView(button("Hide or add buttons", view -> { + dialog.dismiss(); + post(this::showButtonManager); + })); + panel.addView(button("Edit buttons size", view -> { + dialog.dismiss(); + post(this::showButtonSizes); + })); + openDialog(dialog); + } + + private String rowName(int control, boolean withHidden) { + String name = controlName(layout.type(control)); + if (control >= TouchLayout.BASE_COUNT) + name += " (copy)"; + if (withHidden && !layout.shown(control)) + name += " (hidden)"; + return name; + } + + private LinearLayout row(int control, boolean withHidden) { + LinearLayout row = new LinearLayout(getContext()); + row.setPadding(16, 4, 16, 4); + TextView name = text(rowName(control, withHidden)); + name.setTextColor(Color.WHITE); + row.addView(name, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1)); + return row; + } + + private void showButtonSizes() { + LinearLayout list = new LinearLayout(getContext()); + list.setOrientation(LinearLayout.VERTICAL); + for (int i = 0; i < layout.size(); i++) { + final int control = i; + LinearLayout row = row(control, true); + TextView value = text(""); + value.setTextColor(Color.WHITE); + Button smaller = new Button(getContext()); + Button larger = new Button(getContext()); + Runnable refresh = () -> { + int percent = Math.round(layout.sizeScale(control) * 100); + value.setText(percent + "%"); + smaller.setEnabled(percent > 50); + larger.setEnabled(percent < 200); + }; + smaller.setText("-"); + smaller.setOnClickListener(view -> resize(control, -10, refresh)); + larger.setText("+"); + larger.setOnClickListener(view -> resize(control, 10, refresh)); + row.addView(smaller); + row.addView(value); + row.addView(larger); + list.addView(row); + refresh.run(); + } + ScrollView scroll = new ScrollView(getContext()); + scroll.addView(list); + openDialog(new AlertDialog.Builder(getContext()) + .setTitle("Edit buttons size") + .setView(scroll) + .setPositiveButton("Done", null) + .create()); + } + + private void resize(int control, int percent, Runnable refresh) { + int size = Math.round(layout.sizeScale(control) * 100) + percent; + layout.setSize(control, Math.max(50, Math.min(200, size)) / 100f); + saveLayout(); + refresh.run(); + invalidate(); + } + + private void showButtonManager() { + LinearLayout list = new LinearLayout(getContext()); + list.setOrientation(LinearLayout.VERTICAL); + Runnable[] refresh = new Runnable[1]; + refresh[0] = () -> { + list.removeAllViews(); + for (int i = 0; i < layout.size(); i++) { + final int control = i; + LinearLayout row = row(control, false); + row.addView(button(layout.shown(control) ? "Hide" : "Show", view -> { + reset(); + layout.setShown(control, !layout.shown(control)); + saveLayout(); + refresh[0].run(); + invalidate(); + })); + if (layout.type(control) != TouchLayout.LEFT) { + row.addView(button("Duplicate", view -> { + reset(); + if (layout.duplicate(control) < 0) + toast("At most " + TouchLayout.MAX_CONTROLS + " controls. Reset removes the copies."); + else + saveLayout(); + refresh[0].run(); + invalidate(); + })); + } + list.addView(row); + } + }; + refresh[0].run(); + ScrollView scroll = new ScrollView(getContext()); + scroll.addView(list); + AlertDialog dialog = new AlertDialog.Builder(getContext()) + .setTitle("Hide or add buttons") + .setView(scroll) + .setPositiveButton("Done", null) + .setNegativeButton("Reset", null) + .setNeutralButton("Add button", null) + .create(); + openDialog(dialog); + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(view -> confirmReset(refresh[0])); + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(view -> chooseButtonToAdd(refresh[0])); + } + + private void confirmReset(Runnable refresh) { + new AlertDialog.Builder(getContext()) + .setTitle("Reset the buttons?") + .setMessage("This puts every button back in its place and size, shows them all and removes the copies.") + .setNegativeButton("Cancel", null) + .setPositiveButton("Reset", (d, which) -> { + reset(); + layout.resetDefaults(); + setHidden(false); + saveLayout(); + refresh.run(); + invalidate(); + }) + .show(); + } + + private void chooseButtonToAdd(Runnable refresh) { + String[] names = new String[TouchLayout.BASE_COUNT]; + for (int i = 0; i < names.length; i++) + names[i] = controlName(i); + new AlertDialog.Builder(getContext()) + .setTitle("Add button") + .setItems(names, (d, type) -> { + reset(); + if (layout.add(type) < 0) + toast(type == TouchLayout.LEFT ? "The move stick already shows." : "At most " + TouchLayout.MAX_CONTROLS + " controls."); + else + saveLayout(); + refresh.run(); + invalidate(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void showSensitivity() { + LinearLayout panel = new LinearLayout(getContext()); + panel.setOrientation(LinearLayout.VERTICAL); + panel.setPadding(32, 16, 32, 16); + TextView value = text(""); + SeekBar slider = new SeekBar(getContext()); + int steps = Math.round((TouchLayout.MAX_SENSITIVITY - TouchLayout.MIN_SENSITIVITY) / 0.025f); + slider.setMax(steps); + slider.setProgress(Math.round((sensitivity - TouchLayout.MIN_SENSITIVITY) / 0.025f)); + Runnable showValue = () -> value.setText(String.format(Locale.US, "Look sensitivity: %.2fx", sensitivity)); + showValue.run(); + slider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar bar, int progress, boolean user) { + sensitivity = Math.min(TouchLayout.MAX_SENSITIVITY, TouchLayout.MIN_SENSITIVITY + progress * 0.025f); + showValue.run(); + } + + @Override + public void onStartTrackingTouch(SeekBar bar) { + } + + @Override + public void onStopTrackingTouch(SeekBar bar) { + saveLayout(); + } + }); + panel.addView(value); + panel.addView(slider); + openDialog(new AlertDialog.Builder(getContext()) + .setTitle("Look sensitivity") + .setView(panel) + .setPositiveButton("Done", null) + .create()); + } + + private void toast(String message) { + Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show(); + } + + // ---------- the gyroscope and the vibration + private void updateSensors() { boolean needed = deviceInputActive && shown() && layout.gyroscopeEnabled && gyroscope != null; if (needed && !gyroRegistered) { gyroAim.reset(); gyroRegistered = sensors.registerListener(this, gyroscope, SensorManager.SENSOR_DELAY_GAME); } else if (!needed && gyroRegistered) { - sensors.unregisterListener(this); gyroRegistered = false; gyroAim.reset(); + sensors.unregisterListener(this); + gyroRegistered = false; + gyroAim.reset(); + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (!deviceInputActive || !shown() || hidden || !layout.gyroscopeEnabled || editing || optionsOpen) { + gyroAim.reset(); + return; + } + int rotation = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay().getRotation(); + if (gyroAim.sample(event.timestamp, event.values[0], event.values[1], rotation, gyroDelta)) { + // as a swipe of this many logical units + nativeLook(-gyroDelta[0] / LOOK_RADIANS_PER_UNIT * sensitivity, + -gyroDelta[1] / LOOK_RADIANS_PER_UNIT * sensitivity); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + /** the phone vibrates as port 0's controller would, while the controls show */ + private void updateRumble() { + if (vibrator == null || !vibrator.hasVibrator()) + return; + boolean active = shown() && !hidden && layout.rumbleEnabled && !editing && !optionsOpen; + int amplitude = active ? nativeRumble() : 0; + if (amplitude == 0) { + cancelRumble(); + return; + } + long now = SystemClock.uptimeMillis(); + if (amplitude == lastAmplitude && now - lastVibration < VIBRATION_RENEW_MS) + return; + try { + int strength = vibrator.hasAmplitudeControl() ? amplitude : VibrationEffect.DEFAULT_AMPLITUDE; + vibrator.vibrate(VibrationEffect.createOneShot(VIBRATION_MS, strength), rumbleAttributes); + lastAmplitude = amplitude; + lastVibration = now; + } catch (RuntimeException e) { + cancelRumble(); } } private void cancelRumble() { if (lastAmplitude != 0 && vibrator != null) { - try { vibrator.cancel(); } catch (RuntimeException ignored) {} + try { + vibrator.cancel(); + } catch (RuntimeException ignored) { + // the vibrator may already be gone with the activity + } } lastAmplitude = 0; } - @Override public void onSensorChanged(SensorEvent event) { - if (!deviceInputActive || !shown() || !layout.gyroscopeEnabled || editing || optionsOpen) { - gyroAim.reset(); return; - } - int rotation = ((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay().getRotation(); - if (gyroAim.sample(event.timestamp, event.values[0], event.values[1], rotation, gyroDelta)) - // The existing direct-look path accepts logical pixels (0.0022 radians per pixel). - nativeLook(-gyroDelta[0]/0.0022f*sensitivity, -gyroDelta[1]/0.0022f*sensitivity); + // ---------- drawing + + private static int alpha(int color, float opacity) { + int alpha = Math.round(Color.alpha(color) * opacity); + return (color & 0x00ffffff) | (Math.max(0, Math.min(255, alpha)) << 24); } - @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {} - - private void showGeneral() { - optionsOpen = true; reset(); cancelRumble(); - LinearLayout panel = new LinearLayout(getContext()); panel.setOrientation(LinearLayout.VERTICAL); - panel.setPadding(24, 16, 24, 16); - Switch rumble = new Switch(getContext()); - boolean hasRumble = vibrator != null && vibrator.hasVibrator(); - rumble.setText(hasRumble ? "Rumble" : "Rumble (unavailable on this phone)"); - rumble.setChecked(layout.rumbleEnabled); rumble.setEnabled(hasRumble); - rumble.setOnCheckedChangeListener((button, enabled) -> { - layout.rumbleEnabled = enabled; cancelRumble(); saveLayout(); - }); - Switch gyro = new Switch(getContext()); - gyro.setText("Gyroscope aim (Experimental)"+(gyroscope == null ? " - unavailable on this phone" : "")); - gyro.setChecked(layout.gyroscopeEnabled); gyro.setEnabled(gyroscope != null); - gyro.setOnCheckedChangeListener((button, enabled) -> { - layout.gyroscopeEnabled = enabled; reset(); updateSensors(); saveLayout(); - }); - panel.addView(rumble); panel.addView(gyro); - AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("General") - .setView(panel).setPositiveButton("Done", null).create(); - android.widget.Button manage = new android.widget.Button(getContext()); - manage.setText("Hide or add buttons"); - manage.setOnClickListener(v -> { dialog.dismiss(); post(this::showButtonManager); }); - panel.addView(manage); - android.widget.Button sizes = new android.widget.Button(getContext()); - sizes.setText("Edit buttons size"); - sizes.setOnClickListener(v -> { dialog.dismiss(); post(this::showButtonSizes); }); - panel.addView(sizes); - dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); updateSensors(); }); dialog.show(); - } - - private void showButtonSizes() { - optionsOpen = true; reset(); - LinearLayout list = new LinearLayout(getContext()); list.setOrientation(LinearLayout.VERTICAL); - for (int i = 0; i < layout.size(); i++) { - final int control = i; - LinearLayout row = new LinearLayout(getContext()); row.setPadding(16, 4, 16, 4); - TextView label = new TextView(getContext()); label.setTextColor(Color.WHITE); - label.setText(controlName(layout.type(i))+(i >= TouchLayout.BASE_COUNT ? " (copy)" : "") - +(!layout.shown(i) ? " (hidden)" : "")); - row.addView(label, new LinearLayout.LayoutParams(0, -2, 1)); - android.widget.Button minus = new android.widget.Button(getContext()); minus.setText("-"); - TextView value = new TextView(getContext()); value.setTextColor(Color.WHITE); - android.widget.Button plus = new android.widget.Button(getContext()); plus.setText("+"); - Runnable refresh = () -> { - int percent = Math.round(layout.sizeScale(control)*100); - value.setText(percent+"%"); minus.setEnabled(percent > 50); plus.setEnabled(percent < 200); - }; - minus.setOnClickListener(v -> { - layout.setSize(control, Math.max(50, Math.round(layout.sizeScale(control)*100)-10)/100f); - saveLayout(); refresh.run(); invalidate(); - }); - plus.setOnClickListener(v -> { - layout.setSize(control, Math.min(200, Math.round(layout.sizeScale(control)*100)+10)/100f); - saveLayout(); refresh.run(); invalidate(); - }); - row.addView(minus); row.addView(value); row.addView(plus); - list.addView(row); refresh.run(); - } - ScrollView scroll = new ScrollView(getContext()); scroll.addView(list); - AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("Edit buttons size") - .setView(scroll).setPositiveButton("Done", null).create(); - dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); }); dialog.show(); - } - - private void showButtonManager() { - optionsOpen = true; reset(); - LinearLayout list = new LinearLayout(getContext()); list.setOrientation(LinearLayout.VERTICAL); - Runnable[] refresh = new Runnable[1]; - refresh[0] = () -> { - list.removeAllViews(); - for (int i = 0; i < layout.size(); i++) { - final int control = i; - LinearLayout row = new LinearLayout(getContext()); row.setPadding(16, 4, 16, 4); - TextView label = new TextView(getContext()); label.setTextColor(Color.WHITE); - label.setText(controlName(layout.type(i))+(i >= TouchLayout.BASE_COUNT ? " (copy)" : "")); - row.addView(label, new LinearLayout.LayoutParams(0, -2, 1)); - android.widget.Button toggle = new android.widget.Button(getContext()); - toggle.setText(layout.shown(i) ? "Hide" : "Show"); - toggle.setOnClickListener(v -> { - reset(); layout.setShown(control, !layout.shown(control)); - saveLayout(); refresh[0].run(); invalidate(); - }); - row.addView(toggle); - if (layout.type(i) != LEFT) { - android.widget.Button copy = new android.widget.Button(getContext()); copy.setText("Duplicate"); - copy.setOnClickListener(v -> { - reset(); - if (layout.duplicate(control) < 0) - Toast.makeText(getContext(), "Maximum 64 controls. Reset to remove copies.", Toast.LENGTH_LONG).show(); - else saveLayout(); - refresh[0].run(); invalidate(); - }); - row.addView(copy); - } - list.addView(row); - } - }; - refresh[0].run(); - ScrollView scroll = new ScrollView(getContext()); scroll.addView(list); - AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("Hide or add buttons") - .setView(scroll).setPositiveButton("Done", null).setNegativeButton("Reset", null) - .setNeutralButton("Add button", null).create(); - dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); }); - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v -> { - new AlertDialog.Builder(getContext()).setTitle("Reset buttons?") - .setMessage("Restore original positions, show all default controls and remove duplicates?") - .setNegativeButton("Cancel", null).setPositiveButton("Reset", (d, which) -> { - reset(); layout.resetDefaults(); visible = true; saveLayout(); - refresh[0].run(); invalidate(); - }).show(); - }); - dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { - String[] names = new String[TouchLayout.BASE_COUNT]; - for (int i = 0; i < names.length; i++) names[i] = controlName(i); - new AlertDialog.Builder(getContext()).setTitle("Add button").setItems(names, (d, type) -> { - reset(); - if (layout.add(type) < 0) - Toast.makeText(getContext(), type == LEFT ? "Move stick is already visible." : "Maximum 64 controls.", Toast.LENGTH_LONG).show(); - else saveLayout(); - refresh[0].run(); invalidate(); - }).setNegativeButton("Cancel", null).show(); - }); - } - - private void showSensitivity() { - optionsOpen = true; reset(); - LinearLayout panel = new LinearLayout(getContext()); panel.setOrientation(LinearLayout.VERTICAL); - panel.setPadding(32, 16, 32, 16); - TextView value = new TextView(getContext()); - SeekBar slider = new SeekBar(getContext()); slider.setMax(150); - slider.setProgress(Math.round((sensitivity-0.25f)/0.025f)); - value.setText(String.format(java.util.Locale.US, "Look sensitivity: %.2fx", sensitivity)); - slider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - public void onProgressChanged(SeekBar bar, int progress, boolean user) { - sensitivity = 0.25f+progress*0.025f; - value.setText(String.format(java.util.Locale.US, "Look sensitivity: %.2fx", sensitivity)); - saveLayout(); - } - public void onStartTrackingTouch(SeekBar bar) {} - public void onStopTrackingTouch(SeekBar bar) {} - }); - panel.addView(value); panel.addView(slider); - AlertDialog dialog = new AlertDialog.Builder(getContext()).setTitle("Look sensitivity") - .setView(panel).setPositiveButton("Done", null).create(); - dialog.setOnDismissListener(d -> { optionsOpen = false; reset(); }); dialog.show(); - } - - private void circle(Canvas canvas, float x, float y, float radius, String label, boolean active) { - circle(canvas, x, y, radius, label, active, 11); - } - - private void circle(Canvas canvas, float x, float y, float radius, String label, boolean active, float textSize) { + private void circle(Canvas canvas, float x, float y, float radius, String label, boolean active, + float textSize, float opacity) { paint.setStyle(Paint.Style.FILL); - paint.setColor(active ? 0x9983d9ff : 0x55304050); + paint.setColor(alpha(active ? 0x9983d9ff : 0x55304050, opacity)); canvas.drawCircle(x, y, radius, paint); - paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(2); - paint.setColor(active ? 0xffaee7ff : 0xffffffff); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(2); + paint.setColor(alpha(active ? 0xffaee7ff : 0xffffffff, opacity)); canvas.drawCircle(x, y, radius, paint); - paint.setStyle(Paint.Style.FILL); paint.setColor(Color.WHITE); - paint.setTextAlign(Paint.Align.CENTER); paint.setTextSize(textSize); - canvas.drawText(label, x, y + textSize*0.36f, paint); + paint.setStyle(Paint.Style.FILL); + paint.setColor(alpha(Color.WHITE, Math.max(opacity, 0.5f))); + paint.setTextAlign(Paint.Align.CENTER); + paint.setTextSize(textSize); + // (a long name, "Y / Weapon", is made smaller to fit inside the circle) + float width = paint.measureText(label); + if (width > radius * LABEL_WIDTH) { + textSize *= radius * LABEL_WIDTH / width; + paint.setTextSize(textSize); + } + canvas.drawText(label, x, y + textSize * 0.36f, paint); } - private void stick(Canvas canvas, int axis, float x, float y, String label) { - circle(canvas, x, y, layout.radius(LEFT), label, false, 11*layout.sizeScale(LEFT)); - circle(canvas, x + axes[axis]/32767f*layout.radius(LEFT), y + axes[axis+1]/32767f*layout.radius(LEFT), - 24*layout.sizeScale(LEFT), "", held(LEFT)); + /** a toolbar item: never fainter than half, so that it can always be found */ + private void toolbarItem(Canvas canvas, float x, float radius, String label) { + circle(canvas, x, toolbarY(), toolbarRadius(radius), label, false, 11, Math.max(layout.opacity, 0.5f)); } - @Override protected void onDraw(Canvas canvas) { - if (!shown() && !editing) return; - canvas.save(); canvas.translate(offsetX, offsetY); canvas.scale(scale, scale); - if (!editing) circle(canvas, logicalWidth/2, toolbarY(), 28, visible ? "Hide" : "Touch", false); - if (visible) { - if (layout.shown(LEFT)) stick(canvas, 0, layout.x(LEFT), layout.y(LEFT), "Move"); + private void drawToolbar(Canvas canvas) { + if (editing) { + toolbarItem(canvas, optionsX(), OPTIONS_RADIUS, "Save"); + toolbarItem(canvas, optionsX() - 156, OPTIONS_RADIUS, "Export"); + toolbarItem(canvas, optionsX() - 78, OPTIONS_RADIUS, "Import"); + paint.setColor(Color.WHITE); + paint.setTextSize(14); + canvas.drawText("Drag the controls, then push Save", logicalWidth / 2, toolbarY() + 58, paint); + } else { + toolbarItem(canvas, logicalWidth / 2, TOOLBAR_RADIUS, hidden ? "Touch" : "Hide"); + toolbarItem(canvas, optionsX(), OPTIONS_RADIUS, "Options"); + } + } + + private void drawStick(Canvas canvas) { + int stick = TouchLayout.LEFT; + float radius = layout.radius(stick); + float size = layout.sizeScale(stick); + float x = stickCenterX(); + float y = stickCenterY(); + circle(canvas, x, y, radius, "Move", false, 11 * size, layout.opacity); + circle(canvas, x + axes[0] / 32767f * radius, y + axes[1] / 32767f * radius, + Math.max(STICK_KNOB_RADIUS * size, radius * 0.375f), "", held(stick), 11, layout.opacity); + } + + @Override + protected void onDraw(Canvas canvas) { + if (!shown() && !editing) + return; + canvas.save(); + canvas.scale(scale, scale); + if (!hidden || editing) { + if (layout.shown(TouchLayout.LEFT)) + drawStick(canvas); for (int i = 0; i < layout.size(); i++) { - if (!layout.shown(i) || layout.type(i) == LEFT) continue; - Button b = buttons[layout.type(i)]; - circle(canvas, layout.x(i), layout.y(i), layout.radius(i), b.label, held(i) || dragControl == i, 11*layout.sizeScale(i)); + if (!layout.shown(i) || layout.type(i) == TouchLayout.LEFT) + continue; + float textSize = 11 * Math.max(layout.sizeScale(i), layout.radius(i) / 36f); + circle(canvas, layout.x(i), layout.y(i), layout.radius(i), label(layout.type(i)), + held(i) || dragControl == i, Math.min(textSize, 16), layout.opacity); } } - editorButton(canvas); + drawToolbar(canvas); canvas.restore(); } } diff --git a/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java b/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java index 7722a3a0..79d6e4fc 100644 --- a/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java +++ b/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java @@ -6,31 +6,86 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.Properties; -/** Display-relative positions, visibility and action types for touch controls. */ +/** + * The touch controls' layout (TouchControls): each control's kind, place, + * size and whether it shows, with the settings that go with them, in the + * overlay's logical units. Places are kept for the display the layout is + * on and saved on a 960 x 540 grid, so a layout moves between displays of + * other shapes. A layout file is Java properties text (version 3; 1 and 2 + * are still read). + */ final class TouchLayout { - static final int LEFT = 16, FIRE_LEFT = 17; + /** the kinds of control: a button of TouchControls's list, by index */ + static final int LEFT = 16; + static final int FIRE_LEFT = 17; + static final int BASE_COUNT = 18; + static final int MAX_CONTROLS = 64; + static final float MIN_SIZE = 0.5f; + static final float MAX_SIZE = 2f; + static final float MIN_OPACITY = 0.2f; + static final float MAX_OPACITY = 1f; + static final float MIN_SENSITIVITY = 0.25f; + static final float MAX_SENSITIVITY = 4f; + + /** the saved grid */ + private static final float GRID_WIDTH = 960; + private static final float GRID_HEIGHT = 540; + private static final int VERSION = 3; + private static final int MAX_FILE_LENGTH = 65536; + + /** each kind's default place on the grid, and its radius */ private static final float[][] DEFAULTS = { - {856,447}, {913,377}, {794,377}, {850,312}, {915,239}, {802,239}, - {236,449}, {691,449}, {360,490}, {438,490}, {570,36}, {390,36}, - {100,237}, {100,335}, {51,286}, {149,286}, {115,440}, {255,239} + {856, 447}, {913, 377}, {794, 377}, {850, 312}, {915, 239}, {802, 239}, + {236, 449}, {691, 449}, {360, 490}, {438, 490}, {570, 36}, {390, 36}, + {100, 237}, {100, 335}, {51, 286}, {149, 286}, {115, 440}, {255, 239} }; private static final float[] RADII = { - 36,32,34,32,39,35,32,32,27,29,28,28,25,25,25,25,64,39 + 36, 32, 34, 32, 39, 35, 32, 32, 27, 29, 28, 28, 25, 25, 25, 25, 64, 39 }; - static final int BASE_COUNT = 18, MAX_CONTROLS = 64; + private static final class Control { final int type; - float x, y; + float x; + float y; boolean shown = true; float size = 1f; - Control(int type, float x, float y) { this.type = type; this.x = x; this.y = y; } + + Control(int type, float x, float y) { + this.type = type; + this.x = x; + this.y = y; + } } + + /** a layout and the sensitivity saved with it */ + static final class Configuration { + final TouchLayout layout; + final float sensitivity; + + Configuration(TouchLayout layout, float sensitivity) { + this.layout = layout; + this.sensitivity = sensitivity; + } + } + private final ArrayList controls = new ArrayList<>(); + private float width = GRID_WIDTH; + private float height = GRID_HEIGHT; + /** the smallest radius a control is drawn and touched at, in logical units */ + private float minimumRadius; - private float width = 960, height = 540; - boolean rumbleEnabled = true, gyroscopeEnabled = false; - static final float MIN_SIZE = 0.5f, MAX_SIZE = 2f; + boolean rumbleEnabled = true; + boolean gyroscopeEnabled = false; + /** how solid the controls are drawn, MIN_OPACITY..MAX_OPACITY */ + float opacity = 1f; + /** the move stick goes where the thumb lands in the lower left, not at its place */ + boolean floatingStick = false; + TouchLayout() { + resetDefaults(); + } + + /** places the layout on a display of this size, in logical units */ void bounds(float width, float height) { if (!Float.isFinite(width) || !Float.isFinite(height) || width <= 0 || height <= 0) throw new IllegalArgumentException("Invalid display dimensions"); @@ -38,152 +93,237 @@ final class TouchLayout { control.x *= width / this.width; control.y *= height / this.height; } - this.width = width; this.height = height; - for (int i = 0; i < size(); i++) move(i, x(i), y(i)); + this.width = width; + this.height = height; + keepAllInside(); + } + + /** + * the smallest radius, in logical units, at which a control is drawn and + * touched (a finger's width, whatever the display's density) + */ + void setMinimumRadius(float radius) { + minimumRadius = Float.isFinite(radius) && radius > 0 ? radius : 0; + keepAllInside(); + } + + int size() { + return controls.size(); + } + + int type(int control) { + return controls.get(control).type; + } + + float x(int control) { + return controls.get(control).x; + } + + float y(int control) { + return controls.get(control).y; + } + + float savedX(int control) { + return x(control) * GRID_WIDTH / width; + } + + float savedY(int control) { + return y(control) * GRID_HEIGHT / height; + } + + float radius(int control) { + return Math.max(RADII[type(control)] * sizeScale(control), minimumRadius); + } + + float sizeScale(int control) { + return controls.get(control).size; } - float savedX(int i) { return x(i) * 960 / width; } - float savedY(int i) { return y(i) * 540 / height; } - TouchLayout() { resetDefaults(); } - int size() { return controls.size(); } - int type(int control) { return controls.get(control).type; } - float x(int control) { return controls.get(control).x; } - float y(int control) { return controls.get(control).y; } - float radius(int control) { return RADII[type(control)] * controls.get(control).size; } - float sizeScale(int control) { return controls.get(control).size; } void setSize(int control, float size) { if (!Float.isFinite(size) || size < MIN_SIZE || size > MAX_SIZE) throw new IllegalArgumentException("Button size must be between 50% and 200%"); controls.get(control).size = size; move(control, x(control), y(control)); } - boolean shown(int control) { return controls.get(control).shown; } - void setShown(int control, boolean shown) { controls.get(control).shown = shown; } + boolean shown(int control) { + return controls.get(control).shown; + } + + void setShown(int control, boolean shown) { + controls.get(control).shown = shown; + } + + void setOpacity(float opacity) { + if (!validOpacity(opacity)) + throw new IllegalArgumentException("Opacity must be between 20% and 100%"); + this.opacity = opacity; + } + + /** the default controls, at their default places, sizes and visibility */ void resetDefaults() { controls.clear(); for (int i = 0; i < DEFAULTS.length; i++) - controls.add(new Control(i, DEFAULTS[i][0]*width/960, DEFAULTS[i][1]*height/540)); - for (int i = 0; i < size(); i++) move(i, x(i), y(i)); + controls.add(new Control(i, DEFAULTS[i][0] * width / GRID_WIDTH, DEFAULTS[i][1] * height / GRID_HEIGHT)); + keepAllInside(); } + /** adds a copy of a control beside it; -1 if there may be no more (or it is the stick) */ int duplicate(int source) { - if (size() >= MAX_CONTROLS || type(source) == LEFT) return -1; - Control added = new Control(type(source), x(source)+radius(source)*2+12, y(source)); + if (size() >= MAX_CONTROLS || type(source) == LEFT) + return -1; + float offset = radius(source) * 2 + 12; + Control added = new Control(type(source), x(source) + offset, y(source)); added.size = sizeScale(source); controls.add(added); - int index = size()-1; - // If there is no space on the right, place the copy to the left instead. - if (added.x > width-radius(source)) added.x = x(source)-radius(source)*2-12; + int index = size() - 1; + // where there is no room to the right, the copy goes to the left + if (added.x > width - radius(index)) + added.x = x(source) - offset; move(index, added.x, added.y); return index; } + /** shows a hidden control of this kind, or else adds a copy; -1 if neither can be */ int add(int type) { - if (type < 0 || type >= BASE_COUNT) return -1; + if (type < 0 || type >= BASE_COUNT) + return -1; for (int i = 0; i < size(); i++) { - if (type(i) == type && !shown(i)) { setShown(i, true); return i; } + if (type(i) == type && !shown(i)) { + setShown(i, true); + return i; + } } return duplicate(type); } + /** moves a control, keeping all of it on the display */ + void move(int control, float x, float y) { + if (!Float.isFinite(x) || !Float.isFinite(y)) + return; + float radius = radius(control); + controls.get(control).x = Math.max(radius, Math.min(width - radius, x)); + controls.get(control).y = Math.max(radius, Math.min(height - radius, y)); + } + + /** puts a control at a saved place on the grid, if it is a valid one */ + void restore(int control, float x, float y) { + if (Float.isFinite(x) && Float.isFinite(y) && x >= 0 && x <= GRID_WIDTH && y >= 0 && y <= GRID_HEIGHT) { + controls.get(control).x = x; + controls.get(control).y = y; + } + } + + private void keepAllInside() { + for (int i = 0; i < size(); i++) + move(i, x(i), y(i)); + } + + /** the layout as a layout file's text, with the look sensitivity */ String exportConfiguration(float sensitivity) { - if (!validSensitivity(sensitivity)) throw new IllegalArgumentException("Invalid sensitivity"); + if (!validSensitivity(sensitivity)) + throw new IllegalArgumentException("Invalid sensitivity"); Properties values = new Properties(); values.setProperty("format", "halo-touch-layout"); - values.setProperty("version", "2"); + values.setProperty("version", Integer.toString(VERSION)); values.setProperty("rumble", Boolean.toString(rumbleEnabled)); values.setProperty("gyroscope", Boolean.toString(gyroscopeEnabled)); + values.setProperty("opacity", Float.toString(opacity)); + values.setProperty("floating-stick", Boolean.toString(floatingStick)); values.setProperty("count", Integer.toString(size())); values.setProperty("sensitivity", Float.toString(sensitivity)); for (int i = 0; i < size(); i++) { - String key = "control."+i+"."; - values.setProperty(key+"type", Integer.toString(type(i))); - values.setProperty(key+"x", Float.toString(savedX(i))); - values.setProperty(key+"y", Float.toString(savedY(i))); - values.setProperty(key+"visible", Boolean.toString(shown(i))); - values.setProperty(key+"size", Float.toString(sizeScale(i))); + String key = "control." + i + "."; + values.setProperty(key + "type", Integer.toString(type(i))); + values.setProperty(key + "x", Float.toString(savedX(i))); + values.setProperty(key + "y", Float.toString(savedY(i))); + values.setProperty(key + "visible", Boolean.toString(shown(i))); + values.setProperty(key + "size", Float.toString(sizeScale(i))); } StringWriter text = new StringWriter(); - try { values.store(text, "Halo Android touch layout"); } - catch (IOException e) { throw new IllegalStateException(e); } + try { + values.store(text, "Halo Android touch layout"); + } catch (IOException e) { + throw new IllegalStateException(e); + } return text.toString(); } - static final class Configuration { - final TouchLayout layout; - final float sensitivity; - Configuration(TouchLayout layout, float sensitivity) { - this.layout = layout; this.sensitivity = sensitivity; - } - } - - private static boolean validSensitivity(float value) { - return Float.isFinite(value) && value >= 0.25f && value <= 4f; - } - + /** reads a layout file's text; throws IllegalArgumentException for anything not valid */ static Configuration importConfiguration(String text) { - if (text == null || text.length() > 65536) throw new IllegalArgumentException("Invalid layout file"); + if (text == null || text.length() > MAX_FILE_LENGTH) + throw new IllegalArgumentException("Invalid layout file"); Properties values = new Properties(); try { values.load(new StringReader(text)); - String version = values.getProperty("version"); - if (!"halo-touch-layout".equals(values.getProperty("format")) || - !("1".equals(version) || "2".equals(version))) - throw new IllegalArgumentException("Unsupported layout file"); + int version = readVersion(values); int count = Integer.parseInt(values.getProperty("count")); float sensitivity = Float.parseFloat(values.getProperty("sensitivity")); if (count < BASE_COUNT || count > MAX_CONTROLS || !validSensitivity(sensitivity)) throw new IllegalArgumentException("Invalid layout settings"); TouchLayout layout = new TouchLayout(); layout.controls.clear(); - if ("2".equals(version)) { + if (version >= 2) { layout.rumbleEnabled = readBoolean(values, "rumble"); layout.gyroscopeEnabled = readBoolean(values, "gyroscope"); } - for (int i = 0; i < count; i++) { - String key = "control."+i+"."; - int type = Integer.parseInt(values.getProperty(key+"type")); - float x = Float.parseFloat(values.getProperty(key+"x")); - float y = Float.parseFloat(values.getProperty(key+"y")); - String shown = values.getProperty(key+"visible"); - float size = "2".equals(version) ? Float.parseFloat(values.getProperty(key+"size")) : 1f; - if (type < 0 || type >= BASE_COUNT || (i < BASE_COUNT && type != i) || - (i >= BASE_COUNT && type == LEFT) || !Float.isFinite(x) || !Float.isFinite(y) || - x < 0 || x > 960 || y < 0 || y > 540 || - !("true".equals(shown) || "false".equals(shown)) || - !Float.isFinite(size) || size < MIN_SIZE || size > MAX_SIZE) - throw new IllegalArgumentException("Invalid control in layout file"); - Control control = new Control(type, x, y); - control.shown = Boolean.parseBoolean(shown); - control.size = size; - layout.controls.add(control); + if (version >= 3) { + float opacity = Float.parseFloat(values.getProperty("opacity")); + if (!validOpacity(opacity)) + throw new IllegalArgumentException("Invalid opacity"); + layout.opacity = opacity; + layout.floatingStick = readBoolean(values, "floating-stick"); } + for (int i = 0; i < count; i++) + layout.controls.add(readControl(values, i, version)); return new Configuration(layout, sensitivity); } catch (IOException | NullPointerException e) { throw new IllegalArgumentException("Incomplete layout file", e); } } + private static int readVersion(Properties values) { + if (!"halo-touch-layout".equals(values.getProperty("format"))) + throw new IllegalArgumentException("Unsupported layout file"); + String version = values.getProperty("version"); + for (int known = 1; known <= VERSION; known++) { + if (Integer.toString(known).equals(version)) + return known; + } + throw new IllegalArgumentException("Unsupported layout file"); + } + + private static Control readControl(Properties values, int index, int version) { + String key = "control." + index + "."; + int type = Integer.parseInt(values.getProperty(key + "type")); + float x = Float.parseFloat(values.getProperty(key + "x")); + float y = Float.parseFloat(values.getProperty(key + "y")); + String shown = values.getProperty(key + "visible"); + float size = version >= 2 ? Float.parseFloat(values.getProperty(key + "size")) : 1f; + boolean validType = type >= 0 && type < BASE_COUNT && (index >= BASE_COUNT ? type != LEFT : type == index); + boolean validPlace = Float.isFinite(x) && Float.isFinite(y) && x >= 0 && x <= GRID_WIDTH + && y >= 0 && y <= GRID_HEIGHT; + boolean validSize = Float.isFinite(size) && size >= MIN_SIZE && size <= MAX_SIZE; + if (!validType || !validPlace || !validSize || !("true".equals(shown) || "false".equals(shown))) + throw new IllegalArgumentException("Invalid control in layout file"); + Control control = new Control(type, x, y); + control.shown = Boolean.parseBoolean(shown); + control.size = size; + return control; + } + private static boolean readBoolean(Properties values, String key) { String value = values.getProperty(key); if (!("true".equals(value) || "false".equals(value))) - throw new IllegalArgumentException("Invalid "+key+" option"); + throw new IllegalArgumentException("Invalid " + key + " option"); return Boolean.parseBoolean(value); } - void move(int control, float x, float y) { - if (!Float.isFinite(x) || !Float.isFinite(y)) return; - float radius = radius(control); - controls.get(control).x = Math.max(radius, Math.min(width-radius, x)); - controls.get(control).y = Math.max(radius, Math.min(height-radius, y)); + static boolean validSensitivity(float value) { + return Float.isFinite(value) && value >= MIN_SENSITIVITY && value <= MAX_SENSITIVITY; } - void restore(int control, float x, float y) { - if (Float.isFinite(x) && Float.isFinite(y) && x >= 0 - && x <= 960 && y >= 0 && y <= 540) { - controls.get(control).x = x; - controls.get(control).y = y; - } + static boolean validOpacity(float value) { + return Float.isFinite(value) && value >= MIN_OPACITY && value <= MAX_OPACITY; } } diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h index a1d3dac8..ec9520c8 100644 --- a/port/android/guest/runtime/guest_host.h +++ b/port/android/guest/runtime/guest_host.h @@ -128,5 +128,8 @@ void host_touch_look_read(float *delta); void host_touch_rumble(unsigned int low, unsigned int high); /* tells the overlay when to show: HALO_TOUCH_SCENE_* (xinput_sdl.c) */ void host_touch_scene(int scene); +/* the game control on each of the 16 controller buttons (touch_game.c), +for the overlay's labels */ +void host_touch_bindings(const int *controls); #endif diff --git a/port/android/host/host_touch.c b/port/android/host/host_touch.c index 8cf73af5..fc87fb7e 100644 --- a/port/android/host/host_touch.c +++ b/port/android/host/host_touch.c @@ -37,6 +37,11 @@ static int64_t look_ns; static int rumble_amplitude; static int64_t rumble_ns; static volatile int touch_scene; +/* the game control on each controller button (-1: none), and a count of +its changes, which the overlay polls */ +#define TOUCH_BUTTONS 16 +static int32_t touch_bindings[TOUCH_BUTTONS]; +static int touch_bindings_serial; static int64_t now_ns(void) { @@ -164,6 +169,34 @@ void host_touch_scene(int scene) touch_scene = scene; } +/* the guest's, when the profile's mapping changes: the game control on +each of the 16 controller buttons (port/linux/game/touch_game.c) */ +void host_touch_bindings(const int32_t *controls) +{ + pthread_mutex_lock(&touch_lock); + memcpy(touch_bindings, controls, sizeof(touch_bindings)); + touch_bindings_serial++; + pthread_mutex_unlock(&touch_lock); +} + +/* the mapping into controls[16]; returns its count of changes (0 before the +game has sent it) */ +JNIEXPORT jint JNICALL Java_com_halo_decomp_TouchControls_nativeBindings(JNIEnv *env, jclass cls, + jintArray controls) +{ + int32_t copy[TOUCH_BUTTONS]; + int serial; + + (void)cls; + pthread_mutex_lock(&touch_lock); + memcpy(copy, touch_bindings, sizeof(copy)); + serial = touch_bindings_serial; + pthread_mutex_unlock(&touch_lock); + if (controls && (*env)->GetArrayLength(env, controls) >= TOUCH_BUTTONS) + (*env)->SetIntArrayRegion(env, controls, 0, TOUCH_BUTTONS, (const jint *)copy); + return serial; +} + /* 0 until the game has read its controller once */ JNIEXPORT jint JNICALL Java_com_halo_decomp_TouchControls_nativeScene(JNIEnv *env, jclass cls) { diff --git a/port/android/host_imports.list b/port/android/host_imports.list index d1674a02..058c8b97 100644 --- a/port/android/host_imports.list +++ b/port/android/host_imports.list @@ -59,5 +59,6 @@ host_touch_read host_touch_look_read host_touch_rumble host_touch_scene +host_touch_bindings host_errno diff --git a/port/linux/game/touch_game.c b/port/linux/game/touch_game.c index 46ea64c9..86c3b265 100644 --- a/port/linux/game/touch_game.c +++ b/port/linux/game/touch_game.c @@ -9,6 +9,8 @@ game. The platform layer cannot see the game's types, so it asks here #include "cseries.h" #include "game/game.h" #include "cutscene/cinematics.h" +#include "input/input.h" +#include "input/input_abstraction.h" /* asks whether a cinematic is playing that A would skip (player_control.c, player_control_action_test_check_reset_input_blob); tests @@ -27,3 +29,18 @@ int touch_game_cinematic_playing(void) { return cinematic_globals && game_in_progress() && cinematic_in_progress(); } + +/* the game control on each of player 1's 16 controller buttons, as the +gamepad's buttons are numbered (input.h), -1 for none: the touch controls +name their buttons by them, so a profile's own mapping shows */ +void touch_game_button_controls(int *controls) +{ + short buttons[NUMBER_OF_GAMEPAD_BUTTONS]; + short index; + + input_abstraction_port_button_controls(0, buttons); + for (index = 0; index < NUMBER_OF_GAMEPAD_BUTTONS; index++) + { + controls[index] = buttons[index]; + } +} diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index 85633698..fa71f7c4 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -23,11 +23,16 @@ void host_touch_read(int *state); void host_touch_look_read(float *delta); void host_touch_rumble(unsigned int low, unsigned int high); void host_touch_scene(int scene); +void host_touch_bindings(const int *controls); #endif /* the game's (port/linux/game/touch_game.c) */ int touch_game_cinematic_skippable(void); int touch_game_cinematic_playing(void); +void touch_game_button_controls(int *controls); + +/* the gamepad's buttons (input.h), which the touch controls are named by */ +#define TOUCH_BUTTONS 16 /* a tap moves at most this far; a drag of this length is one wheel step */ #define TOUCH_TAP_SLOP_DP 12.0f @@ -204,6 +209,9 @@ enum }; /* input.touch_controls as _touch_scene_on, _touch_scene_off or 0 */ +/* the touch controls' stick at port 0's last read, -1..1, y down */ +static float move_x, move_y; + static int touch_controls_setting(void) { static int setting; @@ -238,26 +246,29 @@ void touch_input_controls(XINPUT_GAMEPAD *pad, int menus) { XINPUT_GAMEPAD_A, XINPUT_GAMEPAD_B, XINPUT_GAMEPAD_X, XINPUT_GAMEPAD_Y }; - SHORT *sticks[4]; + static int bindings_sent[TOUCH_BUTTONS]; + static int bindings_known; + int bindings[TOUCH_BUTTONS]; int state[7]; int index; host_touch_scene(_touch_scene_known | touch_controls_setting() | (menus || touch_game_cinematic_playing() ? _touch_scene_menus : 0)); - sticks[0] = &pad->sThumbLX; - sticks[1] = &pad->sThumbLY; - sticks[2] = &pad->sThumbRX; - sticks[3] = &pad->sThumbRY; + /* (the profile's mapping, for the buttons' names; only when it changes) */ + touch_game_button_controls(bindings); + if (!bindings_known || memcmp(bindings, bindings_sent, sizeof(bindings))) + { + memcpy(bindings_sent, bindings, sizeof(bindings)); + bindings_known = TRUE; + host_touch_bindings(bindings); + } host_touch_read(state); + /* the stick moves the player through the game's input state, whatever + the profile's sticks do (touch_input_move), not as the left stick */ + move_x = state[0] / 32767.0f; + move_y = state[1] / 32767.0f; for (index = 0; index < 4; index++) { - /* SDL's y runs down, the Xbox's up */ - int value = index == 1 || index == 3 ? -state[index] - 1 : state[index]; - - if (value < -32768) value = -32768; - if (value > 32767) value = 32767; - if (abs(value) > abs(*sticks[index])) - *sticks[index] = (SHORT)value; if (state[6] & (1 << index)) pad->bAnalogButtons[analog[index]] = 0xff; } @@ -277,6 +288,14 @@ void touch_input_controls(XINPUT_GAMEPAD *pad, int menus) pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = 0xff; } +int touch_input_move(float *forward, float *strafe) +{ + /* (SDL's y runs down; strafe is positive to the left) */ + *forward = -move_y; + *strafe = -move_x; + return move_x != 0.0f || move_y != 0.0f; +} + void touch_input_look(float scale, float *yaw, float *pitch) { float delta[2]; diff --git a/port/linux/src/touch_input.h b/port/linux/src/touch_input.h index 68356139..ef0ed155 100644 --- a/port/linux/src/touch_input.h +++ b/port/linux/src/touch_input.h @@ -41,6 +41,10 @@ an Android view over the game: tells them whether a menu is up and input.touch_controls, and adds their stick and buttons to port 0's state */ void touch_input_controls(XINPUT_GAMEPAD *pad, int menus); +/* the touch controls' stick as the player's movement, -1..1 (forward, and +strafe to the left); nonzero while it is pushed */ +int touch_input_move(float *forward, float *strafe); + /* adds the touch controls' view swipe since the last call to yaw and pitch, in radians at `scale` radians per pixel */ void touch_input_look(float scale, float *yaw, float *pitch); diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index 6f689711..cce48a63 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -154,6 +154,22 @@ int halo_linux_mouse_look(short gamepad_index, float *yaw, float *pitch) return TRUE; } +/* the touch controls' stick as the player's movement (input_abstraction.c's +keyboard_controls_update), forward and strafe -1..1, whatever the +profile's sticks do; nonzero while it is pushed. Nothing but on Android. */ +int halo_linux_touch_move(short controller_index, float *forward, float *strafe) +{ + *forward = 0.0f; + *strafe = 0.0f; + if (controller_index != 0) + return FALSE; +#ifdef HALO_ANDROID + return touch_input_move(forward, strafe); +#else + return FALSE; +#endif +} + /* the touch controls' swipe since the last call, in radians at the mouse's rate per pixel (the view applies its own sensitivity), and whether it gets the stick's aim assist (input.touch_aim_assist): player_control.c makes it diff --git a/source/input/input_abstraction.c b/source/input/input_abstraction.c index 195f52fe..5caa4942 100644 --- a/source/input/input_abstraction.c +++ b/source/input/input_abstraction.c @@ -273,6 +273,19 @@ static void keyboard_controls_update( state->forward_movement = y * length; state->strafe = -x * length; } + { + /* port: the touch controls' stick (port/linux/src/xinput_sdl.c) + moves the player whatever the profile's sticks do, as the keys do */ + extern int halo_linux_touch_move(short controller_index, real *forward, real *strafe); + real forward; + real strafe; + + if (halo_linux_touch_move((short)controller_index, &forward, &strafe)) + { + state->forward_movement = forward; + state->strafe = strafe; + } + } return; } @@ -323,6 +336,34 @@ real input_abstraction_port_primary_trigger( return gamepad->buttons[button_index] ? 1.f : 0.f; } +/* port: the game control on each of the controller's buttons by its +profile's mapping (NONE for none), numbered as the gamepad's buttons are +(input.h): the touch controls name their buttons by it (touch_game.c) */ +void input_abstraction_port_button_controls( + short controller_index, + short *controls) +{ + short button_index; + short control_index; + + for (button_index = 0; button_index < NUMBER_OF_GAMEPAD_BUTTONS; button_index++) + { + controls[button_index] = NONE; + } + if (controller_index < 0 || controller_index >= MAXIMUM_GAMEPADS) + return; + for (control_index = 0; control_index < NUMBER_OF_GAME_CONTROLS; control_index++) + { + byte button_index_bound = input_abstraction_globals.player_control_preferences[controller_index]. + game_control_to_xbox_buttons[control_index]; + + if (button_index_bound < NUMBER_OF_GAMEPAD_BUTTONS && controls[button_index_bound] == NONE) + { + controls[button_index_bound] = control_index; + } + } +} + /* port: whether the controller's player looks inverted, as its right stick does (input_abstraction_update): its profile's invert, or flying an aircraft with its profile's aircraft invert; the touch controls' swipe follows it diff --git a/source/input/input_abstraction.h b/source/input/input_abstraction.h index 4b439a67..e07904f2 100644 --- a/source/input/input_abstraction.h +++ b/source/input/input_abstraction.h @@ -62,7 +62,11 @@ boolean input_abstraction_port_action_only( short controller_index); real input_abstraction_port_primary_trigger( short controller_index); -/* port: the player's look inverted, as its stick is (the touch controls) */ +/* port: the game control on each of the controller's buttons, and the +player's look inverted as its stick is (the touch controls) */ +void input_abstraction_port_button_controls( + short controller_index, + short *controls); boolean input_abstraction_port_look_inverted( short controller_index); diff --git a/tools/tests/TouchLayoutTest.java b/tools/tests/TouchLayoutTest.java index fba17d09..6914035c 100644 --- a/tools/tests/TouchLayoutTest.java +++ b/tools/tests/TouchLayoutTest.java @@ -62,7 +62,7 @@ public final class TouchLayoutTest { check(wide.add(4) == 4 && wide.shown(4), "Add must restore a hidden button first"); wide.setShown(TouchLayout.LEFT, false); check(wide.add(TouchLayout.LEFT) == TouchLayout.LEFT, "Hidden move stick must be restorable"); - reject(exported.replace("version=2", "version=9")); + reject(exported.replace("version=3", "version=9")); reject(exported.replace("control.18.type=4", "control.18.type=16")); reject(exported.replace("control.18.x=240.0", "control.18.x=NaN")); reject(exported.replace("count=19", "count=10000")); @@ -86,11 +86,43 @@ public final class TouchLayoutTest { reject(sizedText.replace("control.18.size=1.6", "control.18.size=3.0")); reject(sizedText.replace("rumble=false", "rumble=invalid")); reject(sizedText.replace("gyroscope=true", "gyroscope=invalid")); - String legacy = sizedText.replace("version=2", "version=1").replaceAll("(?m)^.*\\.size=.*\\R", "") - .replaceAll("(?m)^(rumble|gyroscope)=.*\\R", ""); + String legacy = sizedText.replace("version=3", "version=1").replaceAll("(?m)^.*\\.size=.*\\R", "") + .replaceAll("(?m)^(rumble|gyroscope|opacity|floating-stick)=.*\\R", ""); TouchLayout.Configuration old = TouchLayout.importConfiguration(legacy); check(old.layout.sizeScale(copy) == 1 && old.layout.rumbleEnabled && !old.layout.gyroscopeEnabled, "Legacy layouts must load with default sizes and gyro disabled"); + // Opacity (version 3) round-trips; version 2 files load fully opaque. + sized.layout.setOpacity(0.45f); + String translucent = sized.layout.exportConfiguration(2.25f); + check(TouchLayout.importConfiguration(translucent).layout.opacity == 0.45f, "Opacity must survive export/import"); + reject(translucent.replace("opacity=0.45", "opacity=0.05")); + reject(translucent.replace("opacity=0.45", "opacity=NaN")); + sized.layout.floatingStick = true; + check(TouchLayout.importConfiguration(sized.layout.exportConfiguration(2.25f)).layout.floatingStick, + "The floating stick must survive export/import"); + sized.layout.floatingStick = false; + reject(translucent.replace("floating-stick=false", "floating-stick=maybe")); + String version2 = translucent.replace("version=3", "version=2").replaceAll("(?m)^(opacity|floating-stick)=.*\\R", ""); + check(TouchLayout.importConfiguration(version2).layout.opacity == 1f + && !TouchLayout.importConfiguration(version2).layout.floatingStick, + "Version 2 layouts load fully opaque, with the stick in its place"); + reject(translucent.replaceAll("(?m)^opacity=.*\\R", "")); + try { + sized.layout.setOpacity(1.5f); + throw new AssertionError("Out-of-range opacity was accepted"); + } catch (IllegalArgumentException expected) { + // refused + } + // A finger-sized minimum radius enlarges small buttons and keeps them on the display. + TouchLayout fingers = new TouchLayout(); + fingers.setMinimumRadius(40); + check(fingers.radius(12) == 40 && fingers.radius(TouchLayout.LEFT) == 64, + "The minimum radius enlarges small controls only"); + fingers.move(12, 0, 0); + check(fingers.x(12) == 40 && fingers.y(12) == 40, "Enlarged controls stay inside the display"); + check(fingers.savedX(12) == 40, "The minimum radius does not change saved places"); + fingers.setMinimumRadius(Float.NaN); + check(fingers.radius(12) == 25, "An invalid minimum radius is none"); wide.resetDefaults(); check(wide.size() == 18 && wide.shown(4) && wide.x(4) == 915*1200f/960, "Reset restores defaults on the current display and removes all copies"); From 91a49f898c66ae2ab19b799e7793cfc5a782a978 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:18:32 +1300 Subject: [PATCH 123/187] Touch controls hidden until a game is played They showed for a moment as the game started: until the main menu's widgets were up nothing said a menu was. They now show only while a game is played (touch_game_playing): not while the game starts, between maps, or at the main menu, whose ui.map is a game too. --- port/linux/game/touch_game.c | 10 ++++++++++ port/linux/src/touch_input.c | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/port/linux/game/touch_game.c b/port/linux/game/touch_game.c index 86c3b265..f1c731f9 100644 --- a/port/linux/game/touch_game.c +++ b/port/linux/game/touch_game.c @@ -11,6 +11,7 @@ game. The platform layer cannot see the game's types, so it asks here #include "cutscene/cinematics.h" #include "input/input.h" #include "input/input_abstraction.h" +#include "interface/ui_widget.h" /* asks whether a cinematic is playing that A would skip (player_control.c, player_control_action_test_check_reset_input_blob); tests @@ -30,6 +31,15 @@ int touch_game_cinematic_playing(void) return cinematic_globals && game_in_progress() && cinematic_in_progress(); } +/* asks whether a game is being played: not while the game starts, nor +between maps, nor at the main menu (whose ui.map is a game too), where the +on-screen touch controls do not show (touch_input.c); cinematic_globals +first, as touch_game_cinematic_skippable has it */ +int touch_game_playing(void) +{ + return cinematic_globals && game_in_progress() && !main_menu_is_active(); +} + /* the game control on each of player 1's 16 controller buttons, as the gamepad's buttons are numbered (input.h), -1 for none: the touch controls name their buttons by them, so a profile's own mapping shows */ diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index fa71f7c4..65ff6e23 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -29,6 +29,7 @@ void host_touch_bindings(const int *controls); /* the game's (port/linux/game/touch_game.c) */ int touch_game_cinematic_skippable(void); int touch_game_cinematic_playing(void); +int touch_game_playing(void); void touch_game_button_controls(int *controls); /* the gamepad's buttons (input.h), which the touch controls are named by */ @@ -252,8 +253,10 @@ void touch_input_controls(XINPUT_GAMEPAD *pad, int menus) int state[7]; int index; + /* (the controls show only in a game: not while it starts, at the main + menu, in a menu or during a cinematic) */ host_touch_scene(_touch_scene_known | touch_controls_setting() | - (menus || touch_game_cinematic_playing() ? _touch_scene_menus : 0)); + (menus || !touch_game_playing() || touch_game_cinematic_playing() ? _touch_scene_menus : 0)); /* (the profile's mapping, for the buttons' names; only when it changes) */ touch_game_button_controls(bindings); if (!bindings_known || memcmp(bindings, bindings_sent, sizeof(bindings))) From 6a74eab5ad5ac10a56c1ab82612488ebc48b7120 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:09:19 +1300 Subject: [PATCH 124/187] Touch controls review fixes With input.touch_aim_assist false, walking with the touch stick still let the view follow a target: the touch controls now count as aiming without the assist until a stick aims again (halo_linux_touch_aiming), as the mouse does. The stick's diagonal is full on both axes, as a controller's square stick gives. The gyroscope reaches the game apart from the swipe (nativeGyro), so the profile's invert look turns only the swipe. The overlay shows once the game has been in play for 200 ms, so it does not flash at a level's first frame. The options dialogs are counted, so a dialog opened from another keeps the controls released, and the layout is saved when the last one closes (a slider moved without a finger was not). A floating stick rests where the thumb lands, even near an edge, and not in the bottom gesture zone. A finger's width is at most 34 logical units, so the D-pad's buttons stay apart on a small dense display. A held button that takes over the swipe from another finger starts from where it is, not from where it went down. The system bars coming and going no longer release every finger. Hide stops the gyroscope. The libtiff comments are corrected: the screenshot_count and screenshot_size globals also reached tiff_export, but read back-buffer memory the port never draws into, so they never had the picture. Stale comments are fixed and the source exclusion test covers folders. --- port/android/README.md | 29 ++-- .../java/com/halo/decomp/TouchControls.java | 126 +++++++++++------- .../java/com/halo/decomp/TouchLayout.java | 10 +- port/android/guest/runtime/guest_host.h | 5 +- port/android/host/host_touch.c | 41 ++++-- port/linux/game/tiff_file.c | 13 +- port/linux/game/touch_game.c | 3 + port/linux/port.json | 2 +- port/linux/src/msvc_crt.c | 7 +- port/linux/src/touch_input.c | 35 ++++- port/linux/src/touch_input.h | 11 +- port/linux/src/xinput_sdl.c | 67 ++++++++-- source/game/player_control.c | 23 ++-- source/interface/ui_widget.c | 3 +- tools/test_linux_port.py | 8 +- tools/test_touch_layout.py | 3 +- tools/tests/TouchLayoutTest.java | 18 ++- 17 files changed, 281 insertions(+), 123 deletions(-) diff --git a/port/android/README.md b/port/android/README.md index e1003119..b48b1aeb 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -6,8 +6,9 @@ The game shows its graphics with OpenGL ES 3. It plays sound through SDL3 (AAudio). It accepts input from game controllers, for example a PlayStation -5 DualSense on Bluetooth, and from the touchscreen (refer to "Controls"). The app needs Android 9 (API 28) or later. It -operates on 64-bit-only devices, for example the Pixel 9 Pro XL. +5 DualSense on Bluetooth, and from the touchscreen (refer to "Controls"). +The app needs Android 9 (API 28) or later. It operates on 64-bit-only +devices, for example the Pixel 9 Pro XL. The Android build uses the platform layer of the Linux build (`port/linux/src`). Refer to [port/linux/README.md](../linux/README.md). @@ -164,14 +165,16 @@ Gamepads): with "Swap triggers", the right trigger's button says "Grenade". The stick moves the player with every "Stick layout", southpaw too. A finger that holds a button can also swipe to look, so you can fire and -aim with one thumb. No control is smaller than a finger (48 dp across), -whatever the size of the screen. A short tap reaches the game even when it is shorter -than one frame. Swipes that start in the edge-gesture zones of Android do -not turn the view. The swipe aims as the controller's stick does, not as -a mouse: the aim slows over a target and follows a moving one, as with a -controller, and it follows the profile's "invert look" (the setting -`input.touch_aim_assist` turns the aim assist off). "Look sensitivity" sets -how far a swipe turns; the mouse settings do not apply. +aim with one thumb. The controls are a finger wide (48 dp) or larger, but +on a small screen of high density they stay apart rather than reach that +size. A short tap reaches the game even when it is shorter than one frame. +Swipes that start in the edge-gesture zones of Android do not turn the +view. The swipe aims as the controller's stick does, not as a mouse: the +aim slows over a target and follows a moving one, as with a controller, +and it follows the profile's "invert look" (the gyroscope turns the view +as the phone turns, never inverted). The setting `input.touch_aim_assist` +turns the aim assist off. "Look sensitivity" sets how far a swipe turns; +the mouse settings do not apply. The touch controls show only in a game. In the menus and during cinematics they hide, and the touchscreen operates the menus as described @@ -182,8 +185,8 @@ to "Settings"). The buttons at the top of the screen: -- "Hide" removes the controls until you push "Touch"; the app remembers - it. +- "Hide" removes the controls (and stops the gyroscope aiming and the + vibration) until you push "Touch"; the app remembers it. - "Options" opens these items: - "General": the phone's vibration (on by default; it follows the vibration setting of the game's profile), aiming with the gyroscope @@ -221,7 +224,7 @@ These settings are only for Android: | Setting | Function | | --- | --- | | `input.touch_aim_assist` | `true` (the default): the touch controls' swipe aiming gets the aim assist of a controller (the aim slows over a target and follows a moving one). `false`: none, as with a mouse; the bullets' own autoaim stays. | -| `input.touch_controls` | The touch controls in a game. `"auto"` (the default): shown on a touchscreen while no controller is connected. `"on"`: also shown with a controller. `"off"`: never shown. A device without a touchscreen never shows them. The menus take taps with each value. | +| `input.touch_controls` | The touch controls in a game. `"auto"` (the default): shown on a touchscreen while no controller is connected. `"on"`: also shown with a controller. `"off"`: never shown. A device without a touchscreen never shows them. The menus take taps in any case. | | `display.screen_width` | The number of columns of the 480-line picture. `0` (the default): the shape of the display (1068 on a 20:9 phone). `640`: the 4:3 shape of the Xbox. | | `debug.sample_seconds` | Refer to "Find problems". | | `debug.memory_watch` | `true` (the default): the app notices the game's writes to textures and vertices by page protection. `false`: it compares page contents once a frame instead, which is slower. Refer to "Limits". | diff --git a/port/android/app/src/main/java/com/halo/decomp/TouchControls.java b/port/android/app/src/main/java/com/halo/decomp/TouchControls.java index e2e808e5..329e1208 100644 --- a/port/android/app/src/main/java/com/halo/decomp/TouchControls.java +++ b/port/android/app/src/main/java/com/halo/decomp/TouchControls.java @@ -41,10 +41,11 @@ import java.util.Locale; * aiming and an editor for the layout (TouchLayout). Its state goes to the * game through port/android/host/host_touch.c, as port 0's controller. * - * It shows only in a game, never over the menus or a cinematic (which take - * taps themselves, port/linux/src/touch_input.c), and with - * input.touch_controls = "auto" only on a touchscreen while no controller - * is connected. While hidden it lets every finger through to the game. + * It shows only in a game, never at the main menu, over a menu (which + * takes taps itself, port/linux/src/touch_input.c) or during a cinematic, + * only on a touchscreen, and with input.touch_controls = "auto" only while + * no controller is connected. While it is not shown it lets every finger + * through to the game. */ public final class TouchControls extends View implements SensorEventListener, InputManager.InputDeviceListener { /** what a finger holds besides a control: the view swipe */ @@ -77,6 +78,8 @@ public final class TouchControls extends View implements SensorEventListener, In /** a label is at most this many radii wide */ private static final float LABEL_WIDTH = 1.7f; private static final long POLL_MS = 16; + /** the game is in play this long before the controls show (no flash at a level's first frame) */ + private static final long SHOW_DELAY_MS = 200; private static final long VIBRATION_MS = 110; private static final long VIBRATION_RENEW_MS = 70; @@ -145,6 +148,7 @@ public final class TouchControls extends View implements SensorEventListener, In private static native void nativeState(int lx, int ly, int rx, int ry, int lt, int rt, int buttons); private static native void nativeLook(float dx, float dy); + private static native void nativeGyro(float dx, float dy); private static native void nativeLookReset(); private static native int nativeRumble(); private static native int nativeScene(); @@ -169,7 +173,8 @@ public final class TouchControls extends View implements SensorEventListener, In /** the controls removed by the toolbar's Hide, until its Touch */ private boolean hidden; private boolean editing; - private boolean optionsOpen; + /** the option dialogs open (one opens before the last one's dismissal is told) */ + private int openDialogs; /** what each finger holds: a control, LOOK, or (in toolbarFingers) a toolbar item */ private final SparseIntArray owners = new SparseIntArray(); @@ -201,10 +206,14 @@ public final class TouchControls extends View implements SensorEventListener, In private boolean controllerConnected; private boolean touchscreen; private boolean deviceInputActive; + /** the controls are on screen: shown() for SHOW_DELAY_MS */ + private boolean showing; + private long shownSince = -1; private boolean gyroRegistered; private int lastAmplitude; private long lastVibration; private final int[] bindings = DEFAULT_BINDINGS.clone(); + private final int[] bindingsRead = new int[bindings.length]; private int bindingsSerial; /** reads what the game says every frame: when to show, the buttons' names, and the rumble */ @@ -213,12 +222,8 @@ public final class TouchControls extends View implements SensorEventListener, In public void run() { if (!deviceInputActive) return; - boolean wasShown = shown(); scene = nativeScene(); - if (shown() != wasShown) { - reset(); - updateSensors(); - } + updateShowing(); readBindings(); updateRumble(); postDelayed(this, POLL_MS); @@ -244,13 +249,17 @@ public final class TouchControls extends View implements SensorEventListener, In setFocusable(false); setContentDescription("Halo touch controller"); setOnApplyWindowInsetsListener((view, insets) -> { + // (only the toolbar moves: the fingers stay held as the system bars come and go) + int right = insetRight; + int top = insetTop; insetRight = insets.getSystemWindowInsetRight(); insetTop = insets.getSystemWindowInsetTop(); if (insets.getDisplayCutout() != null) { insetRight = Math.max(insetRight, insets.getDisplayCutout().getSafeInsetRight()); insetTop = Math.max(insetTop, insets.getDisplayCutout().getSafeInsetTop()); } - layoutControls(); + if (insetRight != right || insetTop != top) + invalidate(); return insets; }); } @@ -316,9 +325,10 @@ public final class TouchControls extends View implements SensorEventListener, In invalidate(); } - /** a finger's width, in logical units */ + /** a finger's width, in logical units (at most TouchLayout.MAX_MINIMUM_RADIUS) */ private float minimumRadius() { - return MINIMUM_RADIUS_DP * getResources().getDisplayMetrics().density / scale; + float radius = MINIMUM_RADIUS_DP * getResources().getDisplayMetrics().density / scale; + return Math.min(radius, TouchLayout.MAX_MINIMUM_RADIUS); } private float logical(float pixels) { @@ -340,9 +350,9 @@ public final class TouchControls extends View implements SensorEventListener, In // ---------- when the controls show /** - * whether the controls are on screen: in a game (no menu or cinematic), - * never without a touchscreen, and with input.touch_controls = "auto" - * only while no controller is connected + * whether the controls belong on screen: in a game (no menu or + * cinematic), never without a touchscreen, and with + * input.touch_controls = "auto" only while no controller is connected */ private boolean shown() { if ((scene & SCENE_KNOWN) == 0 || (scene & (SCENE_MENUS | SCENE_OFF)) != 0 || !touchscreen) @@ -350,6 +360,24 @@ public final class TouchControls extends View implements SensorEventListener, In return (scene & SCENE_ON) != 0 || !controllerConnected; } + /** shows the controls once shown() has held for SHOW_DELAY_MS; hides them at once */ + private void updateShowing() { + boolean show = false; + if (!shown()) { + shownSince = -1; + } else { + long now = SystemClock.uptimeMillis(); + if (shownSince < 0) + shownSince = now; + show = now - shownSince >= SHOW_DELAY_MS; + } + if (show != showing) { + showing = show; + reset(); + updateSensors(); + } + } + /** a game controller: a stick or a hat, not a phone's few gamepad keys */ private static boolean isController(InputDevice device) { if (device == null || device.isVirtual()) @@ -373,13 +401,9 @@ public final class TouchControls extends View implements SensorEventListener, In if (!device.isVirtual() && touchSource) touch = true; } - boolean wasShown = shown(); controllerConnected = controller; touchscreen = touch; - if (shown() != wasShown) { - reset(); - updateSensors(); - } + updateShowing(); } @Override @@ -401,6 +425,7 @@ public final class TouchControls extends View implements SensorEventListener, In reset(); this.hidden = hidden; preferences.edit().putBoolean(KEY_HIDDEN, hidden).apply(); + updateSensors(); } // ---------- the controller state the game reads @@ -422,7 +447,7 @@ public final class TouchControls extends View implements SensorEventListener, In } private void publish() { - if (editing || optionsOpen) { + if (editing || openDialogs > 0) { nativeState(0, 0, 0, 0, 0, 0, 0); return; } @@ -454,12 +479,11 @@ public final class TouchControls extends View implements SensorEventListener, In return stickFloating ? stickY : layout.y(TouchLayout.LEFT); } - /** the floating stick, centred where a thumb landed, kept on the display */ + /** the floating stick, centred where a thumb landed (at rest there, even near an edge) */ private void floatStick(float x, float y) { - float radius = layout.radius(TouchLayout.LEFT); stickFloating = true; - stickX = Math.max(radius, Math.min(logicalWidth - radius, x)); - stickY = Math.max(radius, Math.min(logicalHeight - radius, y)); + stickX = x; + stickY = y; } private boolean inFloatingZone(float x, float y) { @@ -527,7 +551,7 @@ public final class TouchControls extends View implements SensorEventListener, In @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); - if (!shown() && !editing && !optionsOpen) { + if (!showing && !editing && openDialogs == 0) { // the menus and cinematics take the fingers (SDL's surface below) if (action == MotionEvent.ACTION_DOWN) return false; @@ -583,7 +607,7 @@ public final class TouchControls extends View implements SensorEventListener, In int target = hit(x, y); int stick = TouchLayout.LEFT; if (target == NOTHING && layout.floatingStick && !hidden && layout.shown(stick) && !held(stick) - && inFloatingZone(x, y)) { + && inFloatingZone(x, y) && !inGestureZone(px, py, false)) { floatStick(x, y); target = stick; } @@ -624,6 +648,11 @@ public final class TouchControls extends View implements SensorEventListener, In startLook(id, toolbar[0], toolbar[1]); } float[] origin = buttonTouches.get(id); + if (origin != null && lookPointer >= 0 && lookPointer != id) { + // while another finger aims, a held button's finger may only take over from where it is + origin[0] = px; + origin[1] = py; + } if (lookPointer < 0 && origin != null && movedPastSlop(origin, px, py)) { // a held button (either Fire too) also aims lookPointer = id; @@ -708,13 +737,12 @@ public final class TouchControls extends View implements SensorEventListener, In // ---------- the buttons' names, from the player's profile private void readBindings() { - int[] controls = new int[bindings.length]; - int serial = nativeBindings(controls); + int serial = nativeBindings(bindingsRead); if (serial == 0 || serial == bindingsSerial) return; bindingsSerial = serial; - if (!Arrays.equals(controls, bindings)) { - System.arraycopy(controls, 0, bindings, 0, bindings.length); + if (!Arrays.equals(bindingsRead, bindings)) { + System.arraycopy(bindingsRead, 0, bindings, 0, bindings.length); invalidate(); } } @@ -759,20 +787,24 @@ public final class TouchControls extends View implements SensorEventListener, In layout = imported.layout; sensitivity = imported.sensitivity; setHidden(false); - updateSensors(); cancelRumble(); invalidate(); } // ---------- the options + /** opens an option dialog; the controls are released while any is open, and saved when the last closes */ private void openDialog(AlertDialog dialog) { dialog.setOnDismissListener(d -> { - optionsOpen = false; + openDialogs--; + if (openDialogs > 0) + return; + openDialogs = 0; + saveLayout(); reset(); updateSensors(); }); - optionsOpen = true; + openDialogs++; reset(); dialog.show(); } @@ -922,8 +954,8 @@ public final class TouchControls extends View implements SensorEventListener, In Runnable refresh = () -> { int percent = Math.round(layout.sizeScale(control) * 100); value.setText(percent + "%"); - smaller.setEnabled(percent > 50); - larger.setEnabled(percent < 200); + smaller.setEnabled(percent > Math.round(TouchLayout.MIN_SIZE * 100)); + larger.setEnabled(percent < Math.round(TouchLayout.MAX_SIZE * 100)); }; smaller.setText("-"); smaller.setOnClickListener(view -> resize(control, -10, refresh)); @@ -946,7 +978,8 @@ public final class TouchControls extends View implements SensorEventListener, In private void resize(int control, int percent, Runnable refresh) { int size = Math.round(layout.sizeScale(control) * 100) + percent; - layout.setSize(control, Math.max(50, Math.min(200, size)) / 100f); + size = Math.max(Math.round(TouchLayout.MIN_SIZE * 100), Math.min(Math.round(TouchLayout.MAX_SIZE * 100), size)); + layout.setSize(control, size / 100f); saveLayout(); refresh.run(); invalidate(); @@ -1075,7 +1108,7 @@ public final class TouchControls extends View implements SensorEventListener, In // ---------- the gyroscope and the vibration private void updateSensors() { - boolean needed = deviceInputActive && shown() && layout.gyroscopeEnabled && gyroscope != null; + boolean needed = deviceInputActive && showing && !hidden && layout.gyroscopeEnabled && gyroscope != null; if (needed && !gyroRegistered) { gyroAim.reset(); gyroRegistered = sensors.registerListener(this, gyroscope, SensorManager.SENSOR_DELAY_GAME); @@ -1088,15 +1121,15 @@ public final class TouchControls extends View implements SensorEventListener, In @Override public void onSensorChanged(SensorEvent event) { - if (!deviceInputActive || !shown() || hidden || !layout.gyroscopeEnabled || editing || optionsOpen) { + if (!deviceInputActive || !showing || hidden || !layout.gyroscopeEnabled || editing || openDialogs > 0) { gyroAim.reset(); return; } int rotation = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay().getRotation(); if (gyroAim.sample(event.timestamp, event.values[0], event.values[1], rotation, gyroDelta)) { - // as a swipe of this many logical units - nativeLook(-gyroDelta[0] / LOOK_RADIANS_PER_UNIT * sensitivity, + // as a swipe of this many logical units (which the profile's invert leaves alone) + nativeGyro(-gyroDelta[0] / LOOK_RADIANS_PER_UNIT * sensitivity, -gyroDelta[1] / LOOK_RADIANS_PER_UNIT * sensitivity); } } @@ -1109,7 +1142,7 @@ public final class TouchControls extends View implements SensorEventListener, In private void updateRumble() { if (vibrator == null || !vibrator.hasVibrator()) return; - boolean active = shown() && !hidden && layout.rumbleEnabled && !editing && !optionsOpen; + boolean active = showing && !hidden && layout.rumbleEnabled && !editing && openDialogs == 0; int amplitude = active ? nativeRumble() : 0; if (amplitude == 0) { cancelRumble(); @@ -1191,8 +1224,9 @@ public final class TouchControls extends View implements SensorEventListener, In int stick = TouchLayout.LEFT; float radius = layout.radius(stick); float size = layout.sizeScale(stick); - float x = stickCenterX(); - float y = stickCenterY(); + // (a floating stick near an edge is drawn whole, nearer the middle than its centre) + float x = Math.max(radius, Math.min(logicalWidth - radius, stickCenterX())); + float y = Math.max(radius, Math.min(logicalHeight - radius, stickCenterY())); circle(canvas, x, y, radius, "Move", false, 11 * size, layout.opacity); circle(canvas, x + axes[0] / 32767f * radius, y + axes[1] / 32767f * radius, Math.max(STICK_KNOB_RADIUS * size, radius * 0.375f), "", held(stick), 11, layout.opacity); @@ -1200,7 +1234,7 @@ public final class TouchControls extends View implements SensorEventListener, In @Override protected void onDraw(Canvas canvas) { - if (!shown() && !editing) + if (!showing && !editing) return; canvas.save(); canvas.scale(scale, scale); diff --git a/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java b/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java index 79d6e4fc..d0f37f9a 100644 --- a/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java +++ b/port/android/app/src/main/java/com/halo/decomp/TouchLayout.java @@ -26,6 +26,11 @@ final class TouchLayout { static final float MAX_OPACITY = 1f; static final float MIN_SENSITIVITY = 0.25f; static final float MAX_SENSITIVITY = 4f; + /** + * the largest minimum radius: a finger's width is capped here on a small, + * dense display, where the D-pad's buttons (69 apart) would overlap + */ + static final float MAX_MINIMUM_RADIUS = 34; /** the saved grid */ private static final float GRID_WIDTH = 960; @@ -100,10 +105,11 @@ final class TouchLayout { /** * the smallest radius, in logical units, at which a control is drawn and - * touched (a finger's width, whatever the display's density) + * touched (a finger's width, whatever the display's density), at most + * MAX_MINIMUM_RADIUS */ void setMinimumRadius(float radius) { - minimumRadius = Float.isFinite(radius) && radius > 0 ? radius : 0; + minimumRadius = Float.isFinite(radius) && radius > 0 ? Math.min(radius, MAX_MINIMUM_RADIUS) : 0; keepAllInside(); } diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h index ec9520c8..5667fb2a 100644 --- a/port/android/guest/runtime/guest_host.h +++ b/port/android/guest/runtime/guest_host.h @@ -122,11 +122,12 @@ void host_gesture_insets(int *insets); /* the overlay's controller: the SDL axes (left x, y, right x, y, left trigger, right trigger) and the SDL button bits, into state[7] */ void host_touch_read(int *state); -/* the overlay's view swipe since the last read, into delta[2] */ +/* the overlay's view swipe, then its gyroscope turn, since the last read, +into delta[4] */ void host_touch_look_read(float *delta); /* port 0's motors, for the phone's vibration */ void host_touch_rumble(unsigned int low, unsigned int high); -/* tells the overlay when to show: HALO_TOUCH_SCENE_* (xinput_sdl.c) */ +/* tells the overlay when to show: touch_input.c's _touch_scene_* */ void host_touch_scene(int scene); /* the game control on each of the 16 controller buttons (touch_game.c), for the overlay's labels */ diff --git a/port/android/host/host_touch.c b/port/android/host/host_touch.c index fc87fb7e..50eab656 100644 --- a/port/android/host/host_touch.c +++ b/port/android/host/host_touch.c @@ -3,11 +3,13 @@ HOST_TOUCH.C The on-screen touch controls between the overlay and the game. The overlay (app/.../TouchControls.java) is an Android view over SDL's surface: it -hands its stick, buttons and view swipes over JNI on the UI thread, and the -game reads them through host imports when it reads port 0's controller -(port/linux/src/xinput_sdl.c). The game hands back what the overlay needs -to know: whether a menu or a cinematic is up and the input.touch_controls -setting (host_touch_scene), and how hard port 0 rumbles. +hands its stick, buttons, view swipes and gyroscope turns over JNI on the UI +thread, and the game reads them through host imports when it reads port 0's +controller (port/linux/src/xinput_sdl.c). The game hands back what the +overlay needs to know: whether it is in a game with no menu or cinematic up +and the input.touch_controls setting (host_touch_scene), the profile's +button mapping, which names the buttons (host_touch_bindings), and how hard +port 0 rumbles. */ #include @@ -34,6 +36,9 @@ static int32_t touch_state[7]; static int64_t touch_pressed_ns[TOUCH_INPUTS]; static float look_delta[2]; static int64_t look_ns; +/* the gyroscope's turn, apart: the profile's invert is the swipe's only */ +static float gyro_delta[2]; +static int64_t gyro_ns; static int rumble_amplitude; static int64_t rumble_ns; static volatile int touch_scene; @@ -113,26 +118,44 @@ JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeLook( pthread_mutex_unlock(&touch_lock); } +JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeGyro( + JNIEnv *env, jclass cls, jfloat dx, jfloat dy) +{ + (void)env; + (void)cls; + pthread_mutex_lock(&touch_lock); + gyro_delta[0] += dx; + gyro_delta[1] += dy; + gyro_ns = now_ns(); + pthread_mutex_unlock(&touch_lock); +} + JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeLookReset(JNIEnv *env, jclass cls) { (void)env; (void)cls; pthread_mutex_lock(&touch_lock); look_delta[0] = look_delta[1] = 0.0f; + gyro_delta[0] = gyro_delta[1] = 0.0f; pthread_mutex_unlock(&touch_lock); } -/* the guest's: the view swipe since the last read, in the overlay's -logical pixels with its sensitivity applied */ +/* the guest's: the view swipe, then the gyroscope's turn, since the last +read, into delta[4], in the overlay's logical pixels with its sensitivity +applied */ void host_touch_look_read(float *delta) { int64_t now = now_ns(); pthread_mutex_lock(&touch_lock); memcpy(delta, look_delta, sizeof(look_delta)); + memcpy(delta + 2, gyro_delta, sizeof(gyro_delta)); if (now - look_ns > TOUCH_STALE_NS) delta[0] = delta[1] = 0.0f; + if (now - gyro_ns > TOUCH_STALE_NS) + delta[2] = delta[3] = 0.0f; look_delta[0] = look_delta[1] = 0.0f; + gyro_delta[0] = gyro_delta[1] = 0.0f; pthread_mutex_unlock(&touch_lock); } @@ -162,8 +185,8 @@ JNIEXPORT jint JNICALL Java_com_halo_decomp_TouchControls_nativeRumble(JNIEnv *e return amplitude; } -/* the guest's, at every read of port 0: HALO_TOUCH_SCENE_* bits -(port/linux/src/xinput_sdl.c) */ +/* the guest's, at every read of port 0: port/linux/src/touch_input.c's +_touch_scene_* bits */ void host_touch_scene(int scene) { touch_scene = scene; diff --git a/port/linux/game/tiff_file.c b/port/linux/game/tiff_file.c index 3936e945..1609dab0 100644 --- a/port/linux/game/tiff_file.c +++ b/port/linux/game/tiff_file.c @@ -2,10 +2,13 @@ TIFF_FILE.C port: the builds leave out source/bitmaps/tiff_file.c and the libtiff it -writes with (port/linux/port.json). Its one function, tiff_export, is called -only by the Xbox's movie recorder (main.c's screenshot_record, while -main_movie_start's movie runs), which nothing starts; the port's screenshots -are its own (d3d8_gl.c). +writes with (port/linux/port.json). Its one function, tiff_export, is +called by main.c's screenshot_record: for the Xbox's movie recorder +(main_movie_start, which nothing starts) and for the screenshot_count and +screenshot_size globals (screenshot_render). Those read the back buffer's +memory, which the port draws into with OpenGL, never the CPU, so they never +had the picture; the port's screenshots are its own (the Screenshot key, +controls.screenshot, and debug.screenshot_every: d3d8_gl.c). */ #include "cseries/cseries.h" @@ -17,5 +20,5 @@ char const *tiff_export( { (void)file; (void)bitmap; - return "this build writes no TIFF files"; + return "this build writes no TIFF screenshots: use the Screenshot key (controls.screenshot)"; } diff --git a/port/linux/game/touch_game.c b/port/linux/game/touch_game.c index f1c731f9..2d7b7e0d 100644 --- a/port/linux/game/touch_game.c +++ b/port/linux/game/touch_game.c @@ -40,6 +40,9 @@ int touch_game_playing(void) return cinematic_globals && game_in_progress() && !main_menu_is_active(); } +/* (touch_input.c's TOUCH_BUTTONS and the overlay count 16 buttons) */ +typedef char verify_touch_buttons[NUMBER_OF_GAMEPAD_BUTTONS == 16 ? 1 : -1]; + /* the game control on each of player 1's 16 controller buttons, as the gamepad's buttons are numbered (input.h), -1 for none: the touch controls name their buttons by them, so a profile's own mapping shows */ diff --git a/port/linux/port.json b/port/linux/port.json index 10408910..b9dd6a6c 100644 --- a/port/linux/port.json +++ b/port/linux/port.json @@ -1,7 +1,7 @@ { "comment": "Native build inputs, shared by the Windows and Android builds. See port/linux/README.md.", "game": { - "comment": "The game: every C file under root but those excluded (a folder ends in /: libtiff and the TIFF writer that uses it, which only the Xbox's movie recorder called, port/linux/game/tiff_file.c; zlib's own programs), built with these defines and include directories.", + "comment": "The game: every C file under root but those excluded (a folder ends in /: libtiff and the TIFF writer that uses it, port/linux/game/tiff_file.c has why; zlib's own programs), built with these defines and include directories.", "root": "source", "exclude": [ "source/bitmaps/libtiff/", diff --git a/port/linux/src/msvc_crt.c b/port/linux/src/msvc_crt.c index afeaddac..d864f83f 100644 --- a/port/linux/src/msvc_crt.c +++ b/port/linux/src/msvc_crt.c @@ -1,8 +1,8 @@ /* MSVC_CRT.C -Microsoft C runtime functions the game (and its copies of libtiff and zlib) -call that glibc does not provide under the same names, plus the Xbox-path +Microsoft C runtime functions the game (and its copy of zlib) call that +glibc does not provide under the same names, plus the Xbox-path aware fopen/open family that the game's headers redirect to (see port/linux/include/stdio.h). */ @@ -678,7 +678,8 @@ int _fileno(FILE *stream) return fileno(stream); } -/* libtiff maps its POSIX calls onto the MSVC underscore names itself */ +/* the MSVC underscore names of the POSIX file calls (the game's libtiff +used them; the builds leave it out: port.json) */ int _open(const char *path, int flags, ...) { diff --git a/port/linux/src/touch_input.c b/port/linux/src/touch_input.c index 65ff6e23..16611015 100644 --- a/port/linux/src/touch_input.c +++ b/port/linux/src/touch_input.c @@ -13,6 +13,7 @@ finger down (host_gesture_insets). #include "touch_menu.h" #include "port_config.h" +#include #include #include @@ -209,10 +210,10 @@ enum _touch_scene_off = 1 << 3, }; -/* input.touch_controls as _touch_scene_on, _touch_scene_off or 0 */ /* the touch controls' stick at port 0's last read, -1..1, y down */ static float move_x, move_y; +/* input.touch_controls as _touch_scene_on, _touch_scene_off or 0 */ static int touch_controls_setting(void) { static int setting; @@ -293,19 +294,41 @@ void touch_input_controls(XINPUT_GAMEPAD *pad, int menus) int touch_input_move(float *forward, float *strafe) { + float x = move_x; + float y = move_y; + float largest = fabsf(x) > fabsf(y) ? fabsf(x) : fabsf(y); + + if (largest == 0.0f) + { + *forward = 0.0f; + *strafe = 0.0f; + return FALSE; + } + /* the overlay's circle onto the square a controller's stick gives + (input_abstraction_update): a full diagonal is full on both axes */ + { + float scale = sqrtf(x * x + y * y) / largest; + + x *= scale; + y *= scale; + x = x < -1.0f ? -1.0f : x > 1.0f ? 1.0f : x; + y = y < -1.0f ? -1.0f : y > 1.0f ? 1.0f : y; + } /* (SDL's y runs down; strafe is positive to the left) */ - *forward = -move_y; - *strafe = -move_x; - return move_x != 0.0f || move_y != 0.0f; + *forward = -y; + *strafe = -x; + return TRUE; } -void touch_input_look(float scale, float *yaw, float *pitch) +void touch_input_look(float scale, float *yaw, float *pitch, float *gyro_yaw, float *gyro_pitch) { - float delta[2]; + float delta[4]; host_touch_look_read(delta); *yaw -= delta[0] * scale; *pitch -= delta[1] * scale; + *gyro_yaw -= delta[2] * scale; + *gyro_pitch -= delta[3] * scale; } int touch_input_aim_assist(void) diff --git a/port/linux/src/touch_input.h b/port/linux/src/touch_input.h index ef0ed155..15f2f632 100644 --- a/port/linux/src/touch_input.h +++ b/port/linux/src/touch_input.h @@ -31,8 +31,8 @@ void touch_input_menu_set_active(int active); marked as touch */ void touch_input_menu_read(struct platform_ui_pointer *pointer); -/* adds the touch controls to the gamepad state; outside the menus a tap -presses A for a few polls when a cinematic can be skipped */ +/* outside the menus, presses A for a few polls after a tap when a +cinematic can be skipped (the on-screen controls: touch_input_controls) */ void touch_input_gamepad(XINPUT_GAMEPAD *pad); #ifdef HALO_ANDROID @@ -45,9 +45,10 @@ void touch_input_controls(XINPUT_GAMEPAD *pad, int menus); strafe to the left); nonzero while it is pushed */ int touch_input_move(float *forward, float *strafe); -/* adds the touch controls' view swipe since the last call to yaw and -pitch, in radians at `scale` radians per pixel */ -void touch_input_look(float scale, float *yaw, float *pitch); +/* adds the touch controls' view swipe and gyroscope turn since the last +call to yaw and pitch and to gyro_yaw and gyro_pitch, in radians at `scale` +radians per pixel */ +void touch_input_look(float scale, float *yaw, float *pitch, float *gyro_yaw, float *gyro_pitch); /* whether the swipe gets the stick's aim assist (input.touch_aim_assist) */ int touch_input_aim_assist(void); diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c index cce48a63..679bd655 100644 --- a/port/linux/src/xinput_sdl.c +++ b/port/linux/src/xinput_sdl.c @@ -91,10 +91,14 @@ static Uint64 wheel_press_until_ms = 0; static BOOL wheel_scrolling = FALSE; /* the way the scroll under way turns: 1 up (away), -1 down */ static int wheel_direction = 0; -/* when port 0's aim last moved, by the mouse and by the right stick -(halo_linux_mouse_aiming) */ +/* when port 0's aim last moved, by the mouse, by the right stick, and by +the touch controls (their swipe, gyroscope or stick: halo_linux_mouse_aiming, +halo_linux_touch_aiming) */ static Uint64 mouse_aimed_ms = 0; static Uint64 stick_aimed_ms = 0; +#ifdef HALO_ANDROID +static Uint64 touch_aimed_ms = 0; +#endif /* the right stick's deflection that counts as aiming with it, clear of a worn stick's drift */ @@ -154,6 +158,16 @@ int halo_linux_mouse_look(short gamepad_index, float *yaw, float *pitch) return TRUE; } +#ifdef HALO_ANDROID +/* the touch controls moved the player or the aim (halo_linux_touch_aiming) */ +static void touch_used(void) +{ + pthread_mutex_lock(&mouse_lock); + touch_aimed_ms = SDL_GetTicks(); + pthread_mutex_unlock(&mouse_lock); +} +#endif + /* the touch controls' stick as the player's movement (input_abstraction.c's keyboard_controls_update), forward and strafe -1..1, whatever the profile's sticks do; nonzero while it is pushed. Nothing but on Android. */ @@ -164,28 +178,57 @@ int halo_linux_touch_move(short controller_index, float *forward, float *strafe) if (controller_index != 0) return FALSE; #ifdef HALO_ANDROID - return touch_input_move(forward, strafe); + if (!touch_input_move(forward, strafe)) + return FALSE; + touch_used(); + return TRUE; #else return FALSE; #endif } -/* the touch controls' swipe since the last call, in radians at the mouse's -rate per pixel (the view applies its own sensitivity), and whether it gets -the stick's aim assist (input.touch_aim_assist): player_control.c makes it -the stick's rate before the magnetism. Nothing but on Android. */ -int halo_linux_touch_look(short gamepad_index, float *yaw, float *pitch, int *assisted) +/* the touch controls' swipe and gyroscope since the last call, in radians at +the mouse's rate per pixel (the view applies its own sensitivity), each +apart: player_control.c inverts the swipe as the profile inverts the stick, +not the gyroscope, which turns as the phone does, and makes them the +stick's rate before the magnetism. Nothing but on Android. */ +int halo_linux_touch_look(short gamepad_index, float *yaw, float *pitch, float *gyro_yaw, float *gyro_pitch) { *yaw = 0.0f; *pitch = 0.0f; - *assisted = FALSE; + *gyro_yaw = 0.0f; + *gyro_pitch = 0.0f; if (gamepad_index != 0) return FALSE; #ifdef HALO_ANDROID - touch_input_look(0.0022f, yaw, pitch); - *assisted = touch_input_aim_assist(); + touch_input_look(0.0022f, yaw, pitch, gyro_yaw, gyro_pitch); + if (*yaw == 0.0f && *pitch == 0.0f && *gyro_yaw == 0.0f && *gyro_pitch == 0.0f) + return FALSE; + touch_used(); + return TRUE; +#else + return FALSE; +#endif +} + +/* whether port 0's player aims with the touch controls without their aim +assist (input.touch_aim_assist false): no magnetism, as for the mouse +(player_control.c), until a stick moves the aim again */ +int halo_linux_touch_aiming(short gamepad_index) +{ +#ifdef HALO_ANDROID + int aiming; + + if (gamepad_index != 0 || touch_input_aim_assist()) + return FALSE; + pthread_mutex_lock(&mouse_lock); + aiming = touch_aimed_ms != 0 && touch_aimed_ms >= stick_aimed_ms; + pthread_mutex_unlock(&mouse_lock); + return aiming; +#else + (void)gamepad_index; + return FALSE; #endif - return *yaw != 0.0f || *pitch != 0.0f; } /* whether the player on the gamepad aims with the mouse (it moved after the diff --git a/source/game/player_control.c b/source/game/player_control.c index de13ef5c..73603f69 100644 --- a/source/game/player_control.c +++ b/source/game/player_control.c @@ -1143,10 +1143,8 @@ static void get_local_player_input_blob( real yaw_spin_scale; real pitch_spin_scale; real_euler_angles2d look_delta; - /* port: the touch controls' swipe this frame, and whether it - gets the stick's aim assist */ + /* port: the touch controls' swipe or gyroscope this frame */ boolean touching = FALSE; - int touch_assisted = FALSE; if (input_state->buttons[_button_scope_zoom] && controls_enable_doubled_spin) @@ -1185,13 +1183,17 @@ static void get_local_player_input_blob( a turn this frame, made a rate as the stick's look is: the zoom, the stun and the magnetism below act on it as on the stick's, so that it slows over a target and follows a moving - one (input.touch_aim_assist); inverted as the stick is */ - extern int halo_linux_touch_look(short gamepad_index, real *yaw, real *pitch, int *assisted); + one (input.touch_aim_assist); the swipe inverted as the stick + is, the gyroscope as the phone turns */ + extern int halo_linux_touch_look(short gamepad_index, real *yaw, real *pitch, real *gyro_yaw, + real *gyro_pitch); real touch_yaw; real touch_pitch; + real gyro_yaw; + real gyro_pitch; real touch_scale = time_delta_sec * TICKS_PER_SECOND; - if (halo_linux_touch_look(gamepad_index, &touch_yaw, &touch_pitch, &touch_assisted) && + if (halo_linux_touch_look(gamepad_index, &touch_yaw, &touch_pitch, &gyro_yaw, &gyro_pitch) && touch_scale > _real_epsilon) { touching = TRUE; @@ -1199,8 +1201,8 @@ static void get_local_player_input_blob( { touch_pitch = -touch_pitch; } - look_delta.yaw += touch_yaw / touch_scale; - look_delta.pitch += touch_pitch / touch_scale; + look_delta.yaw += (touch_yaw + gyro_yaw) / touch_scale; + look_delta.pitch += (touch_pitch + gyro_pitch) / touch_scale; } } @@ -1261,10 +1263,11 @@ static void get_local_player_input_blob( &target_angular_velocity); { /* no magnetism for the mouse (port/linux/src/xinput_sdl.c), - nor for the touch controls' swipe without its aim assist */ + nor for the touch controls without their aim assist */ extern int halo_linux_mouse_aiming(short gamepad_index); + extern int halo_linux_touch_aiming(short gamepad_index); - if (halo_linux_mouse_aiming(gamepad_index) || (touching && !touch_assisted)) + if (halo_linux_mouse_aiming(gamepad_index) || halo_linux_touch_aiming(gamepad_index)) { control->magnetism_level = 0.f; } diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 8512864a..884a4722 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5675,7 +5675,8 @@ static boolean ui_mouse_click_pending = FALSE; static short ui_mouse_hover_x, ui_mouse_hover_y; static short ui_mouse_click_x, ui_mouse_click_y; /* whether the latest pointer read was the touchscreen: the taller legend -areas are for a finger only, the desktop mouse keeps their original height */ +areas, the merged setting rows and the widened values are for a finger only +(render_ui_widgets); the desktop mouse keeps their original areas */ static boolean ui_mouse_pointer_is_touch = FALSE; /* ---------- the debug view of the targets (debug.touch_targets) diff --git a/tools/test_linux_port.py b/tools/test_linux_port.py index 5fcf7554..4f22447b 100644 --- a/tools/test_linux_port.py +++ b/tools/test_linux_port.py @@ -156,11 +156,15 @@ def test_game_sources_leave_out_the_excluded(tmp_path, monkeypatch): write(tmp_path / "source" / "a.c", "") write(tmp_path / "source" / "zlib" / "b.c", "") write(tmp_path / "source" / "zlib" / "example.c", "") + write(tmp_path / "source" / "tiff" / "c.c", "") + write(tmp_path / "source" / "tiff" / "deep" / "d.c", "") + write(tmp_path / "source" / "tiffx.c", "") write(tmp_path / "source" / "a.h", "") monkeypatch.chdir(tmp_path) - config = {"game": {"root": "source", "exclude": ["source/zlib/example.c"], + # (a folder is a name ending in "/": not a file that only starts like it) + config = {"game": {"root": "source", "exclude": ["source/zlib/example.c", "source/tiff/"], "defines": ["DEBUG"], "include_dirs": ["source", "source/saved games"]}} - assert linux_build.game_sources(config) == [Path("source/a.c"), Path("source/zlib/b.c")] + assert linux_build.game_sources(config) == [Path("source/a.c"), Path("source/tiffx.c"), Path("source/zlib/b.c")] assert linux_build.game_defines_and_includes(config) == '-DDEBUG -Isource -I"source/saved games"' diff --git a/tools/test_touch_layout.py b/tools/test_touch_layout.py index 761166b1..7f7ecd44 100644 --- a/tools/test_touch_layout.py +++ b/tools/test_touch_layout.py @@ -7,12 +7,11 @@ root = Path(__file__).resolve().parent.parent classes = root / "build/touch-layout-tests" classes.mkdir(parents=True, exist_ok=True) subprocess.run([ - "javac", "-d", str(classes), + "javac", "--release", "17", "-d", str(classes), str(root / "port/android/app/src/main/java/com/halo/decomp/TouchLayout.java"), str(root / "port/android/app/src/main/java/com/halo/decomp/GyroscopeAim.java"), str(root / "tools/tests/GyroscopeAimTest.java"), str(root / "tools/tests/TouchLayoutTest.java") ], check=True) subprocess.run(["java", "-cp", str(classes), "com.halo.decomp.TouchLayoutTest"], check=True) - subprocess.run(["java", "-cp", str(classes), "com.halo.decomp.GyroscopeAimTest"], check=True) diff --git a/tools/tests/TouchLayoutTest.java b/tools/tests/TouchLayoutTest.java index 6914035c..e4789d36 100644 --- a/tools/tests/TouchLayoutTest.java +++ b/tools/tests/TouchLayoutTest.java @@ -115,12 +115,22 @@ public final class TouchLayoutTest { } // A finger-sized minimum radius enlarges small buttons and keeps them on the display. TouchLayout fingers = new TouchLayout(); - fingers.setMinimumRadius(40); - check(fingers.radius(12) == 40 && fingers.radius(TouchLayout.LEFT) == 64, + fingers.setMinimumRadius(30); + check(fingers.radius(12) == 30 && fingers.radius(TouchLayout.LEFT) == 64, "The minimum radius enlarges small controls only"); fingers.move(12, 0, 0); - check(fingers.x(12) == 40 && fingers.y(12) == 40, "Enlarged controls stay inside the display"); - check(fingers.savedX(12) == 40, "The minimum radius does not change saved places"); + check(fingers.x(12) == 30 && fingers.y(12) == 30, "Enlarged controls stay inside the display"); + check(fingers.savedX(12) == 30, "The minimum radius does not change saved places"); + fingers.setMinimumRadius(80); + check(fingers.radius(12) == TouchLayout.MAX_MINIMUM_RADIUS, "The minimum radius is capped"); + TouchLayout dense = new TouchLayout(); + dense.setMinimumRadius(80); + for (int a = 12; a <= 15; a++) { + for (int b = a + 1; b <= 15; b++) { + check(Math.hypot(dense.x(a) - dense.x(b), dense.y(a) - dense.y(b)) >= dense.radius(a) + dense.radius(b), + "At the capped minimum radius the D-pad's buttons stay apart"); + } + } fingers.setMinimumRadius(Float.NaN); check(fingers.radius(12) == 25, "An invalid minimum radius is none"); wide.resetDefaults(); From f75c6dc90dc5a73649f21dbc2139ac48799b9b17 Mon Sep 17 00:00:00 2001 From: justinfilip Date: Fri, 9 Oct 2026 18:16:14 -0500 Subject: [PATCH 125/187] Call the original weapon view Default. The menu labeled that choice SAME. A chosen angle already sets that field of view. --- ...yer_profile_edit.video_settings.fov_viewmodels.xml | 4 ++-- port/linux/README.md | 11 ++++++----- port/linux/game/viewmodel_fov.c | 5 +++-- port/linux/src/port_config.c | 3 ++- tools/port_settings.py | 4 ++-- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml index 26986d63..1cea9b97 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml @@ -38,7 +38,7 @@ - + @@ -68,7 +68,7 @@ - + diff --git a/port/linux/README.md b/port/linux/README.md index 8a448957..2a6736b0 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -316,10 +316,11 @@ the setting for one start of the game. It has priority over the file. Video Setup's **FOV AND VIEWMODELS** category offers optional world and weapon field of view settings and first person visibility. **DEFAULT** keeps -the authored world projection. **SAME** keeps the original weapon view, and -viewmodels start **ON**. OK saves and applies changes. Cancel discards them. -The menu offers 80 to 150 degrees in steps of five. The config file accepts -20 to 150 degrees, including fractional values, or zero for Default and Same. +the authored world projection. **DEFAULT** for the weapon keeps its original +view, and viewmodels start **ON**. A chosen weapon angle is that FOV. OK +saves and applies changes. Cancel discards them. The menu offers 80 to 150 +degrees in steps of five. The config file accepts 20 to 150 degrees, +including fractional values, or zero for Default. Angles are horizontal at 16:9; other aspect ratios retain the same vertical view. World FOV applies to on-foot first-person views, preserving authored vehicle, death and cinematic cameras. Completed scopes keep their native @@ -343,7 +344,7 @@ only their display; firing, animation, sound and world lights continue. | `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". | | `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". | | `display.fov` | `0.0` | `HALO_FOV` | On-foot first-person horizontal FOV at 16:9, from 20 to 150 degrees. `0` keeps the authored view. Video Setup's FOV and Viewmodels screen sets it. | -| `display.viewmodel_fov` | `0.0` | `HALO_VIEWMODEL_FOV` | Independent weapon and hands horizontal FOV at 16:9, 20 to 150 degrees. `0` keeps the original weapon view. | +| `display.viewmodel_fov` | `0.0` | `HALO_VIEWMODEL_FOV` | Weapon and hands horizontal FOV at 16:9, 20 to 150 degrees. `0` is Default and keeps the original weapon view. A chosen angle is that FOV. | | `display.viewmodel_visible` | `true` | `HALO_VIEWMODEL_VIS` | Show first-person weapons, hands and attached visuals. This does not change gameplay or other players' models. | | `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. | | `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the resolution the game draws at, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. | diff --git a/port/linux/game/viewmodel_fov.c b/port/linux/game/viewmodel_fov.c index d815bf01..d049a836 100644 --- a/port/linux/game/viewmodel_fov.c +++ b/port/linux/game/viewmodel_fov.c @@ -43,8 +43,9 @@ void viewmodel_projection_begin(void) if (projection_depth++ != 0) return; projection_applied = FALSE; angle = viewmodel_vertical(); - /* SAME keeps the weapon at this frame's own angle. A wide world view - stretches arms and a gun that sit against the camera. */ + /* Default keeps the weapon at this frame's own angle. A chosen angle + is that FOV and is left as it is. A wide world view stretches arms + and a gun that sit against the camera, so Default does not follow it. */ if (!angle) angle = render_fov_authored_vertical(render.local_player_index); if (!angle || render.local_player_index < 0 || render.local_player_index >= MAXIMUM_LOCAL_PLAYERS || diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index c6d80449..c4104603 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -117,7 +117,8 @@ static const struct config_setting config_settings[] = "0 keeps the authored view. Other cameras keep their own FOV." }, { "display.viewmodel_fov", _config_real, "0.0", "HALO_VIEWMODEL_FOV", _environment_value, _platform_all, "Weapon and hands horizontal FOV at 16:9, in degrees (20 to 150);\n" - "0 keeps the original weapon view. Attached visuals use the same projection." }, + "0 is Default and keeps the original weapon view. A chosen angle is\n" + "that FOV. Attached visuals use the same projection." }, { "display.viewmodel_visible", _config_boolean, "true", "HALO_VIEWMODEL_VIS", _environment_value, _platform_all, "Draw the first-person weapon, hands and attached visuals. Turning\n" "this off does not change firing, animation, sound or world lights." }, diff --git a/tools/port_settings.py b/tools/port_settings.py index 53bddec6..0fb941f9 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -198,8 +198,8 @@ SCREENS["video_settings/fov_viewmodels"] = { "rows": [ ("FOV:", "display.fov", [("DEFAULT", "0")] + [(str(n), str(n)) for n in range(80, 151, 5)], "On-foot horizontal FOV at 16:9, in degrees.\nDefault keeps the authored view and scopes.", None), - ("VIEWMODEL FOV:", "display.viewmodel_fov", [("SAME", "0")] + [(str(n), str(n)) for n in range(80, 151, 5)], - "Weapon and hands horizontal FOV at 16:9, in degrees.\nSame keeps the original weapon view.", None), + ("VIEWMODEL FOV:", "display.viewmodel_fov", [("DEFAULT", "0")] + [(str(n), str(n)) for n in range(80, 151, 5)], + "Weapon and hands horizontal FOV at 16:9, in degrees.\nA number sets that FOV. Default keeps the original weapon view.", None), ("VIEWMODELS:", "display.viewmodel_visible", ON_OFF, "Draw first-person weapons, hands and attached\nvisuals. Gameplay and sound continue when off.", None), ], From 7ac597815828b20a681c24d71f445224014d9613 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:49:36 -0400 Subject: [PATCH 126/187] terminal_printf: take its arguments as a va_list terminal_printf started its variable arguments into a char *, which is what va_list is on the Xbox, 32-bit x86 and Apple silicon, but not on x86-64, where va_list is a one-element array of a register-save structure. On an Intel Mac (or under Rosetta) va_start wrote through an uninitialised pointer and vsnprintf read garbage, so the first terminal line printed with arguments crashed the game. Choosing an empty profile slot is such a line: "player profile begin editing" fails, as it does on the Xbox, and console_warning reports it through terminal_printf, so setting up a profile crashed on Intel Macs. (cherry picked from commit 024e41b8cb1553d7ad9b96233860848d49e5ad13) --- source/interface/terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/interface/terminal.c b/source/interface/terminal.c index ee9ca827..a055d5b0 100644 --- a/source/interface/terminal.c +++ b/source/interface/terminal.c @@ -347,7 +347,7 @@ void terminal_printf( ...) { real_argb_color default_terminal_printf_color; - char *arglist; + va_list arglist; va_start(arglist, format); From 744040c8704e759801168d14a65c79f8c0f397d6 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Sat, 3 Oct 2026 20:09:19 -0500 Subject: [PATCH 127/187] Lens flares out of view are not tested or drawn A structure BSP's lens flare markers are submitted for every visible cluster, and each gets an occlusion test every frame, including those behind the camera or off the screen, whose tests count no pixels (so they are never drawn). On the Xbox a test was nearly free; here each is a draw and a query of its own, and a level of many lights (Medical Block) tested about 460 a frame (debug.gpu_trace_heavy). A flare whose test could not reach the screen, by the frustum and a sphere taking in the test's offset point and size, is no longer submitted. (cherry picked from commit e020bbc58dd08b4bcd609d8cf561c94b87cf4580) --- source/rasterizer/rasterizer_lights.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/source/rasterizer/rasterizer_lights.c b/source/rasterizer/rasterizer_lights.c index 412d3840..6d76aa70 100644 --- a/source/rasterizer/rasterizer_lights.c +++ b/source/rasterizer/rasterizer_lights.c @@ -88,6 +88,7 @@ symbols in this file: #include "math/periodic_functions.h" #include "objects/widgets/widget_types.h" #include "render.h" +#include "render/render_cameras_internal.h" /* port: render_frustum_sphere_visible */ #include "scenario/scenario.h" #include "tag_files/tag_groups.h" #include "rasterizer_widgets.h" @@ -559,9 +560,16 @@ void rasterizer_lens_flare_submit( &camera_offset); camera_distance= dot_product3d(&global_window_parameters.camera.forward, &camera_offset); + /* port: none out of view either (behind the camera, off the + screen): its occlusion test would count no pixels, so it would not + be drawn, and a level of many lights (Halo PC's maps) tested + hundreds a frame, each a draw of its own (the sphere takes in the + test's offset point and size, rasterizer_lens_flares_submit_occlusion_tests) */ if ((parameters->definition->far_fade_distance==0.0f || camera_distancedefinition->far_fade_distance) && - (parameters->compressed_light_color&LENS_FLARE_LIGHT_COLOR_ALPHA_MASK)>0) + (parameters->compressed_light_color&LENS_FLARE_LIGHT_COLOR_ALPHA_MASK)>0 && + render_frustum_sphere_visible(&global_window_parameters.frustum, ¶meters->position, + 3.0f * parameters->definition->occlusion_radius)) { struct rasterizer_lens_flare_submit_parameters *lens_flare_parameters= lens_flare_parameters_get((short)local_lens_flare_count++); From d5dedf19e0cf145c1b7e0ab1b085159372026b04 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Sun, 4 Oct 2026 00:39:17 -0500 Subject: [PATCH 128/187] Rotational particles: don't read past a bitmap's last sequence Rotational sprites draw their edge-on view from the sequence after the state's. A particle bitmap on foundation has only one sequence, which halted the game in particle_system_render. Without that sequence the face-on sprite is drawn alone. (cherry picked from commit d436a0c0c3abaf069dd34e4b8d761b49569311e6) --- source/effects/particle_systems.c | 6 +++++- source/render/render_sprite.c | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/source/effects/particle_systems.c b/source/effects/particle_systems.c index b44abaf1..38c9c72b 100644 --- a/source/effects/particle_systems.c +++ b/source/effects/particle_systems.c @@ -1540,8 +1540,12 @@ static void particle_system_render( } bitmap = bitmap_group_get(state_definition->bitmaps.index); + /* port: rotational sprites use the sequence after the state's. Some + Custom Edition bitmaps don't have one (on foundation), so those + draw from the state's own sequence. */ if (type_definition->complex_sprite_render_mode == - _particle_system_type_complex_sprite_render_mode_rotational) + _particle_system_type_complex_sprite_render_mode_rotational && + state_definition->sequence_index + 1 < bitmap->sequences.count) { sequence_index = state_definition->sequence_index + 1; } diff --git a/source/render/render_sprite.c b/source/render/render_sprite.c index 963ad649..66baec47 100644 --- a/source/render/render_sprite.c +++ b/source/render/render_sprite.c @@ -617,6 +617,10 @@ void build_sprite_rotational( &axis_of_rotation) - quarter_circle; fraction = angle*angle/(quarter_circle*quarter_circle); fraction = PIN(fraction, 0.f, 1.f); + /* port: the edge-on sprite is in the next sequence, which some Custom + Edition bitmaps don't have; those always draw the face-on sprite */ + if (first_sequence_index+1 >= bitmap_group_get(data->bitmap_group_index)->sequences.count) + fraction = 0.f; if (fraction>0.05f) { From ff8fc7a654582cfc16d6c12a0432946bbbebbf50 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Sun, 4 Oct 2026 03:01:48 -0500 Subject: [PATCH 129/187] Network: log the Winsock error behind a failed send; the connect wait's select A send that fails with an error the transport doesn't name became endpoint_io with nothing to say why (a first join to a host that then failed). The error is logged now, once for each. The connect wait treats a select that times out as still connecting, and sets its timeout each time (select may leave what is left of it). (cherry picked from commit 5b02133ce00edcb7d9fd788db459a77cd98e9ac4) --- .../network/transport_endpoint_winsock.c | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/source/bungie_net/network/transport_endpoint_winsock.c b/source/bungie_net/network/transport_endpoint_winsock.c index c87e71ab..a95816ef 100644 --- a/source/bungie_net/network/transport_endpoint_winsock.c +++ b/source/bungie_net/network/transport_endpoint_winsock.c @@ -525,7 +525,17 @@ long write_endpoint( result = send(ep->socket, buffer, length, 0); if (result == INVALID_SOCKET) { - switch (WSAGetLastError()) + /* port: the Winsock error a send failed with, which the transport's + errors don't say (logged once for each different one) */ + static int logged_error; + int code = WSAGetLastError(); + + if (code != 0x2733 && code != logged_error) + { + error(_error_silent, "write_endpoint: Winsock error %d", code); + logged_error = code; + } + switch (code) { case 0x2733: ep->error = _transport_result_operation_would_block; @@ -1224,25 +1234,31 @@ short connect_endpoint( unsigned long timeout = system_milliseconds() + 10 * MILLISECONDS_PER_SECOND; struct timeval timeval; - timeval.tv_sec = 1; - timeval.tv_usec = 0; - do { fd_set writeable; fd_set failed; + long ready; + /* (set each time: select may leave what is left of it) */ + timeval.tv_sec = 1; + timeval.tv_usec = 0; writeable.fd_array[0] = ep->socket; writeable.fd_count = 1; failed.fd_array[0] = ep->socket; failed.fd_count = 1; - /* port: a connect that failed is in the error set (not - writeable, and its would-block left as it was) */ - if (select(1, NULL, &writeable, &failed, &timeval) > 0) + /* port: a failed connect is in the error set, and a select + that times out (0) means the connection is still being made */ + ready = select(1, NULL, &writeable, &failed, &timeval); + if (ready > 0) { error = failed.fd_count ? WSAECONNREFUSED : 0; } + else if (ready == 0) + { + error = WSAEWOULDBLOCK; + } else { error = WSAGetLastError(); From ab3c388c15212ee6eb7fc4624032c3469b820264 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Sun, 4 Oct 2026 03:24:47 -0500 Subject: [PATCH 130/187] Joining: a connection made is open, whatever came before it While searching for games the client wrote to its connection before it had a socket (Winsock error 10038); that marked the connection closed, and when the player then picked a game every write of the join request failed, so the first join to a host hung and gave up. Connecting now clears that, and any half-sent data. A second try worked only because it made a new connection. (cherry picked from commit 4b18b99af6d977707203b304d104c84fb23fecb4) --- source/networking/network_connection.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/source/networking/network_connection.c b/source/networking/network_connection.c index 6a058d75..7a7ef880 100644 --- a/source/networking/network_connection.c +++ b/source/networking/network_connection.c @@ -486,6 +486,15 @@ boolean network_connection_connect( return FALSE; } success = TRUE; + /* port: a connection made is open, whatever came before it. A write + made before connecting (while searching for games, to no socket yet) + marked it closed, and every write of the join request then failed. */ + SET_FLAG(connection->flags, _connection_closed_bit, FALSE); + SET_FLAG(connection->flags, _connection_going_stale_bit, FALSE); + if (connection->reliable_outgoing_queue) + { + circular_queue_reset(connection->reliable_outgoing_queue); + } if (connection->unreliable_endpoint) { From 712d1ebe3693114baa17ab407786250e84837985 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 04:05:55 -0400 Subject: [PATCH 131/187] Projectile trails: their bitmaps read before a weapon's first shot A trail's first draw asks the texture cache for its bitmap without waiting and is skipped until the read finishes, so a weapon's first shots could show no trail (the sniper rifle's most of all). A weapon created or readied now asks the cache, without waiting, for the bitmaps of its projectiles' contrails. Only the texture cache sees it: nothing in the game state or on the network changes. From pfista's halo-og (4e66af91), the source/items/weapons.c part only; that commit's spawn inventory and teleport prediction changes are not taken, as they change what a host and its clients exchange. Co-authored-by: Michael Pfister (cherry picked from commit 7fff24d4843758ddcb6468c9f1febecfbce68aa5) (cherry picked from commit 965f48f5c5e2063ba025766f1276467708e8112c) --- source/items/weapons.c | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/source/items/weapons.c b/source/items/weapons.c index d9108af4..ff1144f1 100644 --- a/source/items/weapons.c +++ b/source/items/weapons.c @@ -231,6 +231,12 @@ symbols in this file: #include "units/unit_definitions.h" #include "units/units.h" +#ifdef HALO_PORT_MAXIMUM_NETWORK_PLAYERS +#include "bitmaps/bitmap_group.h" +#include "cache/texture_cache.h" +#include "effects/contrail_definitions.h" +#endif + /* port/linux/game/pal_tags.c's */ short pal_tags_first_person_frames(long graph_index, short animation_index, short frames); @@ -572,6 +578,58 @@ void weapons_dispose( return; } +#ifdef HALO_PORT_MAXIMUM_NETWORK_PLAYERS +/* A trail's first draw requests its bitmap asynchronously and is skipped +until that read finishes. Warm only the projectile trails of a weapon that +has been created or readied, before its first shot. Keep ordinary +streaming nonblocking and leave the original trail tags and lifetime alone. */ +static void weapon_precache_projectile_trails( + long definition_index) +{ + if (definition_index != NONE) + { + struct weapon_definition *definition = weapon_definition_get(definition_index); + short trigger_index; + + for (trigger_index = 0; trigger_index < definition->weapon.triggers.count; trigger_index++) + { + struct weapon_trigger_definition *trigger = TAG_BLOCK_GET_ELEMENT( + &definition->weapon.triggers, trigger_index, struct weapon_trigger_definition); + + if (trigger->projectile.index != NONE) + { + struct projectile_definition *projectile = projectile_definition_get(trigger->projectile.index); + short attachment_index; + + for (attachment_index = 0; attachment_index < projectile->object.attachments.count; attachment_index++) + { + struct object_attachment_definition *attachment = TAG_BLOCK_GET_ELEMENT( + &projectile->object.attachments, attachment_index, struct object_attachment_definition); + + if (attachment->type.group_tag == CONTRAIL_DEFINITION_TAG && attachment->type.index != NONE) + { + struct contrail_definition *contrail = contrail_definition_get(attachment->type.index); + + if (contrail->bitmap.index != NONE) + { + struct bitmap_group *bitmap = bitmap_group_get(contrail->bitmap.index); + short bitmap_index; + + for (bitmap_index = 0; bitmap_index < bitmap->bitmaps.count; bitmap_index++) + { + _texture_cache_bitmap_get_hardware_format( + TAG_BLOCK_GET_ELEMENT(&bitmap->bitmaps, bitmap_index, struct bitmap_data), + FALSE, TRUE); + } + } + } + } + } + } + } +} +#endif + void weapon_place( long weapon_index, struct scenario_weapon_datum *scenario_weapon) @@ -603,6 +661,9 @@ void weapon_ready( struct weapon_datum* weapon = weapon_get(weapon_index); struct weapon_definition *weapon_definition = weapon_definition_get(weapon->definition_index); +#ifdef HALO_PORT_MAXIMUM_NETWORK_PLAYERS + weapon_precache_projectile_trails(weapon->definition_index); +#endif weapon_reset(weapon_index); weapon_set_state(weapon_index, _weapon_state_ready, TRUE); first_person_weapon_message_from_weapon(weapon_index, _first_person_weapon_message_ready); @@ -814,6 +875,9 @@ boolean weapon_new( trigger->idle_ticks = 127; } +#ifdef HALO_PORT_MAXIMUM_NETWORK_PLAYERS + weapon_precache_projectile_trails(weapon->definition_index); +#endif return TRUE; } From 794cbb92c51328a865dcaf463ad0e0e7b5f7eb18 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:54:58 -0400 Subject: [PATCH 132/187] Bitmap sampling: the compressed-block messages print the lod; an unknown format reads black bitmap_2d_get_pixel's out-of-range block messages printed a short where they say lod=%f, and a compressed format it can't decode returned an uninitialized pixel (release builds carry on past the assertion). Object lighting is its only caller. (cherry picked from commit e1169e05a374071e74b3da3d285762c45f1c2a77) (cherry picked from commit 9d827c0dd9164196da54b10a2fbfdd63bf72a0cd) --- source/bitmaps/bitmaps.c | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/source/bitmaps/bitmaps.c b/source/bitmaps/bitmaps.c index a56fe6a9..18464e2a 100644 --- a/source/bitmaps/bitmaps.c +++ b/source/bitmaps/bitmaps.c @@ -1128,18 +1128,6 @@ pixel32 bitmap_2d_get_pixel( x &= 3; y &= 3; - /* BUG (original, preserved for exact matching; admitted narrowly by the owner 2026-09-27 - no general - * varargs exception): both messages end "lod=%f" but January passes the short mipmap_index there - * (0x46c140 +0x2d2 movsx eax,[ebp-0x10] / +0x2e2 push eax, and +0x363 / +0x373; the same defect occurs - * in the August and September 2001 builds and the later /Od build). Stack at the csprintf call: six ints for - * the %d conversions, then mipmap_index, then display_assert's __FILE__ pointer pushed just before it. - * %f is the last conversion, so it consumes exactly those two pushed dwords (no later argument shifts). - * Passing an int where the format reads a double is undefined behaviour in C; the bound below is a - * property of this compiler, CRT and linked image, not of the source: for every mipmap_index value with - * January's __FILE__ address as the high dword the double is a tiny positive normal and the halt message - * shows "lod=0.000000" (at most 182 characters plus NUL in the 256-byte buffer; January's formatter has no x87 code). - * display_assert then returns into an unconditional system_exit, which never returns (halt_and_catch_fire). - * A corrected build passes lod. */ match_vassert( "c:\\halo\\SOURCE\\bitmaps\\bitmaps.c", 0x2A0, @@ -1153,7 +1141,7 @@ pixel32 bitmap_2d_get_pixel( (short)bitmap->mipmap_count, fast_ftol((real)width * point->x - 0.5f) % width, fast_ftol((real)height * point->y - 0.5f) % height, - mipmap_index)); + lod)); match_vassert( "c:\\halo\\SOURCE\\bitmaps\\bitmaps.c", 0x2A9, @@ -1167,7 +1155,7 @@ pixel32 bitmap_2d_get_pixel( (short)bitmap->mipmap_count, fast_ftol((real)width * point->x - 0.5f) % width, fast_ftol((real)height * point->y - 0.5f) % height, - mipmap_index)); + lod)); switch (bitmap->format) { @@ -1201,15 +1189,10 @@ pixel32 bitmap_2d_get_pixel( 0x2B7, FALSE, "### ERROR unsupported bitmap format"); + pixel = 0; break; } - /* BUG (original, preserved for exact matching; admitted narrowly by the owner 2026-09-27): the - * unsupported-format default arm leaves pixel unassigned and January returns it after the fatal - * assertion (0x46c140 +0x3ed mov eax,[ebp+8]). In this image the assertion is followed by an - * unconditional system_exit, which never returns (halt_and_catch_fire loops or calls _exit on re-entry). - * The uninitialised return expression would read an indeterminate value if the halt returned, but is - * not executed on this path. A corrected build assigns pixel in that arm. */ return pixel; } From 84dd8a2c32e118d20579f043018ab7bb68948501 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:53:40 -0400 Subject: [PATCH 133/187] Particles: a fully culled group stores no NaN in its shader The average radius of a group whose particles were all culled divided zero by zero, leaving a NaN in the effect shader's secondary map radius. (cherry picked from commit 11128fd78ade85c50a9a3a5a292bd2bb33bebf30) (cherry picked from commit b52d79541dc2143de41943367542ae63ad4f384f) --- source/render/render_particles.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/source/render/render_particles.c b/source/render/render_particles.c index 244d5daf..ce777f27 100644 --- a/source/render/render_particles.c +++ b/source/render/render_particles.c @@ -392,12 +392,9 @@ void render_particles( rendered_particle++; } - /* BUG (original): a fully culled group divides zero by zero. - * A corrected non-matching build would store 0.0f when - * built_particle_count is zero. */ ((struct shader_effect_definition *)sprite_data.shader)-> - secondary_map_radius = - total_radius / built_particle_count; + secondary_map_radius = built_particle_count ? + total_radius / built_particle_count : 0.0f; build_sprites_end(&sprite_data); } } From 953b0298d4187a05a848c35e64f1b8ef929f6f89 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:52:42 -0400 Subject: [PATCH 134/187] Scripts: boolean and short results are whole values, not part uninitialized Seven of the script engine's conversions and comparisons wrote only the low byte or word of a long and returned the rest uninitialized, which the scripts' stacks and globals, and so the saved games, carried along. Every reader takes only the low byte or word (the script type sizes), so the scripts' results are unchanged. (cherry picked from commit bb239e2207668593679eea5de45eda8e5f635b34) (cherry picked from commit d6811ee3264b2082ba6b943087ce26293f59768a) --- source/hs/hs_runtime.c | 53 ++++++------------------------------------ 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/source/hs/hs_runtime.c b/source/hs/hs_runtime.c index 8053ac97..75462f60 100644 --- a/source/hs/hs_runtime.c +++ b/source/hs/hs_runtime.c @@ -1658,13 +1658,7 @@ static long hs_long_to_boolean( { long result; - /* BUG: only the low byte of this uninitialised long is written, and the whole long is returned; its - upper three bytes are indeterminate. January homes result in the argument slot, so they are bits 8-31 - of n; where hs_string_to_boolean inlines this function they come from its 'push ecx' slot, which holds - hs_cast's table index 254 (0x000000fe), so they are zero there. No January consumer reads a boolean - cell beyond its low byte, but hs_return and the pass-through forms copy the whole long into hs thread - stacks and hs globals, so these bytes reach the saved-game CRC and the save files - (game_state_write_to_persistent_storage, game_state_write_to_file, game_state_write_core). */ + result = 0; *(boolean *)&result = n==0; return result; @@ -1675,12 +1669,7 @@ static long hs_short_to_boolean( { long result; - /* BUG: only the low byte of this uninitialised long is written, and the whole long is returned; its - upper three bytes are indeterminate. January homes result in the argument slot, so they are bits 8-31 - of s. No January consumer reads a boolean cell beyond its low byte, but hs_return and the pass-through - forms copy the whole long into hs thread stacks and hs globals, so these bytes reach the saved-game CRC - and the save files (game_state_write_to_persistent_storage, game_state_write_to_file, - game_state_write_core). */ + result = 0; *(boolean *)&result = (short)s==0; return result; @@ -1733,12 +1722,7 @@ static long hs_real_to_short( { long result; - /* BUG: only the low word of this uninitialised long is written, and the whole long is returned; its - upper word is indeterminate. January homes result in the argument slot, so it is the upper word of r's - bit pattern. No January consumer reads a short cell beyond its low word, but hs_return and the - pass-through forms copy the whole long into hs thread stacks and hs globals, so these bytes reach the - saved-game CRC and the save files (game_state_write_to_persistent_storage, game_state_write_to_file, - game_state_write_core). */ + result = 0; *(short *)&result = (short)*(real *)&r; return result; @@ -1755,12 +1739,7 @@ static long hs_long_to_short( { long result; - /* BUG: only the low word of this uninitialised long is written, and the whole long is returned; its - upper word is indeterminate. January homes result in the argument slot, so it is the upper word of l - (January returns l unchanged). No January consumer reads a short cell beyond its low word, but - hs_return and the pass-through forms copy the whole long into hs thread stacks and hs globals, so these - bytes reach the saved-game CRC and the save files (game_state_write_to_persistent_storage, - game_state_write_to_file, game_state_write_core). */ + result = 0; *(short *)&result = (short)l; return result; @@ -1999,13 +1978,7 @@ void hs_evaluate_equality( if (function_index==_hs_function_not_equal) equal = !equal; - /* BUG: only the low byte of this uninitialised long is written, and the whole long is passed to - hs_return; its upper three bytes are indeterminate. January reuses the function_index argument - slot, which parameter_types has just filled with the argument type twice, so the long is - (type<<16)|equal. No January consumer reads a boolean cell beyond its low byte, but hs_return and - the pass-through forms copy the whole long into hs thread stacks and hs globals, so these bytes - reach the saved-game CRC and the save files (game_state_write_to_persistent_storage, - game_state_write_to_file, game_state_write_core). */ + result_long = 0; *(boolean *)&result_long = equal; hs_return(thread_index, result_long); } @@ -2051,13 +2024,7 @@ void hs_evaluate_inequality( break; } - /* BUG: only the low byte of this uninitialised long is written, and the whole long is passed to - hs_return; its upper three bytes are indeterminate. January reuses the function_index argument - slot, where it has just spilled value1, so they are bits 8-31 of value1's bit pattern. No January - consumer reads a boolean cell beyond its low byte, but hs_return and the pass-through forms copy - the whole long into hs thread stacks and hs globals, so these bytes reach the saved-game CRC and - the save files (game_state_write_to_persistent_storage, game_state_write_to_file, - game_state_write_core). */ + result_long = 0; *(boolean *)&result_long = comparison; hs_return(thread_index, result_long); } @@ -2107,13 +2074,7 @@ void hs_evaluate_logical( } else { - /* BUG: only the low byte of this uninitialised long is written, and the whole long is passed to - hs_return; its upper three bytes are indeterminate. January reuses the initialize argument slot, - so they are the upper bytes of the dword hs_thread_main pushed for initialize (stack residue from - hs_thread_main's frame). No January consumer reads a boolean cell beyond its low byte, but - hs_return and the pass-through forms copy the whole long into hs thread stacks and hs globals, so - these bytes reach the saved-game CRC and the save files (game_state_write_to_persistent_storage, - game_state_write_to_file, game_state_write_core). */ + result_long = 0; *(boolean *)&result_long = *result; hs_return(thread_index, result_long); From 07bb9500644c57c35200af35d9ec2f25c3c8a3cd Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Tue, 6 Oct 2026 14:43:00 -0500 Subject: [PATCH 135/187] Co-op: the scripts' look tests pass for any player objects_can_see_object and objects_can_see_flag on player0 tested one player only, the first with a unit, so the Pillar of Autumn tutorial's panels (look at each light in turn) never lit for anyone else. In network co-op a test of the players now passes when any player looks, as volume_test_objects_all already does. (cherry picked from commit 1e8e0f9affe212d3e30817b768d3124027b678f7) --- source/hs/hs_library_external.c | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/source/hs/hs_library_external.c b/source/hs/hs_library_external.c index 0e210b0a..73dde5cf 100644 --- a/source/hs/hs_library_external.c +++ b/source/hs/hs_library_external.c @@ -152,6 +152,10 @@ void hs_object_create_anew( short object_name_index); void hs_object_destroy( long object_index); +boolean hs_unit_can_see_flag( + long unit_index, + short cutscene_flag_index, + real degrees); /* ---------- globals */ @@ -274,6 +278,32 @@ boolean hs_unit_can_see_object( return result; } +/* port: in network co-op a test of the players (the scripts' player0) +passes for any player, as volume_test_objects_all does (coop_scripts.c), so +the a10 tutorial's panels light for whoever looks at them: whether any +player's unit sees the object, or with object_index NONE the cutscene flag */ +static boolean hs_any_player_can_see( + long object_index, + short cutscene_flag_index, + real degrees) +{ + struct data_iterator iterator; + struct player_datum *player; + + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + if (player->unit_index != NONE && + (object_index != NONE ? hs_unit_can_see_object(player->unit_index, object_index, degrees) : + hs_unit_can_see_flag(player->unit_index, cutscene_flag_index, degrees))) + { + return TRUE; + } + } + + return FALSE; +} + boolean hs_objects_can_see_object( long object_list_index, long object_index, @@ -283,6 +313,8 @@ boolean hs_objects_can_see_object( long unit_index; boolean result; + if (object_index != NONE && coop_scripts_any_player_will_do(object_list_index)) + return hs_any_player_can_see(object_index, NONE, degrees); result = FALSE; unit_index = object_list_get_first(object_list_index, &reference_index); while (unit_index != NONE) @@ -332,6 +364,8 @@ boolean hs_objects_can_see_flag( long reference_index; long unit_index; + if (coop_scripts_any_player_will_do(object_list_index)) + return hs_any_player_can_see(NONE, cutscene_flag_index, degrees); unit_index = object_list_get_first(object_list_index, &reference_index); while (unit_index != NONE) { From 767a4edb0be1f2aa5bda2ca886ae913d680c5498 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Tue, 6 Oct 2026 18:26:56 -0500 Subject: [PATCH 136/187] Netcode: carry units' integrated lights in the object states A unit's light is only toggled by presses, so a client that missed one, or joined after it, kept a vehicle's (or biped's) light the wrong way and unit_get_current_flashlight_state with it. The host's states now carry it, with a bit saying so, so older hosts' zeros are not read as "off". What the client's own player drives is left to its own presses. (cherry picked from commit 0392b4639e663faa632c928301ff89a425bb0cd0) --- port/linux/game/network_objects.c | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/port/linux/game/network_objects.c b/port/linux/game/network_objects.c index 1da5e4d5..f7de0259 100644 --- a/port/linux/game/network_objects.c +++ b/port/linux/game/network_objects.c @@ -243,6 +243,10 @@ enum /* (struct distributed_object_state, the host's) the host has the vehicle where the client it goes to had it at the tick in time */ _distributed_object_predicted_bit, + /* (struct distributed_object_state) a unit's integrated light is on, and + the host sent it at all: older builds leave both 0, which is not "off" */ + _distributed_object_light_sent_bit, + _distributed_object_light_on_bit, }; struct distributed_object_change @@ -672,6 +676,18 @@ static boolean distributed_vehicle_unsteered( unit_get(vehicle->unit.driver_object_index)->unit.player_index == NONE; } +/* whether this machine's player drives the unit: it is their biped, or the +vehicle they drive */ +static boolean distributed_unit_driven_here( + long unit_index) +{ + struct unit_datum *unit = unit_get(unit_index); + + if (unit->unit.driver_object_index != NONE) + unit = unit_get(unit->unit.driver_object_index); + return unit->unit.player_index != NONE && distributed_player_is_local(unit->unit.player_index); +} + /* Whether a client puts its copy exactly where the host has it, rather than closing on it: an unsteered vehicle, and anything in a co-op cutscene (where it keeps up with the host's camera). */ @@ -807,6 +823,12 @@ static void distributed_state_from_object( SET_FLAG(state->flags, _distributed_object_at_rest_bit, TEST_FLAG(object->object.flags, _object_at_rest_bit)); SET_FLAG(state->flags, _distributed_object_dead_bit, TEST_FLAG(_object_mask_unit, object->object.type) && TEST_FLAG(object->object.damage_flags, _object_dead_bit)); + if (TEST_FLAG(_object_mask_unit, object->object.type)) + { + SET_FLAG(state->flags, _distributed_object_light_sent_bit, TRUE); + SET_FLAG(state->flags, _distributed_object_light_on_bit, + TEST_FLAG(unit_get(object_index)->unit.flags, _unit_integrated_light_on_bit)); + } state->position = object->object.position; distributed_vector_pack(&object->object.forward, DISTRIBUTED_UNIT_SCALE, &state->forward); distributed_vector_pack(&object->object.up, DISTRIBUTED_UNIT_SCALE, &state->up); @@ -2496,6 +2518,17 @@ void network_objects_handle_states( if (!distributed_object_index_valid(state->object_index) || !network_objects_client_has(state->object_index)) continue; object = object_get(state->object_index); + /* (the unit's light as the host has it: only presses toggle it here, + so one missed, or made before this machine joined, left it wrong for + good. Not for what this machine's player drives: their press lands + here first) */ + if (TEST_FLAG(state->flags, _distributed_object_light_sent_bit) && + TEST_FLAG(_object_mask_unit, object->object.type) && + !distributed_unit_driven_here(state->object_index)) + { + SET_FLAG(unit_get(state->object_index)->unit.flags, _unit_integrated_light_on_bit, + TEST_FLAG(state->flags, _distributed_object_light_on_bit)); + } if (TEST_FLAG(state->flags, _distributed_object_dead_bit)) { distributed_client_dead_biped(state->object_index); From 65e690321d530c3f94ba4a21faffbf6c6a912c8b Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Thu, 8 Oct 2026 14:16:54 -0500 Subject: [PATCH 137/187] Log and error messages print what their formats ask for Arguments a format never read are dropped, missing ones passed (the migration speech type, DirectSound's result), a real printed as %f, a target AI's name printed for it rather than the source's twice, and two messages built from saved-game names printed through %s. (cherry picked from commit dbcf4ab91c2e42c19f57ca436f1d4f2b41964582) --- source/ai/actor_looking.c | 6 +---- source/ai/ai_script.c | 25 +++---------------- source/bitmaps/bitmap_utilities.c | 12 +++------ source/physics/breakable_surfaces.c | 7 +----- .../rasterizer/xbox/rasterizer_xbox_profile.c | 12 +-------- source/saved games/saved_game_files.c | 7 +----- source/sound/sound_dsound_xbox.c | 7 +----- 7 files changed, 11 insertions(+), 65 deletions(-) diff --git a/source/ai/actor_looking.c b/source/ai/actor_looking.c index 6b3f6dcc..93de6c2d 100644 --- a/source/ai/actor_looking.c +++ b/source/ai/actor_looking.c @@ -1116,14 +1116,10 @@ static boolean actor_look_decode_direction( switch (specification->type) { case _direction_specification_movement: - /* BUG (preserved for exact matching): January loads the point's - * z field twice (actor + 0x514). A corrected diagnostic should - * print the y field as its second point component. - */ sprintf(temporary, "denormalized %f: %smoving (p%f %f %f) (v%f %f %f)", magnitude, actor->control.moving ? "" : "not ", actor->control.moving_towards_point.x, - actor->control.moving_towards_point.z, + actor->control.moving_towards_point.y, actor->control.moving_towards_point.z, actor->control.moving_towards_vector.i, actor->control.moving_towards_vector.j, diff --git a/source/ai/ai_script.c b/source/ai/ai_script.c index d46d1929..3fd3ab39 100644 --- a/source/ai/ai_script.c +++ b/source/ai/ai_script.c @@ -1563,11 +1563,7 @@ void ai_scripting_follow_target_ai( char target_ai_name[512]; ai_index_to_string(ai_reference, global_scenario_get(), ai_name, sizeof(ai_name)); - /* BUG (preserved for exact matching): January formats ai_reference again instead of - * target_ai_reference, so the log prints the first name twice. A corrected build should - * pass target_ai_reference here. - */ - ai_index_to_string(ai_reference, global_scenario_get(), target_ai_name, sizeof(target_ai_name)); + ai_index_to_string(target_ai_reference, global_scenario_get(), target_ai_name, sizeof(target_ai_name)); error(_error_silent, "%s: ai_follow_target_ai %s %s", hs_runtime_get_executing_thread_name(), ai_name, target_ai_name); } @@ -2725,28 +2721,13 @@ void ai_scripting_migrate_and_speak( global_scenario_get(), target_name, sizeof(target_name)); - /* BUG (preserved for exact matching): the format has four %s conversions - * but January supplies only three values, so the fourth conversion reads - * past the end of the argument list. - * Evidence: January's .rdata holds the 34-byte literal - * "%s: ai_migrate_and_speak %s %s %s", which has four conversions, while - * its call site cleans 0x14 bytes - five dwords, i.e. only three values - - * and loads speech_type only afterwards, at +0x7d. The same defect - * survives unrepaired in the 2020 build. - * Consequence: with January's prologue the fourth slot lands on the saved - * EDI home, so vsprintf dereferences the caller's entry EDI as a char *. - * The branch is gated on ai_debug.print_migration or - * ai_debug.print_scripting. Both default to false, but they are exposed as - * the HaloScript globals ai_print_migration and ai_print_scripting, so a - * scenario script or the debug console can reach this path at runtime. - * A corrected build should pass speech_type as the fourth value. - */ error( _error_silent, "%s: ai_migrate_and_speak %s %s %s", hs_runtime_get_executing_thread_name(), source_name, - target_name); + target_name, + speech_type); } if (_stricmp(speech_type, "advance") == 0) diff --git a/source/bitmaps/bitmap_utilities.c b/source/bitmaps/bitmap_utilities.c index f4e0662e..9c73121f 100644 --- a/source/bitmaps/bitmap_utilities.c +++ b/source/bitmaps/bitmap_utilities.c @@ -1178,11 +1178,7 @@ static void bitmap_cm_smooth( match_assert("c:\\halo\\SOURCE\\bitmaps\\bitmap_utilities.c", 0x354, bitmap->type==_bitmap_type_cube_map); match_assert("c:\\halo\\SOURCE\\bitmaps\\bitmap_utilities.c", 0x355, filter_coefficients); - /* BUG (preserved for exact matching): January passes a newline as an - * unused vararg even though the format has no conversion. A corrected - * build should append the newline to the format string instead. - */ - fprintf(stdout, "### WARNING tried to smooth a cube map", "\r\n"); + fprintf(stdout, "### WARNING tried to smooth a cube map\r\n"); fflush(stdout); return; @@ -1365,8 +1361,7 @@ static void bitmap_3d_sharpen( match_assert("c:\\halo\\SOURCE\\bitmaps\\bitmap_utilities.c", 0x3E4, positive_table); match_assert("c:\\halo\\SOURCE\\bitmaps\\bitmap_utilities.c", 0x3E5, negative_table); - /* BUG (preserved for exact matching): see bitmap_cm_smooth. */ - fprintf(stdout, "### WARNING tried to sharpen a 3d bitmap", "\r\n"); + fprintf(stdout, "### WARNING tried to sharpen a 3d bitmap\r\n"); fflush(stdout); return; @@ -1383,8 +1378,7 @@ static void bitmap_cm_sharpen( match_assert("c:\\halo\\SOURCE\\bitmaps\\bitmap_utilities.c", 0x3F5, positive_table); match_assert("c:\\halo\\SOURCE\\bitmaps\\bitmap_utilities.c", 0x3F6, negative_table); - /* BUG (preserved for exact matching): see bitmap_cm_smooth. */ - fprintf(stdout, "### WARNING tried to sharpen a cube map", "\r\n"); + fprintf(stdout, "### WARNING tried to sharpen a cube map\r\n"); fflush(stdout); return; diff --git a/source/physics/breakable_surfaces.c b/source/physics/breakable_surfaces.c index 508cb3ba..7cc05f41 100644 --- a/source/physics/breakable_surfaces.c +++ b/source/physics/breakable_surfaces.c @@ -272,12 +272,7 @@ void breakable_surface_damage_area_of_effect( if (cutoff_radius > 4.0f) { - /* BUG (preserved for exact matching): %d is given a real, which the variadic call - * passes as a double (January 0x536430 +0x73..+0x79 fstp qword [esp]), so the - * message prints the double's low word. Reached for damaging area-of-effect damage - * with a cutoff radius above 4 while breakable surfaces are enabled. A corrected - * build should use %f. Source-policy approval pending (2026-09-27 audit). */ - error(_error_silent, "WARNING: area of effect breakable surface damage with radius %d", cutoff_radius); + error(_error_silent, "WARNING: area of effect breakable surface damage with radius %f", cutoff_radius); } /* port: no more than the globals hold (MAXIMUM_BREAKABLE_SURFACES_PER_MAP: diff --git a/source/rasterizer/xbox/rasterizer_xbox_profile.c b/source/rasterizer/xbox/rasterizer_xbox_profile.c index 3f528298..d1249fab 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_profile.c +++ b/source/rasterizer/xbox/rasterizer_xbox_profile.c @@ -630,17 +630,7 @@ static void profile_assert( } else { - /* BUG (preserved for exact matching): January pushes the same two - * varargs (profile, message) in both branches (target push and - * relocation order; the later /Od build at 0x8004c0 does the same), so - * this format's %s consumes the NONE profile value, not the message. - * A corrected build should pass only message here. - */ - error( - _error_silent, - "### PROFILE: %s -- tell Bernie!", - profile, - message); + error(_error_silent, "### PROFILE: %s -- tell Bernie!", message); } rasterizer_profile_error_count++; diff --git a/source/saved games/saved_game_files.c b/source/saved games/saved_game_files.c index c075feaf..c1aa125d 100644 --- a/source/saved games/saved_game_files.c +++ b/source/saved games/saved_game_files.c @@ -1883,14 +1883,9 @@ static void enumerate_memory_units( find_data.szSaveGameName, find_data.wfd.cFileName); message[MAXIMUM_FILENAME_LENGTH] = 0; - /* BUG (preserved for exact matching): the message embeds the save's display name - * and file name and is passed as the format (January 0x5b4b00 +0x263 pushes the - * converted text as error's format), so a '%' in either is taken as a conversion. - * Reached for a saved-game directory holding neither blam.sav nor blam.lst; whether - * such a directory occurs is not shown. A corrected build should pass the text - * through "%s". Source-policy approval pending (2026-09-27 audit). */ error( _error_silent, + "%s", wide_to_ascii(message, (char *)message, sizeof(message))); file.type = NONE; } diff --git a/source/sound/sound_dsound_xbox.c b/source/sound/sound_dsound_xbox.c index 91f7b23e..7db31f1e 100644 --- a/source/sound/sound_dsound_xbox.c +++ b/source/sound/sound_dsound_xbox.c @@ -2936,12 +2936,7 @@ static void dsound_error( break; } - /* BUG (preserved for exact matching): the format has three conversions but January - * passes two values (0x5b93e0 +0x93..+0xa2), so "#%d" prints the next stack word. - * Reached on every DirectSound failure reported through dsound_error. A corrected - * build should pass result as the third value. Source-policy approval pending - * (2026-09-27 audit). */ - error(_error_silent, "DirectSound: '%s' (%s#%d)", message, result_name); + error(_error_silent, "DirectSound: '%s' (%s#%d)", message, result_name, (int)result); return; } From e9703057244d035d0088ed41a17d6df3d19ce6de Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Thu, 8 Oct 2026 14:16:54 -0500 Subject: [PATCH 138/187] Profiler: frame descriptions stay inside their buffer Each of the 25 appends now goes through profile_append, which bounds and terminates; a 150-tick frame could run past the 512-byte buffer. (cherry picked from commit ecba1a0cecc7ff5d60d26654bdd50b7330d43f65) --- source/cseries/profile.c | 88 +++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/source/cseries/profile.c b/source/cseries/profile.c index 0ef43da2..0437d140 100644 --- a/source/cseries/profile.c +++ b/source/cseries/profile.c @@ -276,6 +276,7 @@ symbols in this file: #include "main/main.h" #include "game/players.h" +#include #include /* ---------- constants */ @@ -1302,6 +1303,26 @@ static void profile_timesection_inherit( return; } +/* (never past maximum_length, and always terminated) */ +static void profile_append( + char *string, + short maximum_length, + char const *format, + ...) +{ + long length = csstrlen(string); + va_list arguments; + + if (length >= maximum_length - 1) + return; + va_start(arguments, format); + _vsnprintf(string + length, maximum_length - length - 1, format, arguments); + va_end(arguments); + string[maximum_length - 1] = 0; + + return; +} + static void profile_describe_frame( struct profile_frame *frame, char *string, @@ -1312,14 +1333,9 @@ static void profile_describe_frame( short game_tick_index; short window_index; - /* BUG (preserved for exact matching): January repeatedly appends with - * maximum_length-strlen without handling CRT truncation or exhaustion. - * A 512-byte dump buffer can be exhausted by the legal 150-tick record. - * A corrected build should bound appends and guarantee NUL termination. - */ csstrcpy(string, ""); - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), + profile_append(string, maximum_length, "frame %5d vbl %5I64d tot%6.2f", frame->frame_index, frame->vertical_blank_index, @@ -1329,113 +1345,113 @@ static void profile_describe_frame( { if (global_frame_rate_throttle) { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "(lost%3d)", frame->lapsed_frames); + profile_append(string, maximum_length, "(lost%3d)", frame->lapsed_frames); } else { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "(free%3d)", frame->lapsed_frames); + profile_append(string, maximum_length, "(free%3d)", frame->lapsed_frames); } } else if (frame->lapsed_msec>0) { if (global_frame_rate_throttle) { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "(l.%3dms)", frame->lapsed_msec); + profile_append(string, maximum_length, "(l.%3dms)", frame->lapsed_msec); } else { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "(f.%3dms)", frame->lapsed_msec); + profile_append(string, maximum_length, "(f.%3dms)", frame->lapsed_msec); } } else { if (frame->lapsed_msec_valid) { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "(slowed) "); + profile_append(string, maximum_length, "(slowed) "); } else { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "(synced) "); + profile_append(string, maximum_length, "(synced) "); } } if (frame->idle.frame_total>0.0f) { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "idle%6.2f ", frame->idle.frame_total); + profile_append(string, maximum_length, "idle%6.2f ", frame->idle.frame_total); } else { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), " ", frame->idle.frame_total); + profile_append(string, maximum_length, " "); } game_tick_display_count = MAX(frame->game_tick_count, 8); - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "game%2d ", frame->game_tick_count); + profile_append(string, maximum_length, "game%2d ", frame->game_tick_count); - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), " ("); + profile_append(string, maximum_length, " ("); for (game_tick_index = 0; game_tick_indexgame_tick_count) { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "%6.2f%s", + profile_append(string, maximum_length, "%6.2f%s", frame->game_ticks[game_tick_index].frame_total, game_tick_indexwindow_count, local_player_count()+1); - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), " render%6.2f", frame->render.total); + profile_append(string, maximum_length, " render%6.2f", frame->render.total); - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), " ("); + profile_append(string, maximum_length, " ("); for (window_index = 0; window_indexwindow_count) { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "%s%6.2f%s", + profile_append(string, maximum_length, "%s%6.2f%s", frame->window_ids[window_index] ? "p" : "n", frame->windows[window_index].frame_total, window_indexstall.frame_total>0.0f) { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "stall%6.2f ", frame->stall.frame_total); + profile_append(string, maximum_length, "stall%6.2f ", frame->stall.frame_total); } else { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), " ", frame->stall.frame_total); + profile_append(string, maximum_length, " "); } if (frame->texture.frame_total>0.0f) { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "tex%6.2f ", frame->texture.frame_total); + profile_append(string, maximum_length, "tex%6.2f ", frame->texture.frame_total); } else { - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), " ", frame->texture.frame_total); + profile_append(string, maximum_length, " "); } - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "r-misc %6.2f ", frame->render.frame_total); + profile_append(string, maximum_length, "r-misc %6.2f ", frame->render.frame_total); - _snprintf(string+csstrlen(string), maximum_length-csstrlen(string), "f-misc %6.2f ", frame->frame.frame_total); + profile_append(string, maximum_length, "f-misc %6.2f ", frame->frame.frame_total); csstrncat(string+csstrlen(string), frame->lapsed_reason, maximum_length-csstrlen(string)); @@ -1661,17 +1677,7 @@ int compare_profile_sections( default: match_assert("c:\\halo\\SOURCE\\cseries\\profile.c", 844, !"unreachable"); - /* BUG (original, preserved for exact matching): this arm leaves result - * unassigned, and January returns it after the fatal assertion: 0x47e840 +0x61 mov eax,[ebp+8] - * reads the dead first-parameter home. The Sept-25-2001 build is identical; the Aug-15-2001 - * build reads its uninitialised [ebp-4] slot the same way. The later /Od+/RTC build attests the - * uninitialised declaration: its single exit calls _RTC_UninitUse("result"). - * The arm is unreachable in defined execution. compare_type is written only by profile_dump, - * after its sort_mode range assertion; January's two profile_dump callers pass 0/1 and 2; and - * each of the NUMBER_OF_PROFILE_SORT_MODES (3) modes has a case above that assigns result. - * Were the arm entered, display_assert returns into an unconditional system_exit, which never - * returns: halt_and_catch_fire loops, or calls exit() on re-entry. So the uninitialised return - * is not executed in January. A corrected build assigns result in this arm. */ + result = 0; break; } } From 60cd1254d6e16acd7bf93342351aa6cf331ba94f Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Thu, 8 Oct 2026 14:16:54 -0500 Subject: [PATCH 139/187] Fix the decompiled code's latent bugs, and say plainly what is kept Uninitialised locals get values (debug vertex, datastore result, index buffer, map tool direction, AI entry points, escape alignment, grenade aim from a vehicle, game options descriptions, unreachable switch arms), the AI's squad and platoon records move whole, a speech cause point is read, the geosphere's out-of-memory path frees itself, a console cond clause without a result is refused, and the crash reporter rejects truncated map files. Gameplay the original game has, kept so it plays the same, is noted in a line rather than a page. (cherry picked from commit 75afc4a1e57af2ba697e8be0e33b2afeee7639f8) --- source/ai/action_vehicle.c | 13 +---- source/ai/actions.c | 9 +-- source/ai/actor_combat.c | 8 +-- source/ai/actor_perception.c | 43 ++------------ source/ai/actors.c | 7 +-- source/ai/ai.c | 12 +--- source/ai/ai_communication.c | 5 +- source/ai/path.c | 9 +-- source/ai/path_smoothing.c | 9 +-- source/ai/path_structure_bsp.c | 34 ++--------- source/cseries/stack_walk_windows.c | 58 +++++-------------- source/effects/effects.c | 11 +--- source/game/game_engine.c | 7 +-- source/game/game_engine_ctf.c | 5 -- source/hs/hs_compile.c | 14 +---- source/interface/hud_weapon.c | 5 +- .../ui_widget_game_data_input_functions.c | 15 +---- source/items/projectiles.c | 4 +- source/math/geometry.c | 12 +--- source/rasterizer/rasterizer.c | 8 +-- .../xbox/rasterizer_xbox_hardware_geometry.c | 8 +-- source/structures/leaf_map.c | 10 ---- source/tag_files/files.c | 9 +-- source/tool/connected_geometry.c | 14 +---- source/units/unit_dialogue.c | 8 +-- 25 files changed, 54 insertions(+), 283 deletions(-) diff --git a/source/ai/action_vehicle.c b/source/ai/action_vehicle.c index 26b7b71a..ede538e1 100644 --- a/source/ai/action_vehicle.c +++ b/source/ai/action_vehicle.c @@ -650,9 +650,9 @@ static short action_vehicle_find_impromptu_seat( struct unit_datum *vehicle = unit_get(vehicle_index); struct unit_definition *vehicle_definition = unit_definition_get(vehicle->definition_index); - real_point3d best_entry_point; - real_vector3d best_entry_facing; - real_point3d best_hint_point; + real_point3d best_entry_point = { 0 }; + real_vector3d best_entry_facing = { 0 }; + real_point3d best_hint_point = { 0 }; long best_seat_index = NONE; real best_seat_weight = 0.0f; long seat_index; @@ -688,13 +688,6 @@ static short action_vehicle_find_impromptu_seat( } } - /* BUG (preserved for exact matching): when no seat qualifies, best_seat_index stays - * NONE and the three copies below store the never-assigned best_* locals (January - * 0x40a1b0 +0xe2..+0x11d copies them with plain dword moves). Reachable whenever no - * seat qualifies; the only caller, action_vehicle_setup_impromptu, reads the outputs - * only when the returned seat index is not NONE. A corrected build should copy them - * only when best_seat_index != NONE. Source-policy approval pending (2026-09-27 audit). - */ if (entry_point) { *entry_point = best_entry_point; diff --git a/source/ai/actions.c b/source/ai/actions.c index 8d8e32f7..7c88974a 100644 --- a/source/ai/actions.c +++ b/source/ai/actions.c @@ -4011,7 +4011,7 @@ static boolean actor_action_find_escape_from_danger( boolean result = FALSE; boolean selected_is_ledge = FALSE; real evade_distance = unit_definition->unit.dive_distance; - real_vector2d alignment_vector; + real_vector2d alignment_vector = *global_forward2d; match_assert( "c:\\halo\\SOURCE\\ai\\actions.c", @@ -4175,13 +4175,6 @@ static boolean actor_action_find_escape_from_danger( system_exit(-1); } - /* BUG (preserved for exact matching): alignment_vector is assigned only when the - * unit's dive_distance is positive; otherwise it is stored below unassigned (January - * 0x40c970 +0x386..+0x38f). Reachable for any unit whose dive distance is not - * positive; escape_direction is then NONE, and the only caller, - * actor_action_handle_danger_avoidance, leaves that block without reading the vector. - * A corrected build should store a defined vector on that path. Source-policy - * approval pending (2026-09-27 audit). */ *escape_direction_reference = escape_direction; *escape_distance_reference = evade_distance; *escape_is_ledge_reference = selected_is_ledge; diff --git a/source/ai/actor_combat.c b/source/ai/actor_combat.c index 674af1b2..819e6269 100644 --- a/source/ai/actor_combat.c +++ b/source/ai/actor_combat.c @@ -1572,11 +1572,11 @@ long actor_aim_grenade( actor_combat_reaim_grenade(actor_index, origin); + aim_vector = actor->control.grenade_current_aim_vector; if (actor->input.vehicle_index == NONE) { real_vector2d horizontal_aim_vector; - aim_vector = actor->control.grenade_current_aim_vector; horizontal_aim_vector = *(real_vector2d const *)&aim_vector; if (normalize2d(&horizontal_aim_vector) > 0.0f && @@ -1609,12 +1609,6 @@ long actor_aim_grenade( } } - /* BUG (preserved for exact matching): aim_vector is assigned only on foot. - * With actor->input.vehicle_index != NONE, January branches from its vehicle - * test straight to this scale and reads the unassigned local; the 2011 HCEX - * and later /Od builds keep the same path. A corrected build should start - * from actor->control.grenade_current_aim_vector. - */ scale_vector3d( &aim_vector, actor->control.grenade_current_aim_speed, diff --git a/source/ai/actor_perception.c b/source/ai/actor_perception.c index f3a583a8..a0bfef91 100644 --- a/source/ai/actor_perception.c +++ b/source/ai/actor_perception.c @@ -3383,30 +3383,7 @@ void actor_emotion_update( actor->emotions.forced_to_charge = TRUE; } - /* BUG (preserved for exact matching): the scan starts at - * NUMBER_OF_ACTOR_THREAT_TYPES rather than NUMBER_OF_ACTOR_THREAT_TYPES-1, - * so its first iteration reads specific_threats[9] - one element past the - * end of the nine-element array, which is cumulative_threats[0]. - * Evidence: January emits `mov eax,9` at +0xf5, then the six-byte alignment - * nop 8d 9b 00 00 00 00 (`lea ebx,[ebx]`, which touches neither EAX nor - * memory), and falls into the loop head at +0x100, `movsx edx,ax` followed by - * `cmp byte ptr [edx+esi+0x1ee],0`; the `dec eax` is at +0x10d, AFTER that - * load. In the same object actor_situation_update increments - * cumulative_threats for threat types 1..8 at esi+0x1f8..esi+0x1ff and - * area_friends at esi+0x200, which fixes cumulative_threats at esi+0x1f7 and - * so specific_threats at esi+0x1ee with exactly nine elements. - * Layout is asserted, not assumed: see - * actor_perception_specific_threats_size_assert and - * actor_perception_threat_arrays_adjacent_assert above, which fail to compile - * if the two arrays ever stop being adjacent or change length. - * Consequence: none observable. cumulative_threats[_actor_threat_none] is the - * one element of that array nothing ever writes - every write in the tree - * uses _actor_threat_visible..._actor_threat_damaging_me, i.e. 1..8, and the - * single variable-index use is a read-only csprintf argument in the - * actor_debug_print_threat macro - so the stray byte is always zero, the - * early-out is never taken on the stray iteration, and the scan falls through - * to the intended starting index. */ - for (priority = NUMBER_OF_ACTOR_THREAT_TYPES; + for (priority = NUMBER_OF_ACTOR_THREAT_TYPES - 1; priority > 0 && actor->situation.specific_threats[priority] <= 0; priority--) @@ -3858,12 +3835,8 @@ void prop_status_refresh( prop->preferred_target = TRUE; break; - /* BUG (preserved for exact matching): January compares a platoon - * reference with the actor's squad index (actor+0x3a) and a squad - * reference with its platoon index (actor+0x3c), the reverse of - * actor_action_handle_vehicle_entry. A corrected build should compare - * platoon references with meta.platoon_index and squad references with - * meta.squad_index. */ + /* (as the original game, kept: a platoon reference is compared with the actor's squad, + and a squad reference with its platoon) */ case _ai_reference_type_squad: if ((short)(((unsigned long)actor->external_orders.desired_target_ai_index >> 16) & UNSIGNED_CHAR_MAX) == prop_actor->meta.platoon_index) @@ -5815,10 +5788,8 @@ static void actor_perception_refresh( } else { - /* BUG (preserved for exact matching): January walks the perceiving actor's - * swarm unit list (actor+0x24 through the actor_get(actor_index) pointer), - * not the uncached swarm actor's list. A corrected build should start from - * swarm_actor->meta.swarm_unit_index. */ + /* (as the original game, kept: the perceiving actor's swarm is walked, not the swarm + actor's) */ long unit_index = actor->meta.swarm_unit_index; while (unit_index != NONE) @@ -5940,9 +5911,7 @@ static void actor_perception_refresh( prop_position_refresh(actor_index, prop_index, &position, FALSE, FALSE); } - /* BUG (preserved for exact matching): January leaves the loop before - * advancing past the entry that reached the limit, so the discard loop - * below also deletes that entry when it was an existing prop. */ + /* (as the original game, kept: the entry that reached the limit is discarded below too) */ if (++enemies.accepted_count >= 4) { break; diff --git a/source/ai/actors.c b/source/ai/actors.c index dc5c70b8..9afd0077 100644 --- a/source/ai/actors.c +++ b/source/ai/actors.c @@ -696,11 +696,8 @@ static void actor_verify_unit_activation( struct unit_datum *unit = unit_get(unit_index); struct actor_datum *actor = actor_get(actor_index); - /* BUG (preserved for exact matching): January tests - * unit->unit.last_vehicle_index + 30 < game_time_get() here; the exact - * unit_exit_seat_end proves last_vehicle_index at 0x2DC with the exit time in - * the following long. A corrected build should compare - * unit->unit.game_time_at_last_vehicle_exit. */ + /* (as the original game, kept: the vehicle's index is compared with the time, where + the time of the last vehicle exit was meant) */ if (unit->object.parent_object_index == NONE && unit->unit.last_vehicle_index + 30 < game_time_get()) { diff --git a/source/ai/ai.c b/source/ai/ai.c index d6ae2c79..d78094b3 100644 --- a/source/ai/ai.c +++ b/source/ai/ai.c @@ -2712,14 +2712,10 @@ void ai_handle_editing( "editing caused an overflow of MAXIMUM_SQUADS_PER_MAP (%d)", MAXIMUM_SQUADS_PER_MAP)); - /* BUG (preserved for exact matching): January passes the squad record COUNT as the - * csmemmove byte size, so only that many bytes of the trailing squad records move. - * A corrected build should multiply the count by sizeof(struct squad_datum). - */ csmemmove( &squad_array[encounter->squad_base + encounter_definition->squads.count], &squad_array[encounter->squad_base + encounter->squad_count], - last_squad_index - encounter->squad_base - encounter->squad_count); + (last_squad_index - encounter->squad_base - encounter->squad_count) * sizeof(struct squad_datum)); if (squad_count_delta > 0) { @@ -2746,14 +2742,10 @@ void ai_handle_editing( "editing caused an overflow of MAXIMUM_PLATOONS_PER_MAP (%d)", MAXIMUM_PLATOONS_PER_MAP)); - /* BUG (preserved for exact matching): January passes the platoon record COUNT as the - * csmemmove byte size, so only that many bytes of the trailing platoon records move. - * A corrected build should multiply the count by sizeof(struct platoon_datum). - */ csmemmove( &platoon_array[encounter->platoon_base + encounter_definition->platoons.count], &platoon_array[encounter->platoon_base + encounter->platoon_count], - last_platoon_index - encounter->platoon_base - encounter->platoon_count); + (last_platoon_index - encounter->platoon_base - encounter->platoon_count) * sizeof(struct platoon_datum)); if (platoon_count_delta > 0) { diff --git a/source/ai/ai_communication.c b/source/ai/ai_communication.c index 50d5dfbf..538c907d 100644 --- a/source/ai/ai_communication.c +++ b/source/ai/ai_communication.c @@ -2402,10 +2402,7 @@ static long ai_communication_find_global_actor_to_talk( } if (cause_unit_index != NONE) { - /* BUG (preserved for exact matching): January asks for the subject - * position again here and leaves cause_point uninitialized. A corrected - * build should call unit_get_head_position(cause_unit_index, &cause_point). */ - unit_get_head_position(subject_unit_index, &subject_point); + unit_get_head_position(cause_unit_index, &cause_point); } actor_iterator_new(&iterator, TRUE); diff --git a/source/ai/path.c b/source/ai/path.c index d40891c1..fd2e78cf 100644 --- a/source/ai/path.c +++ b/source/ai/path.c @@ -1271,14 +1271,7 @@ static short build_path_edges_for_surface( 0x5EE, (edge->adjacent_surface_index >= 0) && (edge->adjacent_surface_index < bsp->surfaces.count)); } - /* BUG (preserved for exact matching): January tests adjacent_surface_index for NONE only to - * skip the range assertion (+0xb9..+0xbe) and then loads pathfinding_surfaces[adjacent] - * unconditionally (+0xeb..+0xf0); the Aug-15-2001, Sept-25-2001 and later /Od (0x4c3730) - * builds do the same. The index is NONE only for an edge with no surface on its far side (an - * open collision BSP); the byte before the array then becomes the neighbour's flags, which the - * search treats as walkable when bit 0x40 is set. No structure BSP in the shipped - * 01.10.12.2276 maps has an open edge (0 of 2,066,607 edges in 82 BSPs). - */ + /* (an open edge, with no surface beyond it, is in no shipped map) */ /* port: (and a neighbor, not NONE, that is no surface of the bsp's or has no pathfinding surface is not walkable: a map's index; nor is NONE of a bsp with no pathfinding surfaces, with no array to read diff --git a/source/ai/path_smoothing.c b/source/ai/path_smoothing.c index 98d41ba5..64cb19eb 100644 --- a/source/ai/path_smoothing.c +++ b/source/ai/path_smoothing.c @@ -118,14 +118,7 @@ static boolean surface_is_walkable( struct collision_surface const *collision_surface; byte breakable_surface_index; - /* BUG (preserved for exact matching): find_turning_point passes a collision edge's surface - * index (surface_indices[0], surface_indices[!matches_end]) with no NONE test, and January - * reads pathfinding_surfaces[surface_index] first (inlined in _find_turning_point at - * +0x99..+0x9c and +0x274..+0x278). The index is NONE only for an edge with no surface on that - * side (an open collision BSP); the byte before the array is then taken as the surface's flags. - * No structure BSP in the shipped 01.10.12.2276 maps has an open edge (0 of 2,066,607 edges in - * 82 BSPs). - */ + /* (an open edge, with no surface beyond it, is in no shipped map) */ /* port: (NONE still is; any other index past the pathfinding surfaces, a map's, is no walkable surface. The retail bsps have as many pathfinding surfaces as surfaces, and every edge's surfaces are theirs) */ diff --git a/source/ai/path_structure_bsp.c b/source/ai/path_structure_bsp.c index d745b448..e61aaca2 100644 --- a/source/ai/path_structure_bsp.c +++ b/source/ai/path_structure_bsp.c @@ -121,17 +121,7 @@ boolean structure_test_ray2d( /* port: the pathfinding surfaces' flags are read through path_pathfinding_surface, which keeps the read below for NONE and gives no walkable surface for any other index past them (a map's index) */ - /* BUG (preserved for exact matching): January loads - * pathfinding_surfaces[surface_result.enter_surface_index] (+0x5d..+0x63) and the exit - * index (+0xe6..+0xec) before any NONE test; the later /Od build does the same (0x4cfe60 - * +0x98, +0x168). For a finite distance both indices come from a crossed edge - * (collision_surface_test_line2d starts at REAL_MIN/REAL_MAX), so they are NONE only for an - * edge with no surface on its far side (an open collision BSP). The read then takes the byte - * before the array: zero, or a value adopted as next_surface_index == NONE, stops at the - * edge; with the breakable bit set and broken surfaces not ignored, TAG_BLOCK_GET_ELEMENT - * receives NONE and its index assertion halts. No structure BSP in the shipped - * 01.10.12.2276 maps has an open edge (0 of 2,066,607 edges in 82 BSPs). - */ + /* (an open edge, with no surface beyond it, is in no shipped map) */ while (TRUE) { long next_surface_index = NONE; @@ -404,14 +394,7 @@ boolean structure_test_line2d( if (cross_product2d(&p0p1, &p0e0) > 0.0f && cross_product2d(&p0e1, &p0p1) > 0.0f) { - /* BUG (preserved for exact matching): January loads - * pathfinding_surfaces[next_surface_index] (+0x207..+0x20b) before any NONE test, as - * does the later /Od build (0x4cee00 +0x330..+0x339). next_surface_index is NONE only - * for an open edge; the byte before the array then decides: walkable bit clear reports - * a collision at this edge; set, NONE reaches TAG_BLOCK_GET_ELEMENT (below, or at the - * top of the surface loop), whose index assertion halts. No structure BSP in the - * shipped 01.10.12.2276 maps has an open edge (0 of 2,066,607 edges in 82 BSPs). - */ + /* (an open edge, with no surface beyond it, is in no shipped map) */ long next_surface_index = edge->surface_indices[!on_right_side]; /* port: (path_pathfinding_surface: a map's index past the pathfinding surfaces is no walkable one) */ @@ -743,17 +726,8 @@ boolean structure_test_pill2d( best_result = NULL; } - /* BUG (preserved for exact matching): a side whose p0 surface index is NONE gets only - * its collision flag, so when p0_surface_index is NONE the copy below returns - * left_result's point, surface_index, edge_index and t unassigned (January 0x452500 - * writes only the byte [ebp-0x4c] at +0x189 and copies seven dwords from [ebp-0x4c] - * at +0x24f..+0x25a), and the function returns FALSE. actor_move_try_evasion_vector - * passes the actor's pathfinding surface as p0 and, on a FALSE return, reads - * result->point.z to accept or reject the evasion point; actor_find_pathfinding_location - * leaves that surface NONE for a non-flying actor when no ground surface is found or - * when the actor's vehicle is not a ground vehicle. A runtime occurrence was not - * traced. A corrected build should fill both side results before selecting. - * Source-policy approval pending (2026-09-27 audit). */ + /* (as the original game, kept: a side whose first surface is NONE leaves the rest of its + result unset) */ if (!best_result || distance_squared2d((real_point2d const *)&best_result->point, p1) < radius * radius) { diff --git a/source/cseries/stack_walk_windows.c b/source/cseries/stack_walk_windows.c index 70c20987..e487baa8 100644 --- a/source/cseries/stack_walk_windows.c +++ b/source/cseries/stack_walk_windows.c @@ -479,13 +479,6 @@ int load_symbol_table( { found_symbols_section = TRUE; } - else if (strstr(line, "Timestamp")) - { - /* BUG (preserved for exact matching): January performs the - * timestamp search but ignores whether it succeeds. A corrected - * build should reject a map whose timestamp does not match. */ - strstr(line, timestamp_str); - } } string_storage_size = 0; @@ -523,24 +516,9 @@ int load_symbol_table( goto corrupt_map_file; } - /* BUG (preserved for exact matching): January consumes each of - * these continuation lines without checking whether fgets failed. - * A corrected build should reject EOF before reading line[0] or - * tokenizing the buffer. */ - fgets(line, sizeof(line), map_file); - if (!isspace(line[0])) - { - goto corrupt_map_file; - } - - fgets(line, sizeof(line), map_file); - if (!strstr(line, "Static symbols")) - { - goto corrupt_map_file; - } - - fgets(line, sizeof(line), map_file); - if (!isspace(line[0])) + if (!fgets(line, sizeof(line), map_file) || !isspace(line[0]) || + !fgets(line, sizeof(line), map_file) || !strstr(line, "Static symbols") || + !fgets(line, sizeof(line), map_file) || !isspace(line[0])) { goto corrupt_map_file; } @@ -559,15 +537,13 @@ int load_symbol_table( } symbol_address = strtoul(token, &end_str, 16); - /* BUG (preserved for exact matching): January leaves symbol_name - * unchanged if the continuation entry omits its name token, then - * continues parsing. A corrected build should reject that entry. */ token = strtok(NULL, " \t\n\r"); - if (token) + if (!token) { - strncpy(symbol_name, token, sizeof(symbol_name)-1); - symbol_name[sizeof(symbol_name)-1] = 0; + goto corrupt_map_file; } + strncpy(symbol_name, token, sizeof(symbol_name)-1); + symbol_name[sizeof(symbol_name)-1] = 0; } token = strtok(NULL, " \t\n\r"); @@ -582,11 +558,8 @@ int load_symbol_table( stack_walk_globals.fixup = rva_base - (unsigned long)load_symbol_table; } - /* BUG (preserved for exact matching): January checks only whether - * strtoul assigned an end pointer, then advances five bytes without - * validating the conversion or remaining field width. A corrected - * build should validate both before advancing. */ - if (!end_str) + /* (the address's five columns, " f ", before the object name) */ + if (!end_str || strlen(end_str) < 5) { goto corrupt_map_file; } @@ -696,10 +669,8 @@ finished: sizeof(*symbol_table->symbols), symbol_sort_proc); - /* BUG (preserved for exact matching): January assumes at least one - * nonzero RVA while trimming sentinels. A corrected build should - * stop before number_of_symbols reaches zero. */ - while (symbol_table->symbols[symbol_table->number_of_symbols-1].rva_base==0) + while (symbol_table->number_of_symbols > 0 && + symbol_table->symbols[symbol_table->number_of_symbols-1].rva_base==0) { symbol_table->number_of_symbols--; } @@ -733,9 +704,10 @@ static int symbol_sort_proc( const struct debug_symbol *symbol1 = elem1; const struct debug_symbol *symbol2 = elem2; - /* BUG (preserved for exact matching): January returns 1 when both RVAs - * are zero, violating comparator antisymmetry. A corrected build should - * return 0 for two equal zero-RVA sentinel records. */ + if (symbol1->rva_base == symbol2->rva_base) + { + return 0; + } if (symbol1->rva_base==0 || symbol1->rva_base > symbol2->rva_base) { return 1; diff --git a/source/effects/effects.c b/source/effects/effects.c index d00ab05a..18691f49 100644 --- a/source/effects/effects.c +++ b/source/effects/effects.c @@ -2623,16 +2623,7 @@ static boolean effect_allowed_by_environment( 888, FALSE, NULL); - /* BUG (original, preserved for exact matching): this arm leaves allowed - * unassigned, and January returns it after the fatal assertion: 0x48b160 +0x5f mov al,[ebp-1] - * reads the never-written byte slot. The later /Od+/RTC build attests the uninitialised - * declaration: its single exit calls _RTC_UninitUse("allowed"). - * The read is not executed in January: display_assert returns into an unconditional - * system_exit, which never returns (it jumps to halt_and_catch_fire, which loops, or calls - * _exit on re-entry), and no other path reaches the read. The arm itself is entered only for - * an environment value outside the four cases above, each of which assigns allowed; the - * value comes from effect tag data, so this arm is not proven unreachable. A corrected build - * assigns allowed in this arm. */ + allowed = FALSE; break; } diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 04bd4cd6..40f95edf 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -8206,12 +8206,7 @@ static void game_engine_verify_current_map( _netgame_flag_race_track, "NETGAME MAP FAILURE: duplicate race track flag [team %d]"); - /* BUG (preserved for exact matching): January passes team index zero for - * both CTF checks, and netgame_verify_spawn_points never reads that - * formal parameter. - * A corrected build should filter starting locations by an authoritatively - * recovered team-index field before reporting per-team counts. - */ + /* (the team index is not read: both checks count every ctf spawn) */ netgame_verify_spawn_points( game_engine_ctf, 0, diff --git a/source/game/game_engine_ctf.c b/source/game/game_engine_ctf.c index 9a5b204d..5caf9159 100644 --- a/source/game/game_engine_ctf.c +++ b/source/game/game_engine_ctf.c @@ -622,11 +622,6 @@ static void ctf_engine_player_update( { ctf_award_capture(player_index, player->team_index); ctf_player_drop_flag(player_index, weapon_index); - /* BUG (preserved for exact matching): January (and the - later HCEA build) call game_engine_get_variant() here - and discard the result. A corrected build should drop - this statement. */ - game_engine_get_variant(); } else { diff --git a/source/hs/hs_compile.c b/source/hs/hs_compile.c index e5acaf53..e31047e0 100644 --- a/source/hs/hs_compile.c +++ b/source/hs/hs_compile.c @@ -714,11 +714,7 @@ static long hs_parse_cond_recursive( long condition_expression_index = hs_syntax_get(expression_index)->data; struct hs_syntax_node *condition_expression = hs_syntax_get(condition_expression_index); - /* BUG (preserved for exact matching): January tests the logical negation of the - * result index against NONE, which is never true, so a clause without a result is - * not rejected here. A corrected build tests - * condition_expression->next_node_index != NONE. */ - if (!condition_expression->next_node_index != NONE) + if (condition_expression->next_node_index != NONE) { long if_then_expression_index = datum_new(hs_syntax_data); long then_value_expression_index = datum_new(hs_syntax_data); @@ -3012,13 +3008,7 @@ boolean hs_parse_begin( { if (argument_count < 1) { - /* BUG (preserved for exact matching): January passes the function name - * although this format has no conversion for it; the argument is ignored. */ - sprintf( - hs_compile_globals.error_buffer, - "a statement block must contain at least one argument.", - hs_function_get(function_index)->name); - hs_compile_globals.error = hs_compile_globals.error_buffer; + hs_compile_globals.error = "a statement block must contain at least one argument."; hs_compile_globals.error_offset = hs_syntax_get(expression_index)->source_offset; result = FALSE; } diff --git a/source/interface/hud_weapon.c b/source/interface/hud_weapon.c index 8f801a71..ca4e64a7 100644 --- a/source/interface/hud_weapon.c +++ b/source/interface/hud_weapon.c @@ -894,10 +894,7 @@ static void hud_update_weapon_local_player( "c:\\halo\\SOURCE\\interface\\hud_weapon.c", 0x16E, !"unreachable"); - /* BUG (preserved for exact matching): January (T+0x404) and the later /Od build (0x638ac2) - leave result unassigned in this arm. The arm is unreachable in defined execution: - crosshair_index only takes the values 0..NUMBER_OF_CROSSHAIR_STATES-1 (0..18), and each of - those 19 states has a case above that assigns result. */ + result = 0; break; } diff --git a/source/interface/ui_widget_game_data_input_functions.c b/source/interface/ui_widget_game_data_input_functions.c index d863a2e2..b39fd464 100644 --- a/source/interface/ui_widget_game_data_input_functions.c +++ b/source/interface/ui_widget_game_data_input_functions.c @@ -2277,7 +2277,7 @@ static void game_options_menu_update_text_desc( struct widget_instance *extended_description; struct widget_instance *spinner_list; struct ui_widget_definition *definition; - long description_index; + long description_index = 0; match_vassert( "c:\\halo\\SOURCE\\interface\\ui_widget_game_data_input_functions.c", @@ -2339,12 +2339,6 @@ static void game_options_menu_update_text_desc( } } - /* BUG (preserved for exact matching): description_index is assigned only when the - * list has a focused child. Without one, January stores the low word of the widget - * argument slot instead (0x4e1ce0: +0xa4 branches to +0x119 mov dx,[ebp+8]; the slot - * is never written). Whether a game options list is updated without a focused child - * is not shown. A corrected build should initialise description_index to 0. - * Source-policy approval pending (2026-09-27 audit). */ extended_description->parameters.text_box.string_list_index = (short)description_index; return; } @@ -2356,7 +2350,7 @@ static void game_options_menu_update_pic_desc( struct widget_instance *extended_description; struct widget_instance *spinner_list; struct ui_widget_definition *definition; - long description_index; + long description_index = 0; match_vassert( "c:\\halo\\SOURCE\\interface\\ui_widget_game_data_input_functions.c", @@ -2408,11 +2402,6 @@ static void game_options_menu_update_pic_desc( } } - /* BUG (preserved for exact matching): as in game_options_menu_update_text_desc, - * without a focused child January stores the low word of the widget argument slot - * (0x4e1ea0 +0x119 mov dx,[ebp+8]). Whether that occurs is not shown. A corrected - * build should initialise description_index to 0. Source-policy approval pending - * (2026-09-27 audit). */ extended_description->animation.current_frame_index = (short)description_index; return; } diff --git a/source/items/projectiles.c b/source/items/projectiles.c index 86100135..8fbb7b8f 100644 --- a/source/items/projectiles.c +++ b/source/items/projectiles.c @@ -1198,9 +1198,7 @@ static void projectile_calculate_deceleration( definition, definition->projectile.air_damage_range_lower_bound, definition->projectile.air_damage_range_upper_bound); - /* BUG (preserved for exact matching): January loads the water upper bound - * in the air branch. A corrected build should use - * definition->projectile.air_damage_range_upper_bound. */ + /* (as the original game, kept: in the air the water damage range's upper bound is used) */ projectile->projectile.maximum_damage_distance = definition->projectile.water_damage_range_upper_bound; diff --git a/source/math/geometry.c b/source/math/geometry.c index d6df49e5..ef23db87 100644 --- a/source/math/geometry.c +++ b/source/math/geometry.c @@ -318,20 +318,12 @@ struct geosphere *geosphere_new( &vertex_index, &triangle_strip_vertex_indices_index, vertex_subdivision_indices); } - /* BUG (preserved for exact matching): at segment_count == 1, - * January fills the allocated strip buffer but rejects equality. - * A corrected build should allow a fully filled valid buffer. - */ match_assert("c:\\halo\\SOURCE\\math\\geometry.c", 98, - triangle_strip_vertex_indices_index < (NUMBER_OF_VERTICES_PER_TRIANGLE + 1) * result->triangle_count); + triangle_strip_vertex_indices_index <= (NUMBER_OF_VERTICES_PER_TRIANGLE + 1) * result->triangle_count); match_assert("c:\\halo\\SOURCE\\math\\geometry.c", 99, vertex_index == result->vertex_count); } else { - /* BUG (preserved for exact matching): January frees allocated - * children but returns the owner with dangling member pointers. - * A corrected build should free the owner and return NULL. - */ if (result->vertices) { match_free("c:\\halo\\SOURCE\\math\\geometry.c", 103, result->vertices); @@ -340,6 +332,8 @@ struct geosphere *geosphere_new( { match_free("c:\\halo\\SOURCE\\math\\geometry.c", 104, result->triangle_strip_vertex_indices); } + match_free("c:\\halo\\SOURCE\\math\\geometry.c", 105, result); + result = NULL; } if (vertex_subdivision_indices) diff --git a/source/rasterizer/rasterizer.c b/source/rasterizer/rasterizer.c index 15ba5dd0..2d18b8c4 100644 --- a/source/rasterizer/rasterizer.c +++ b/source/rasterizer/rasterizer.c @@ -1901,6 +1901,7 @@ void rasterizer_debug_model_vertices( { debug_vertex_count = 0; closest_debug_vertex_index = NONE; + closest_debug_vertex_dot = -1.f; match_assert( "c:\\halo\\SOURCE\\rasterizer\\rasterizer.c", 857, @@ -2032,13 +2033,6 @@ void rasterizer_debug_model_vertices( camera_dot = dot_product3d( &camera_to_vertex, &global_window_parameters.camera.forward); - /* BUG (preserved for exact matching): closest_debug_vertex_dot is never initialised - * (only closest_debug_vertex_index is), so the first vertex compares an unassigned - * real here (January 0x56cb00 reads [ebp-0x2c] at +0x496 and +0x4a3; its only store - * is +0x4b9). Reached whenever the hs global rasterizer_debug_model_vertices is set - * and the obscuring object is drawn; the value only chooses which vertex label is - * drawn red. A corrected build should initialise it to -1.0f. Source-policy approval - * pending (2026-09-27 audit). */ if ((dot_product3d(&camera_to_vertex, &normal) < 0.f && closest_debug_vertex_dot < camera_dot) || closest_debug_vertex_dot == -1.f) diff --git a/source/rasterizer/xbox/rasterizer_xbox_hardware_geometry.c b/source/rasterizer/xbox/rasterizer_xbox_hardware_geometry.c index 46dd4e14..261f3c37 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_hardware_geometry.c +++ b/source/rasterizer/xbox/rasterizer_xbox_hardware_geometry.c @@ -209,7 +209,7 @@ boolean rasterizer_triangle_buffer_new( long count, void const *triangles) { - D3DIndexBuffer *d3d_index_buffer; + D3DIndexBuffer *d3d_index_buffer = NULL; byte *locked_triangles; boolean success; long buffer_size; @@ -267,12 +267,6 @@ boolean rasterizer_triangle_buffer_new( result, "IDirect3DDevice8_CreateIndexBuffer(global_d3d_device, buffer_size, RASTERIZER_STATIC_BUFFER_USAGE, D3DFMT_INDEX16, RASTERIZER_STATIC_BUFFER_POOL, &d3d_index_buffer)"); } - /* BUG (preserved for exact matching): as in rasterizer_vertex_buffer_new, the - * January XDK CreateIndexBuffer failure path (0x5de5d3..0x5de5d8) returns without - * writing d3d_index_buffer, so after a failed call this test reads it unassigned. - * success is already FALSE on that path and the pointer is cleared below, so the - * result does not depend on the value. Source-policy approval pending (2026-09-27 - * audit). */ if (!d3d_index_buffer) success = FALSE; if (!success) diff --git a/source/structures/leaf_map.c b/source/structures/leaf_map.c index 60b3b698..0e80dfdc 100644 --- a/source/structures/leaf_map.c +++ b/source/structures/leaf_map.c @@ -1311,16 +1311,6 @@ static void leaf_map_build_portal_from_leaves( short portal_designator_index0 = (short)tag_block_add_element(&leaf0->portal_designators); short portal_designator_index1 = (short)tag_block_add_element(&leaf1->portal_designators); - /* BUG (preserved for exact matching): January checks vertex_count - * against NONE only after entering the positive-count branch, where - * the assertion cannot fail. A corrected build should perform this - * check immediately after convex_hull2d_intersect returns. */ - match_vassert( - "c:\\halo\\SOURCE\\structures\\leaf_map.c", - 487, - vertex_count!=NONE, - "too many vertices in portal"); - if (portal_index != NONE && portal_designator_index0 != NONE && portal_designator_index1 != NONE) { struct leaf_portal *portal = TAG_BLOCK_GET_ELEMENT( diff --git a/source/tag_files/files.c b/source/tag_files/files.c index 3d8e99d7..b6280dfc 100644 --- a/source/tag_files/files.c +++ b/source/tag_files/files.c @@ -298,7 +298,7 @@ boolean datastore_read( void *data) { struct file_reference file_ref; - boolean success; + boolean success = FALSE; unsigned long datastore_size = 0; struct datastore *datastore; @@ -356,13 +356,6 @@ boolean datastore_read( } } - /* BUG (preserved for exact matching): success is never initialised, so a missing - * file, a wrong-sized datastore or an absent field returns it unassigned (January - * 0x589370 keeps it in the high byte of the field_name argument slot, [ebp+0xf], - * written only at +0x1f5 and read at +0x19b and +0x20c). Not reached in January: - * cachebeta.exe has no call, jump or address reference to this function. A corrected - * build should initialise success to FALSE. Source-policy approval pending - * (2026-09-27 audit). */ return success; } diff --git a/source/tool/connected_geometry.c b/source/tool/connected_geometry.c index e89bac11..1d19c83c 100644 --- a/source/tool/connected_geometry.c +++ b/source/tool/connected_geometry.c @@ -276,19 +276,7 @@ static long connected_geometry_find_or_add_edge( long point_index0, long point_index1) { - /* BUG (preserved for exact matching): January declares `direction` without an - * initializer. It is written only at the two matches (+0x47, +0x4d) and on edge - * creation (+0x67), and read once, at +0xbe. The only path that reaches that read - * without a write requires geometry->edges.count < 0 (a deleted array, whose count is - * NONE, is one way this can arise). On that path edge_index stays 0, so the - * dynamic_array_get_element call (+0x9b) runs first: its unconditional - * `array->count>=0` assertion fails, and match_assert's failure path (display_assert, - * then system_exit -> halt_and_catch_fire, which never returns: it loops on the error - * screen, or calls exit on re-entry) ends this build before `direction` is read. - * The later /Od+/RTC build attests the declaration with _RTC_UninitUse("direction"). - * A corrected build should initialize direction to TRUE. - */ - boolean direction; + boolean direction = TRUE; long edge_index; for (edge_index = 0; edge_index < geometry->edges.count; edge_index++) diff --git a/source/units/unit_dialogue.c b/source/units/unit_dialogue.c index 429df104..4ac63380 100644 --- a/source/units/unit_dialogue.c +++ b/source/units/unit_dialogue.c @@ -998,12 +998,8 @@ void unit_dialogue_update( if (unit->unit.speech.damage_minor_timer > 0) unit->unit.speech.damage_minor_timer--; - /* BUG (preserved for exact matching): January decrements damage_minor_timer - * (+0x39C) a second time here and never touches damage_major_timer (+0x39E, - * the field unit_make_damage_sound tests against zero and sets to 60), so a - * unit's major pain vocalization is suppressed for good once one has played. - * A corrected build should decrement damage_major_timer in this statement. - */ + /* (as the original game, kept: the minor pain timer counts down twice and the major one + not at all, so a unit cries out in major pain once) */ if (unit->unit.speech.damage_minor_timer > 0) unit->unit.speech.damage_minor_timer--; From 7df9be77856ba3d4368035eb52bf748429ffa434 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Thu, 8 Oct 2026 14:35:46 -0500 Subject: [PATCH 140/187] Give unset locals values, and drop the decompiler's dated audit notes 25 notes ("left unassigned only by this default arm ... approval pending, 2026-09-27 audit") argued that a variable was never read unset. Where the arm ends in system_exit they were right and simply go. A release build's assertions return, though, so where the arm only asserts the variable now starts with a value (a Custom Edition map's HUD data can reach hud_draw's). A failed map precache reads as failed. rasterizer_debug_drawing_begin had two declarations, one with an argument its definition never takes; it now has one, and the port's macro papering over the mismatch is gone. (cherry picked from commit 74f8a048e4c3cc7dbc4b050dd41a8bae912bb367) --- port/linux/README.md | 2 +- port/linux/include/halo_linux_source_fixups.h | 12 +----------- source/ai/actor_looking.c | 8 ++------ source/ai/actor_perception.c | 10 +--------- source/ai/encounters.c | 6 +----- source/bitmaps/bitmap_drawing.c | 16 ++++------------ source/bitmaps/bitmap_extract.c | 12 ++---------- source/bitmaps/bitmap_utilities.c | 4 ---- source/cache/cache_files_windows.c | 10 +--------- source/cutscene/recorded_animation_playback.c | 6 +----- source/devices/device_controls.c | 4 ---- source/game/players.c | 4 ---- source/hs/hs_runtime.c | 6 +----- source/interface/hud_draw.c | 6 +----- source/interface/hud_nav_points.c | 11 +---------- source/interface/hud_unit.c | 8 ++------ source/items/projectiles.c | 4 ---- source/math/geometry.c | 9 ++------- source/math/periodic_functions.c | 8 -------- source/memory/byte_swapping.c | 8 +------- source/memory/data_encoding.c | 4 ---- source/rasterizer/rasterizer_debug.c | 11 +++-------- source/rasterizer/xbox/rasterizer_xbox.c | 4 ---- source/rasterizer/xbox/rasterizer_xbox_debug.h | 9 +-------- .../xbox/rasterizer_xbox_transparent_geometry.c | 12 ++---------- source/render/render_particles.c | 8 +------- source/sound/sound_manager.c | 6 +----- source/units/unit_dialogue.c | 6 +----- 28 files changed, 31 insertions(+), 183 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index 12ea9e88..dd840ba6 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -944,7 +944,7 @@ These files supply the MSVC functions that clang does not have: | `port/include/xdk` | The Xbox SDK declarations. The compiler reads this folder after all the other folders. | | `tools/linux_msvc_semantics.py` | Makes a header that declares each struct tag at file scope, as MSVC does. It also makes the header inline functions weak, as the COMDAT functions of MSVC. `game/msvc_comdat.c` gives one external copy of each. | | `include/halo_linux_winsock_names.h` | Gives new names to the Winsock functions of the SDK. Thus they do not link to the glibc functions with the same names. | -| `include/halo_linux_source_fixups.h` | Repairs one declaration conflict (`rasterizer_debug_drawing_begin`). | +| `include/halo_linux_source_fixups.h` | Declares the port's functions that the game's sources call. | `tools/linux_link_check.py` stops the link if a weak reference has no definition. Without this check, the linker gives the reference the address diff --git a/port/linux/include/halo_linux_source_fixups.h b/port/linux/include/halo_linux_source_fixups.h index d4586323..83c4b95f 100644 --- a/port/linux/include/halo_linux_source_fixups.h +++ b/port/linux/include/halo_linux_source_fixups.h @@ -1,22 +1,12 @@ /* HALO_LINUX_SOURCE_FIXUPS.H -Game-only workarounds for source that MSVC accepts but clang rejects, where -editing the source itself would change the byte-matched MSVC output (see -port/linux/README.md for how each was checked). +The port's functions that the game's sources call, declared for them. */ #ifndef __HALO_LINUX_SOURCE_FIXUPS_H #define __HALO_LINUX_SOURCE_FIXUPS_H -/* rasterizer.h declares rasterizer_debug_drawing_begin(boolean opaque) while -rasterizer_xbox_debug.h declares a second `long zbias` parameter, and -rasterizer_debug.c includes both and passes two arguments. MSVC tolerates -the mismatch; the definition ignores zbias. Adding the parameter to -rasterizer.h perturbs MSVC's register allocation elsewhere, so instead every -declaration and call collapses to the one-parameter form here. */ -#define rasterizer_debug_drawing_begin(opaque, ...) (rasterizer_debug_drawing_begin)(opaque) - /* frames between the 30 Hz ticks (port/linux/game/render_interpolation.c); the platform layer reads the display.interpolation setting */ struct observer_result; diff --git a/source/ai/actor_looking.c b/source/ai/actor_looking.c index 93de6c2d..4f720c40 100644 --- a/source/ai/actor_looking.c +++ b/source/ai/actor_looking.c @@ -829,8 +829,8 @@ static long actor_look_idle_timer( struct actor_datum *actor = actor_get(actor_index); struct actor_definition *definition = actor_definition_get(actor->meta.definition_index); struct weapon_definition *weapon_definition = actor_get_weapon_definition(actor_index); - real time_lower_bound; - real time_upper_bound; + real time_lower_bound = 0.0f; + real time_upper_bound = 0.0f; real time; long ticks; @@ -848,10 +848,6 @@ static long actor_look_idle_timer( time_lower_bound = looking_definition->idle_look_time_lower_bound; time_upper_bound = looking_definition->idle_look_time_upper_bound; break; - /* time_lower_bound and time_upper_bound are left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert( "c:\\halo\\SOURCE\\ai\\actor_looking.c", diff --git a/source/ai/actor_perception.c b/source/ai/actor_perception.c index a0bfef91..f0d8945f 100644 --- a/source/ai/actor_perception.c +++ b/source/ai/actor_perception.c @@ -1504,10 +1504,6 @@ void actor_situation_update_target_status( (target_prop->definitely_located != FALSE) + 5; break; - /* target_type is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: display_assert( NULL, @@ -6509,7 +6505,7 @@ void actor_perception_update( { short knowledge_type = actor_get_perception_knowledge(actor_index, iterator.index); short awareness_speed; - real awareness_delta; + real awareness_delta = 0.0f; #line 489 "c:\\halo\\SOURCE\\ai\\actor_perception.c" assert((knowledge_type >= 0) && (knowledge_type < NUMBER_OF_ACTOR_KNOWLEDGE_TYPES)); @@ -6539,10 +6535,6 @@ void actor_perception_update( awareness_delta = 1.0f; break; - /* awareness_delta is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: #line 516 "c:\\halo\\SOURCE\\ai\\actor_perception.c" assert(!"unreachable"); diff --git a/source/ai/encounters.c b/source/ai/encounters.c index c3b3de1c..143bb7e6 100644 --- a/source/ai/encounters.c +++ b/source/ai/encounters.c @@ -2190,7 +2190,7 @@ void encounter_create( struct squad_definition *squad_definition = TAG_BLOCK_GET_ELEMENT( &encounter_definition->squads, squad_index, struct squad_definition); short initial_variant; - short count; + short count = 0; short actor_type; short i; @@ -2214,10 +2214,6 @@ void encounter_create( case _game_difficulty_level_impossible: count = squad_definition->max_count; break; - /* count is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert("c:\\halo\\SOURCE\\ai\\encounters.c", 1730, FALSE, NULL); } diff --git a/source/bitmaps/bitmap_drawing.c b/source/bitmaps/bitmap_drawing.c index 238d008a..99e0ef9c 100644 --- a/source/bitmaps/bitmap_drawing.c +++ b/source/bitmaps/bitmap_drawing.c @@ -379,8 +379,8 @@ void bitmap_fill_rectangle( short alpha, inverse_alpha; short x_minimum, x_maximum, y_minimum, y_maximum; short width, y; - short mode; - long pixel; + short mode = 0; + long pixel = 0; match_assert( "c:\\halo\\SOURCE\\bitmaps\\bitmap_drawing.c", @@ -402,10 +402,6 @@ void bitmap_fill_rectangle( mode = (alpha != 255) ? _bitmap_fill_blend_r5g6b5 : _bitmap_fill_write_16bit; break; - /* mode and pixel are left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert( "c:\\halo\\SOURCE\\bitmaps\\bitmap_drawing.c", @@ -1050,8 +1046,8 @@ void bitmap_draw_line( short y_maximum = destination->height; short alpha = color >> 24; short inverse_alpha = 255 - alpha; - short mode; - long pixel; + short mode = 0; + long pixel = 0; point2d point0, point1; struct bitmap_line_state state; @@ -1079,10 +1075,6 @@ void bitmap_draw_line( mode = (alpha != 255) ? _bitmap_pixel_blend_a8r8g8b8 : _bitmap_pixel_write_32bit; break; - /* mode and pixel are left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert( "c:\\halo\\SOURCE\\bitmaps\\bitmap_drawing.c", diff --git a/source/bitmaps/bitmap_extract.c b/source/bitmaps/bitmap_extract.c index 052ef7a7..8b99551b 100644 --- a/source/bitmaps/bitmap_extract.c +++ b/source/bitmaps/bitmap_extract.c @@ -1363,7 +1363,7 @@ struct bitmap_data *extract_build_debug_plate( struct bitmap_data *converted_bitmap = NULL; struct bitmap_data *debug_bitmap = NULL; short mipmap_index; - short slice_count; + short slice_count = 0; switch (bitmap->type) { @@ -1448,10 +1448,6 @@ struct bitmap_data *extract_build_debug_plate( case _bitmap_type_cube_map: slice_count = 6; break; - /* slice_count is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert( "c:\\halo\\SOURCE\\bitmaps\\bitmap_extract.c", @@ -1510,7 +1506,7 @@ struct bitmap_data *extract_build_debug_plate( if (slice_bitmap && slice_bitmap->base_address) { - short mipmap_slice_count; + short mipmap_slice_count = 0; short slice_index; switch (converted_bitmap->type) @@ -1524,10 +1520,6 @@ struct bitmap_data *extract_build_debug_plate( case _bitmap_type_cube_map: mipmap_slice_count = 6; break; - /* mipmap_slice_count is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert( "c:\\halo\\SOURCE\\bitmaps\\bitmap_extract.c", diff --git a/source/bitmaps/bitmap_utilities.c b/source/bitmaps/bitmap_utilities.c index 9c73121f..b95bca02 100644 --- a/source/bitmaps/bitmap_utilities.c +++ b/source/bitmaps/bitmap_utilities.c @@ -2447,10 +2447,6 @@ union rgb_color *hsv_color_to_rgb_color( } } - /* The sector switch has no default. Not reached unassigned: for a word hue, - * scaled_hue = hue * 6 / 65536 is at most 393210 / 65536 < 6 (exact in real), so - * sector is 0..5 and each of those cases assigns red, green and blue. Source-policy - * approval pending (2026-09-27 audit). */ rgb->red = (word)(long)(red * 65535.0f); rgb->green = (word)(long)(green * 65535.0f); rgb->blue = (word)(long)(blue * 65535.0f); diff --git a/source/cache/cache_files_windows.c b/source/cache/cache_files_windows.c index d8c7e176..29ded990 100644 --- a/source/cache/cache_files_windows.c +++ b/source/cache/cache_files_windows.c @@ -917,7 +917,7 @@ short cache_file_read( short cache_files_precache_map_status( real *progress) { - short status; + short status = _cached_map_file_failed; match_assert( "c:\\halo\\SOURCE\\cache\\cache_files_windows.c", @@ -943,10 +943,6 @@ short cache_files_precache_map_status( status = _cached_map_file_success; break; - /* status is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert("c:\\halo\\SOURCE\\cache\\cache_files_windows.c", 1013, FALSE, NULL); break; @@ -1332,10 +1328,6 @@ static short cached_map_files_find_free_map( last_map_file_index = 2; break; - /* first_map_file_index and last_map_file_index are left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ /* port: the type is the map's header's, and a release build's assertion goes on: a type no cache file is for finds none */ default: diff --git a/source/cutscene/recorded_animation_playback.c b/source/cutscene/recorded_animation_playback.c index 6a795a2b..293b4b90 100644 --- a/source/cutscene/recorded_animation_playback.c +++ b/source/cutscene/recorded_animation_playback.c @@ -415,7 +415,7 @@ boolean recorded_animation_apply_event_stream( byte const *playback_stream_end) { struct animation_event_header const *header; - word time_delta; + word time_delta = 0; word header_size; recorded_animation_apply_proc apply; @@ -466,10 +466,6 @@ boolean recorded_animation_apply_event_stream( time_delta>UNSIGNED_CHAR_MAX); break; - /* time_delta is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_assert( "c:\\halo\\SOURCE\\cutscene\\recorded_animation_playback.c", diff --git a/source/devices/device_controls.c b/source/devices/device_controls.c index e72a8f35..7d5f6747 100644 --- a/source/devices/device_controls.c +++ b/source/devices/device_controls.c @@ -188,10 +188,6 @@ static void control_toggle( case 3: desired_value= definition->control.call_value; break; - /* desired_value is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: display_assert( NULL, diff --git a/source/game/players.c b/source/game/players.c index ea7fdcd5..51dbc730 100644 --- a/source/game/players.c +++ b/source/game/players.c @@ -4186,10 +4186,6 @@ static void player_handle_powerup_equipment( powerup_index = 1; break; - /* powerup_index is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: display_assert( NULL, diff --git a/source/hs/hs_runtime.c b/source/hs/hs_runtime.c index 75462f60..f9f27a75 100644 --- a/source/hs/hs_runtime.c +++ b/source/hs/hs_runtime.c @@ -1046,7 +1046,7 @@ void render_debug_trigger_volumes( { if (debug_trigger_volumes) { - real_matrix4x3 matrix; + real_matrix4x3 matrix = { 0 }; short volume_index; struct scenario *scenario = global_scenario_get(); @@ -1091,10 +1091,6 @@ void render_debug_trigger_volumes( matrix4x3_transform_vector(&matrix, &local_extent, &world_extent); break; - /* matrix is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_assert( "c:\\halo\\SOURCE\\hs\\hs_runtime.c", diff --git a/source/interface/hud_draw.c b/source/interface/hud_draw.c index ce9d0807..59dcc2e4 100644 --- a/source/interface/hud_draw.c +++ b/source/interface/hud_draw.c @@ -787,7 +787,7 @@ static void hud_draw_multitexture_overlay( { static real theta; struct multitexture_overlay_hud_element_effector_definition *effector; - real source_value; + real source_value = 0.0f; real dest_value; real_rgb_color dest_color; @@ -839,10 +839,6 @@ static void hud_draw_multitexture_overlay( break; } - /* The switch above has no default: a source value outside the eight enumerators - * leaves source_value unassigned for the interpolation below. Not shown reachable: - * the value comes from hud tag data, which was not scanned. Source-policy approval - * pending (2026-09-27 audit). */ if (effector->in_bounds[1] == effector->in_bounds[0] || effector->out_bounds[1] == effector->out_bounds[0]) { diff --git a/source/interface/hud_nav_points.c b/source/interface/hud_nav_points.c index c253d21f..f30b2705 100644 --- a/source/interface/hud_nav_points.c +++ b/source/interface/hud_nav_points.c @@ -912,7 +912,7 @@ void hud_render_nav_points( for (nav_point_index = 0; nav_point_indexnav_points[nav_point_index]; - real_point3d position; + real_point3d position = { 0 }; if (nav_point->nav_index==NONE || nav_point->reference_index==NONE || @@ -953,10 +953,6 @@ void hud_render_nav_points( } break; - /* position is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_assert("c:\\halo\\SOURCE\\interface\\hud_nav_points.c", 725, !"unreachable"); break; @@ -1058,11 +1054,6 @@ static void hud_update_nav_point_local_player( break; } - /* The default arm leaves position unassigned. Not reached: every store to - * nav_point->type in this file writes one of the three types handled above or NONE - * (the map-start memset and the deactivations), the nav point array is private to - * this file, and NONE entries are skipped above. Source-policy approval pending - * (2026-09-27 audit). */ position.z += nav_point->z_offset; nav_point->screen_type = hud_get_nav_point_render_type( local_player_index, diff --git a/source/interface/hud_unit.c b/source/interface/hud_unit.c index f1f2f31a..4b13657a 100644 --- a/source/interface/hud_unit.c +++ b/source/interface/hud_unit.c @@ -607,8 +607,8 @@ void hud_render_damage_indicators( { struct hud_damage_indicators_definition *definition = &hud_globals->damage_indicators; - real theta; - real_point2d screen_position; + real theta = 0.0f; + real_point2d screen_position = { 0 }; real scale = hud_globals_get_scale(local_player_count() > 1); byte damage_indicators[NUMBER_OF_HUD_DAMAGE_INDICATOR_DIRECTIONS]; short direction; @@ -671,10 +671,6 @@ void hud_render_damage_indicators( render.camera.viewport_bounds.y1) / 2); break; - /* theta and screen_position are left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_assert( "c:\\halo\\SOURCE\\interface\\hud_unit.c", diff --git a/source/items/projectiles.c b/source/items/projectiles.c index 8fbb7b8f..77783077 100644 --- a/source/items/projectiles.c +++ b/source/items/projectiles.c @@ -501,10 +501,6 @@ void projectile_export_function_values( value = 0.f; break; - /* value is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: display_assert( NULL, diff --git a/source/math/geometry.c b/source/math/geometry.c index ef23db87..15e1f9a8 100644 --- a/source/math/geometry.c +++ b/source/math/geometry.c @@ -721,8 +721,8 @@ short convex_hull2d( real accumulated_angle = 0.f; real minimum_x = REAL_MAX; real minimum_y = REAL_MAX; - short current_index; - short best_index; + short current_index = 0; + short best_index = 0; short index; for (index = 0; index < vertex_count; index++) @@ -765,11 +765,6 @@ short convex_hull2d( break; } - /* current_index and best_index are assigned only inside the scans. For finite - * points with dimension 2 both scans assign them; a NaN or infinite coordinate can - * leave either unassigned. Not shown reachable: the callers' points (king hill - * markers, lens flare projections) were not traced for non-finite values. - * Source-policy approval pending (2026-09-27 audit). */ hull_indices[hull_count++] = current_index; for (index = 0; index < vertex_count; index++) diff --git a/source/math/periodic_functions.c b/source/math/periodic_functions.c index afc767ca..5e257468 100644 --- a/source/math/periodic_functions.c +++ b/source/math/periodic_functions.c @@ -387,10 +387,6 @@ static void transition_function_build_table( case _transition_function_cosine: result = ((real)sin(value*3.1415927f-1.5707964f)+1.0f)*0.5f; break; - /* result is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: display_assert(NULL, "c:\\halo\\SOURCE\\math\\periodic_functions.c", 411, TRUE); system_exit(-1); @@ -479,10 +475,6 @@ static void periodic_function_build_table( result = (real)fmod((double)random_x, 1.0); result *= result; break; - /* result is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: display_assert(NULL, "c:\\halo\\SOURCE\\math\\periodic_functions.c", 499, TRUE); system_exit(-1); diff --git a/source/memory/byte_swapping.c b/source/memory/byte_swapping.c index f3e6fac6..909045c4 100644 --- a/source/memory/byte_swapping.c +++ b/source/memory/byte_swapping.c @@ -188,7 +188,7 @@ static void _byte_swap_data( { long code; long offset; - long code_index; + long code_index = 0; register long array_count; long repeat_count; long local_size; @@ -334,12 +334,6 @@ next_iteration: } while (repeat_count!=0); -/* A zero array count reaches done with code_index never assigned, and it is stored - * through next_code. Not reached with January's code tables: all 15 _begin_bs_array - * entries in cachebeta.exe's data sections have count 1 (scan of every aligned - * _begin_bs_array word from .rdata onwards), and every byte_swap_definition in the - * source points at a static table. Source-policy approval pending (2026-09-27 audit). - */ done: if (size) *size = offset; diff --git a/source/memory/data_encoding.c b/source/memory/data_encoding.c index ef722a03..f4965238 100644 --- a/source/memory/data_encoding.c +++ b/source/memory/data_encoding.c @@ -528,10 +528,6 @@ void *data_decode_array( case -8: element_count = (long)data_decode_int64(state); break; - /* element_count is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: display_assert(NULL, "c:\\halo\\SOURCE\\memory\\data_encoding.c", 370, TRUE); system_exit(-1); diff --git a/source/rasterizer/rasterizer_debug.c b/source/rasterizer/rasterizer_debug.c index abd08e5d..9b53f923 100644 --- a/source/rasterizer/rasterizer_debug.c +++ b/source/rasterizer/rasterizer_debug.c @@ -78,11 +78,6 @@ enum MAXIMUM_VERTICES_PER_DEBUG_PRIMITIVE = 3 }; -enum -{ - RASTERIZER_DEBUG_LINE_ZBIAS = 16 -}; - enum { _rasterizer_stats_none = 0, @@ -388,7 +383,7 @@ void rasterizer_debug_draw( } rasterizer_dynamic_vertices_unlock(vertex_buffer_index); - rasterizer_debug_drawing_begin(TRUE, 0); + rasterizer_debug_drawing_begin(TRUE); rasterizer_draw_dynamic_vertices( 0, triangle_count, @@ -440,7 +435,7 @@ void rasterizer_debug_draw( } rasterizer_dynamic_vertices_unlock(vertex_buffer_index); - rasterizer_debug_drawing_begin(TRUE, RASTERIZER_DEBUG_LINE_ZBIAS); + rasterizer_debug_drawing_begin(TRUE); rasterizer_draw_dynamic_vertices( 0, line_count, @@ -485,7 +480,7 @@ void rasterizer_debug_draw( primitive->vertices, primitive->vertex_count*sizeof(struct rasterizer_debug_vertex)); rasterizer_dynamic_vertices_unlock(vertex_buffer_index); - rasterizer_debug_drawing_begin(FALSE, 0); + rasterizer_debug_drawing_begin(FALSE); rasterizer_draw_dynamic_vertices( 0, 1, diff --git a/source/rasterizer/xbox/rasterizer_xbox.c b/source/rasterizer/xbox/rasterizer_xbox.c index 3a6ce72c..fe416c72 100644 --- a/source/rasterizer/xbox/rasterizer_xbox.c +++ b/source/rasterizer/xbox/rasterizer_xbox.c @@ -3509,10 +3509,6 @@ void *rasterizer_get_bitmap_default_hardware_format( hardware_format = rasterizer_globals.default_cm_hardware_format; break; - /* hardware_format is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: display_assert( "### ERROR unsupported bitmap type", diff --git a/source/rasterizer/xbox/rasterizer_xbox_debug.h b/source/rasterizer/xbox/rasterizer_xbox_debug.h index 9a02982c..2f871c64 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_debug.h +++ b/source/rasterizer/xbox/rasterizer_xbox_debug.h @@ -12,15 +12,8 @@ RASTERIZER_XBOX_DEBUG.H /* ---------- prototypes/RASTERIZER_XBOX_DEBUG.C */ -/* rasterizer_xbox_debug.c defines rasterizer_debug_drawing_begin with the single - * parameter opaque, as rasterizer.h declares it. This declaration, used only by - * rasterizer_debug.c, adds zbias because January's rasterizer_debug_draw (0x56d9e0 - * +0x20e, +0x2ee, +0x3a0) pushes two arguments while the callee (0x549db0) reads only - * [ebp+8]. The two declarations conflict (C4031); under cdecl the callee ignores the - * extra argument. Source-policy approval pending (2026-09-27 audit). */ void rasterizer_debug_drawing_begin( - boolean opaque, - long zbias); + boolean opaque); void rasterizer_debug_drawing_end( void); diff --git a/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c b/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c index afc971bc..53a07012 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c +++ b/source/rasterizer/xbox/rasterizer_xbox_transparent_geometry.c @@ -2188,7 +2188,7 @@ void rasterizer_transparent_geometry_group_draw( { real vsh_constants__texscale[3][4]; short fade_source = shader_transparent_generic->generic.framebuffer_fade_source; - unsigned long combiner_constant; + unsigned long combiner_constant = 0; vsh_constants__texscale[0][0] = 0.0f; vsh_constants__texscale[0][1] = 0.0f; @@ -2247,10 +2247,6 @@ void rasterizer_transparent_geometry_group_draw( combiner_constant = 0x05; break; - /* combiner_constant is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert( "c:\\halo\\SOURCE\\rasterizer\\xbox\\rasterizer_xbox_transparent_geometry.c", @@ -2717,7 +2713,7 @@ void rasterizer_transparent_geometry_group_draw( { real vsh_constants__texscale[3][4]; short fade_source = shader_transparent_chicago->chicago.framebuffer_fade_source; - unsigned long combiner_constant; + unsigned long combiner_constant = 0; vsh_constants__texscale[0][0] = 0.0f; vsh_constants__texscale[0][1] = 0.0f; @@ -2778,10 +2774,6 @@ void rasterizer_transparent_geometry_group_draw( combiner_constant = 0x05; break; - /* combiner_constant is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert( "c:\\halo\\SOURCE\\rasterizer\\xbox\\rasterizer_xbox_transparent_geometry.c", diff --git a/source/render/render_particles.c b/source/render/render_particles.c index ce777f27..9ef213c0 100644 --- a/source/render/render_particles.c +++ b/source/render/render_particles.c @@ -198,7 +198,7 @@ void render_particles( { struct rendered_particle_datum *rendered_particle = rendered_particles; - short *current_sprite_group; + short *current_sprite_group = NULL; short previous_definition_index = NONE; short previous_cluster_index = NONE; boolean previous_attached_to_first_person_weapon = FALSE; @@ -212,12 +212,6 @@ void render_particles( rendered_particle->attached_to_first_person_weapon == previous_attached_to_first_person_weapon) { - /* current_sprite_group is not initialised. Not reached unassigned: the first - * rendered particle cannot take this arm, because its definition index differs from - * the initial NONE (particle_new returns for NONE and otherwise calls - * particle_definition_get, whose tag lookup halts unless the low word of the index - * is non-negative; the field is written nowhere else). Source-policy approval - * pending (2026-09-27 audit). */ (*current_sprite_group)++; } else diff --git a/source/sound/sound_manager.c b/source/sound/sound_manager.c index 3e29c53f..138111c2 100644 --- a/source/sound/sound_manager.c +++ b/source/sound/sound_manager.c @@ -2099,7 +2099,7 @@ static real source_distance_squared( short listener_index, struct sound_source *source) { - real distance_squared; + real distance_squared = 0.0f; switch (source->spatialization_mode) { @@ -2124,10 +2124,6 @@ static real source_distance_squared( source->location.position.z * source->location.position.z; break; - /* distance_squared is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_vassert( "c:\\halo\\SOURCE\\sound\\sound_manager.c", diff --git a/source/units/unit_dialogue.c b/source/units/unit_dialogue.c index 4ac63380..f926fd9f 100644 --- a/source/units/unit_dialogue.c +++ b/source/units/unit_dialogue.c @@ -897,7 +897,7 @@ boolean unit_scream( short scream_type) { struct unit_datum *unit = unit_get(unit_index); - short vocalization_type; + short vocalization_type = 0; match_assert( "c:\\halo\\SOURCE\\units\\unit_dialogue.c", @@ -927,10 +927,6 @@ boolean unit_scream( case _unit_scream_resurrection: vocalization_type = _vocalization_resurrect; break; - /* vocalization_type is left unassigned only by this default arm. Not reached unassigned: the - * arm's assertion failure calls system_exit, which does not return in January - * (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit). - * Source-policy approval pending (2026-09-27 audit). */ default: match_assert( "c:\\halo\\SOURCE\\units\\unit_dialogue.c", From d2256722caba6524e403d4ec886a2b280e3f75ef Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 05:28:58 -0400 Subject: [PATCH 141/187] Disc image setup: only plain file names written The maps folder copied out of a disc image takes only plain file names (printable, no path or drive characters), and on the desktop ports is never written through a link. (cherry picked from commit 7deed0430bb01510e4df431fae89a5c9675010d2) (cherry picked from commit 106f1c6eb6e98646f36c9774a7c1750c78662cfe) --- .../java/com/halo/decomp/XisoExtractor.java | 19 +++++++++-- port/linux/src/xiso.c | 33 ++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/port/android/app/src/main/java/com/halo/decomp/XisoExtractor.java b/port/android/app/src/main/java/com/halo/decomp/XisoExtractor.java index 31ffe28a..95ce9372 100644 --- a/port/android/app/src/main/java/com/halo/decomp/XisoExtractor.java +++ b/port/android/app/src/main/java/com/halo/decomp/XisoExtractor.java @@ -135,6 +135,20 @@ final class XisoExtractor { return true; } + /** whether a name is one to write in the maps folder: no path, no "." or + * "..", and only printable ASCII that every file system takes as a plain + * name (a disc's maps folder holds .map files and loading.tga) */ + static boolean isPlainName(String name) { + if (name.isEmpty() || name.equals(".") || name.equals("..")) + return false; + for (int index = 0; index < name.length(); index++) { + char c = name.charAt(index); + if (c < 0x20 || c > 0x7E || "/\\:*?\"<>|".indexOf(c) >= 0) + return false; + } + return true; + } + /** the partition's volume descriptor: the root directory's sector and size */ private long[] findVolume() throws IOException { for (long offset : PARTITION_OFFSETS) { @@ -190,8 +204,9 @@ final class XisoExtractor { for (int index = 0; index < nameLength; index++) bytes[index] = table.get(at + ENTRY_HEADER_SIZE + index); String name = new String(bytes, StandardCharsets.ISO_8859_1); - /* (as extract-xiso refuses them: no name may leave the folder) */ - if (!name.equals(".") && !name.equals("..") && name.indexOf('/') < 0 && name.indexOf('\\') < 0) + /* (as extract-xiso refuses them: no name may leave the folder, and + none but a plain file name is written) */ + if (isPlainName(name)) out.add(new Entry(name, table.getInt(at + 4) & 0xFFFFFFFFL, table.getInt(at + 8) & 0xFFFFFFFFL)); } if (right != 0) diff --git a/port/linux/src/xiso.c b/port/linux/src/xiso.c index 066b4300..c9b08adb 100644 --- a/port/linux/src/xiso.c +++ b/port/linux/src/xiso.c @@ -76,6 +76,9 @@ includes software developed by in . #ifndef O_CLOEXEC #define O_CLOEXEC 0 #endif +#ifndef O_NOFOLLOW +#define O_NOFOLLOW 0 +#endif enum { @@ -110,6 +113,8 @@ struct xiso_image int error_size; }; +static int name_is_plain(const char *name); + static unsigned long read_u32(const unsigned char *bytes) { return (unsigned long)bytes[0] | (unsigned long)bytes[1] << 8 | (unsigned long)bytes[2] << 16 | @@ -224,9 +229,9 @@ static void walk_directory(struct directory_walk *walk, unsigned long offset, in file->name[name_length] = 0; file->sector = read_u32(entry + 4); file->size = read_u32(entry + 8); - /* (as extract-xiso refuses them: no name may leave the folder) */ - if (strcmp(file->name, ".") && strcmp(file->name, "..") && !strchr(file->name, '/') && - !strchr(file->name, '\\')) + /* (as extract-xiso refuses them: no name may leave the folder, and + none but a plain file name is written) */ + if (name_is_plain(file->name)) { walk->entry_count++; } @@ -235,6 +240,25 @@ static void walk_directory(struct directory_walk *walk, unsigned long offset, in walk_directory(walk, right, depth + 1); } +/* whether a name is one to write in the maps folder: no path, no "." or +"..", and only printable ASCII that every desktop file system takes as a +plain name (a disc's maps folder holds .map files and loading.tga) */ +static int name_is_plain(const char *name) +{ + const char *at; + + if (!*name || !strcmp(name, ".") || !strcmp(name, "..")) + return 0; + for (at = name; *at; at++) + { + unsigned char c = (unsigned char)*at; + + if (c < 0x20 || c > 0x7e || strchr("/\\:*?\"<>|", c)) + return 0; + } + return 1; +} + static int names_match(const char *a, const char *b) { for (; *a && *b; a++, b++) @@ -254,7 +278,8 @@ static int copy_file(struct xiso_image *image, const struct xiso_file *file, con { unsigned long long offset = image->partition + (unsigned long long)file->sector * SECTOR_SIZE; unsigned long remaining = file->size; - int output = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + /* (not through a link left in the folder) */ + int output = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0644); if (output < 0) return fail(image, "Could not write %s.", path); From 1ac8186d6ca0c6d78e4c18af990eff1ff58563ca Mon Sep 17 00:00:00 2001 From: thelinkin3000 Date: Sun, 4 Oct 2026 06:58:49 +0000 Subject: [PATCH 142/187] Scan the motion sensor for up to four local players The scan kept each local player's blip count and the scan order in one array of four, the counts first and the order from 2, and had two camera positions: all the Xbox's two player co-op needed. With three or four local players in split screen they were written past their ends and into the function's stack guard, which halted the game as it started drawing the HUD ("corrupt stack at 0!"). The counts and the order are now arrays of their own, and there is a camera position for every local player. (cherry picked from commit 266a556967273bfcf096c1e6009acdb7a5ee045d) (cherry picked from commit c02cb986c2c5acc6426fcb043f39d1062617f7f7) (cherry picked from commit ee9cbf17d1422f2b57b5ca07c98010ece1bcbad6) --- source/interface/motion_sensor.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/source/interface/motion_sensor.c b/source/interface/motion_sensor.c index 9607f857..be07f7fe 100644 --- a/source/interface/motion_sensor.c +++ b/source/interface/motion_sensor.c @@ -329,11 +329,18 @@ static void motion_sensor_update( } else { - short local_player_slots[4] = { 0 }; + /* port: each local player's blip count, by local player, and the + local players in scan order, as two arrays of four. The Xbox's kept + both in one array of four (the counts first, the order from 2) and + had two camera positions, all it needed for its two player co-op; + three or four local players (split screen) wrote past both and into + stack_buffer ("corrupt stack at 0") */ + short blip_counts[MAXIMUM_LOCAL_PLAYERS] = { 0 }; + short scan_players[MAXIMUM_LOCAL_PLAYERS]; short player_count = local_player_count(); short local_player_index = local_player_get_next(NONE); struct object_iterator iter; - real_point3d camera_positions[2]; + real_point3d camera_positions[MAXIMUM_LOCAL_PLAYERS]; boolean done = FALSE; short player_scan_index; @@ -352,7 +359,7 @@ static void motion_sensor_update( local_player_get_player_index(local_player_index))->unit_index; short blip_index; - local_player_slots[player_scan_index + 2] = local_player_index; + scan_players[player_scan_index] = local_player_index; camera_positions[local_player_index].x = 0.0f; camera_positions[local_player_index].y = 0.0f; camera_positions[local_player_index].z = 0.0f; @@ -397,7 +404,7 @@ static void motion_sensor_update( player_scan_index++) { short scan_player_index = - local_player_slots[player_scan_index + 2]; + scan_players[player_scan_index]; long player_index = local_player_get_player_index(scan_player_index); @@ -406,7 +413,7 @@ static void motion_sensor_update( local_player_get_player_index(scan_player_index)) ->unit_index != NONE) { - short blip_index = local_player_slots[scan_player_index]; + short blip_index = blip_counts[scan_player_index]; if (blip_index >= MAXIMUM_MOTION_SENSOR_BLIPS) { @@ -452,7 +459,7 @@ static void motion_sensor_update( player->unit_indices[blip_index] = iter.index; sensor->blip_count++; - local_player_slots[scan_player_index] = + blip_counts[scan_player_index] = (short)(blip_index + 1); } } From 0fb9b576055a29f59bb1618eab6266c4b86df015 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 05:23:48 -0400 Subject: [PATCH 143/187] Telnet console: a client sending an HTTP request is dropped The console runs what it is sent as scripts. A browser can be made to send an HTTP request to a local port; such a client is now dropped at its first line instead of having its lines run. (cherry picked from commit faa2a331383f4dedce63487d813b7348c00b6f26) (cherry picked from commit 40a6262e9be05db11317522f9ee9424405124025) --- source/networking/telnet_console.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/source/networking/telnet_console.c b/source/networking/telnet_console.c index 1be0f0fb..fb83db23 100644 --- a/source/networking/telnet_console.c +++ b/source/networking/telnet_console.c @@ -319,6 +319,27 @@ static void telnet_client_disconnect( return; } +/* port: whether a line is part of an HTTP request. A web page can make the +browser send one to this machine's port, whose lines would otherwise run as +scripts: such a client is dropped at its first line. No script starts with +a request's method and a path, nor holds " HTTP/". */ +static boolean telnet_line_is_http( + char const *line) +{ + static char const *const methods[] = + { + "GET ", "POST ", "PUT ", "HEAD ", "DELETE ", "OPTIONS ", "PATCH ", "CONNECT ", "TRACE ", + }; + long index; + + for (index = 0; index < (long)NUMBEROF(methods); index++) + { + if (!csstrncmp(line, methods[index], csstrlen(methods[index]))) + return TRUE; + } + return strstr(line, " HTTP/") != NULL; +} + /* FALSE when the client was lost (it is then dropped) */ static boolean process_telnet_client_buffer( char *buffer, @@ -367,6 +388,12 @@ static boolean process_telnet_client_buffer( expression[TELNET_CLIENT_BUFFER_SIZE-1] = 0; client->buffer[0] = 0; + if (telnet_line_is_http(expression)) + { + telnet_client_disconnect(client); + error(2, "dropped a telnet client that sent an HTTP request"); + return TRUE; + } if (hs_compile_and_evaluate(expression)) { telnet_client_write(client, "\r\n", 2); From 8fd4f06a900567e8988b971a52e4363a9e8d720f Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:17:11 -0400 Subject: [PATCH 144/187] Windows: the game starts on Windows 7 clock_gettime's wall clock imported GetSystemTimePreciseAsFileTime, which KERNEL32 has only from Windows 8, so Windows 7 refused to load halo.exe. It is looked up at run time, with GetSystemTimeAsFileTime where it is missing. It was the only import newer than Windows 7 in halo.exe; SDL3.dll has none. Fixes #77 (cherry picked from commit 8b862537ef55592ae496c2b9dde6f840baa8ebcf) (cherry picked from commit affe58156dfe9ce6fd4ae46765f50ee43560dfac) --- port/windows/src/win32_posix.c | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/port/windows/src/win32_posix.c b/port/windows/src/win32_posix.c index fe48ed00..b79c3304 100644 --- a/port/windows/src/win32_posix.c +++ b/port/windows/src/win32_posix.c @@ -308,6 +308,28 @@ int pthread_cond_broadcast(pthread_cond_t *condition) /* ---------- time */ +typedef VOID (WINAPI *system_time_proc)(LPFILETIME); + +/* the wall clock: GetSystemTimePreciseAsFileTime where Windows has it +(Windows 8 and later), else GetSystemTimeAsFileTime. Importing the precise +one directly would keep the game from starting on Windows 7. Threads that +race here store the same pointer. */ +static void system_time(FILETIME *now) +{ + static system_time_proc volatile proc; + system_time_proc get_time = proc; + + if (!get_time) + { + get_time = (system_time_proc)(void *)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), + "GetSystemTimePreciseAsFileTime"); + if (!get_time) + get_time = GetSystemTimeAsFileTime; + proc = get_time; + } + get_time(now); +} + int clock_gettime(clockid_t clock, struct timespec *time) { if (clock == CLOCK_MONOTONIC) @@ -327,7 +349,7 @@ int clock_gettime(clockid_t clock, struct timespec *time) FILETIME now; unsigned long long intervals; - GetSystemTimePreciseAsFileTime(&now); + system_time(&now); intervals = ((unsigned long long)now.dwHighDateTime << 32) | now.dwLowDateTime; intervals -= 116444736000000000ULL; time->tv_sec = (time_t)(intervals / 10000000ULL); From 129d3483dcf0602eb46b033738dd92cae960500c Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:28:28 -0400 Subject: [PATCH 145/187] Multiplayer: the pause menu's controller no longer moves the player A network game's pause menu pauses nothing, and the controller that moved about it also moved its player: the D-pad and sticks walked and turned them as they chose an item. While a menu of a player's own is up (ui_widgets_active_for_local_player), the player gets no input, as when the director holds it, and the buttons still held as the menu closes are let go of first, as before (ui_widget_delete). Fixes #119 (cherry picked from commit 771a257f25ecf64fe52d84d0dbd77dc11b2a8680) (cherry picked from commit b5fc042ca61cdfeb9a27693b2cb3f65bf93e2c0c) --- source/game/player_control.c | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/source/game/player_control.c b/source/game/player_control.c index 73603f69..429fd821 100644 --- a/source/game/player_control.c +++ b/source/game/player_control.c @@ -195,6 +195,7 @@ symbols in this file: #include "cutscene/cinematics.h" #include "input/input.h" #include "input/input_abstraction.h" +#include "interface/ui_widget.h" #include "interface/player_ui.h" #include "items/weapons.h" #include "main/main.h" @@ -602,6 +603,23 @@ static void player_action_clear( return; } +/* port: whether a menu of the player's own is up (a network game's pause +menu, which pauses nothing): its controller drives the menu, not the player, +whose D-pad and sticks would otherwise move them as they move about it. The +buttons still held as it closes are let go of first (ui_widget_delete). */ +static boolean player_control_port_menu_has_controller( + short local_player_index) +{ + long player_index = local_player_get_player_index(local_player_index); + short gamepad_index; + + if (player_index == NONE) + return FALSE; + gamepad_index = player_get(player_index)->local_player_index; + return gamepad_index >= 0 && gamepad_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS && + ui_widgets_active_for_local_player(gamepad_index); +} + static void handle_one_player_input( short local_player_index, real time_delta_sec) @@ -634,7 +652,8 @@ static void handle_one_player_input( player->desired_angles.yaw); } - if (director_inhibited_input(local_player_index)) + if (director_inhibited_input(local_player_index) || + player_control_port_menu_has_controller(local_player_index)) { csmemset(&input, 0, sizeof(input)); } From 412c5224e13915c9b9ab9bd4d7fdd77cbd06743c Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:40:42 -0400 Subject: [PATCH 146/187] Profiles: a deleted profile's name can be used again Deleting a saved game file moves the files after it down the memory unit's list, but the players kept their profiles' old indices. A profile deleted before the one in use left that one's index naming the next file, often the next profile made: the profile in use was then saved over it, taking its name in the list, while its folder kept the new name, which then read as in use ("a profile with this name already exists") with no such profile shown. The indices the players and the profile editor hold now follow their files down the list, and one naming the file deleted is none. Fixes #148 (cherry picked from commit 4575f6e6ec7010de1036a3a639ee2794181874d8) (cherry picked from commit 4bbd4965afbbaa22cb3a655a181c3770382bc505) --- source/interface/player_ui.c | 29 +++++++++++++++++++++++++ source/interface/player_ui.h | 3 +++ source/saved games/saved_game_files.c | 31 +++++++++++++++++++++++++++ source/saved games/saved_game_files.h | 3 +++ 4 files changed, 66 insertions(+) diff --git a/source/interface/player_ui.c b/source/interface/player_ui.c index 9c92e7e5..5e427f65 100644 --- a/source/interface/player_ui.c +++ b/source/interface/player_ui.c @@ -1210,6 +1210,35 @@ void player0_look_invert_pitch( return; } +/* port: a saved game file deleted (delete_enumerated_saved_game_file): the +files after it moved down its memory unit's list, so the indices of the +players' profiles and the one being edited follow them, and none is left +naming the file deleted. A player profile deleted before the one in use +left that one's index naming the next file, often the next profile made, +which the profile in use was then saved over (taking its name, while its +folder kept the new profile's name, which then read as in use). */ +void player_ui_saved_game_file_removed( + long removed_index) +{ + short local_player_index; + + for (local_player_index = 0; local_player_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS; local_player_index++) + { + player_ui_globals.local_players[local_player_index].active_profile_index = + saved_game_file_index_after_removal( + player_ui_globals.local_players[local_player_index].active_profile_index, + removed_index); + } + player_ui_globals.edit_profile_index = saved_game_file_index_after_removal( + player_ui_globals.edit_profile_index, + removed_index); + player1_last_used_profile_index = saved_game_file_index_after_removal( + player1_last_used_profile_index, + removed_index); + + return; +} + static void clear_profile_edit_data( void) { diff --git a/source/interface/player_ui.h b/source/interface/player_ui.h index 59a51f6c..0478a7fc 100644 --- a/source/interface/player_ui.h +++ b/source/interface/player_ui.h @@ -114,6 +114,9 @@ boolean player_ui_prompt_user_to_rename_edit_profile( void); void player0_look_invert_pitch( boolean invert); +/* port: a saved game file deleted (saved_game_files.c) */ +void player_ui_saved_game_file_removed( + long removed_index); /* ---------- globals */ diff --git a/source/saved games/saved_game_files.c b/source/saved games/saved_game_files.c index c1aa125d..642d5945 100644 --- a/source/saved games/saved_game_files.c +++ b/source/saved games/saved_game_files.c @@ -320,6 +320,7 @@ symbols in this file: #include "saved games/game_state.h" #include "saved games/player_profile.h" #include "saved games/playlist_profile.h" +#include "interface/player_ui.h" #include "text/text_group.h" #include "tag_files/tag_groups.h" /* the saved game file checksum is an XDK content signature, and enumerated @@ -1546,6 +1547,12 @@ boolean delete_enumerated_saved_game_file( error(_error_silent, "remove_nth_entry_in_mapfile() failed"); success = FALSE; } + else + { + /* port: the files after it moved down the list; the + indices the players hold follow them */ + player_ui_saved_game_file_removed(profile_index); + } if (memory_unit != _memory_unit_hard_drive) { @@ -1573,6 +1580,30 @@ boolean delete_enumerated_saved_game_file( return success; } +/* port: a file's index once the file of removed_index has left its memory +unit's list (delete_enumerated_saved_game_file): the files after it move +down one, so their indices do; NONE for the file removed */ +long saved_game_file_index_after_removal( + long profile_index, + long removed_index) +{ + long n; + long removed_n; + + if (profile_index == NONE || removed_index == NONE || + SAVED_GAME_FILE_INDEX_MEMORY_UNIT(profile_index) != SAVED_GAME_FILE_INDEX_MEMORY_UNIT(removed_index)) + { + return profile_index; + } + n = SAVED_GAME_FILE_INDEX_FILE_INDEX(profile_index); + removed_n = SAVED_GAME_FILE_INDEX_FILE_INDEX(removed_index); + if (n == removed_n) + return NONE; + if (n < removed_n) + return profile_index; + return (long)(((unsigned long)profile_index & ~(0xFFFUL << 16)) | ((unsigned long)(n - 1) << 16)); +} + void saved_game_file_get_useable_untitled_profile_name( wchar_t *display_name) { diff --git a/source/saved games/saved_game_files.h b/source/saved games/saved_game_files.h index e4ec1ba0..e397b651 100644 --- a/source/saved games/saved_game_files.h +++ b/source/saved games/saved_game_files.h @@ -89,6 +89,9 @@ long create_enumerated_saved_game_file( wchar_t *display_name); boolean delete_enumerated_saved_game_file( long profile_index); +long saved_game_file_index_after_removal( + long profile_index, + long removed_index); void saved_game_file_get_useable_untitled_profile_name( wchar_t *display_name); void saved_game_files_enumerate_available_to_local_player_index( From d978f663732a6864d066ab8ca9323096fd83666c Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:10:17 -0400 Subject: [PATCH 147/187] Multiplayer: a map with fewer than three multiplayer vehicles game_engine_predict_resources and game_engine_remap_vehicle took the globals' multiplayer vehicles 0, 1 and 2 (warthog, ghost, scorpion) without looking at the block's count. Every retail map has them; a Custom Edition map's globals may not, and the game then stopped on tag_block_get_element's assertion or, with assertions off, read past the block and predicted or compared whatever was there. A missing one is now NONE: not predicted, and matching no placed vehicle. Maps with all three play as before. (cherry picked from commit f1eb47cf3da8cf3e34ad2b6c2e304f994540ab46) (cherry picked from commit 2f282f1aa2342af9619d5b7fcf0d92d1962b9ee8) --- source/game/game_engine.c | 130 +++++++++++++------------------------- 1 file changed, 45 insertions(+), 85 deletions(-) diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 40f95edf..84bb011c 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -7199,73 +7199,61 @@ void game_engine_variant_cleanup( return; } -static void game_engine_predict_resources( - void) +/* port: one of the globals' three multiplayer vehicles (0 warthog, 1 ghost, +2 scorpion), or NONE for one the map's globals lack: a Custom Edition map's +may have fewer, which the original read past the end of */ +static long game_engine_multiplayer_vehicle( + long index) { - struct game_globals *game_globals; - struct game_globals_multiplayer_information *multiplayer_information; - struct game_globals_vehicle *vehicle; - long weapon_indices[10]; - long weapon_index; + struct game_globals *game_globals = scenario_get_game_globals(); + struct game_globals_multiplayer_information *information; - game_globals = scenario_get_game_globals(); - multiplayer_information = TAG_BLOCK_GET_ELEMENT( + if (game_globals->multiplayer_information.count <= 0) + return NONE; + information = TAG_BLOCK_GET_ELEMENT( &game_globals->multiplayer_information, 0, struct game_globals_multiplayer_information); + if (index >= information->vehicles.count) + return NONE; + return TAG_BLOCK_GET_ELEMENT(&information->vehicles, index, struct game_globals_vehicle)->vehicle.index; +} + +static void game_engine_predict_multiplayer_vehicle( + long index) +{ + long definition_index = game_engine_multiplayer_vehicle(index); + + if (definition_index != NONE) + object_definition_predict(definition_index); +} + +static void game_engine_predict_resources( + void) +{ + long weapon_indices[10]; + long weapon_index; - /* port: the cases below take the three multiplayer vehicles the Xbox's - globals always have; a Halo Custom Edition map's can have fewer, and then - gets no vehicle predicted (port/linux/game/custom_edition_cache.c) */ - if (multiplayer_information->vehicles.count >= 3) switch (global_variant.universal_variant.vehicle_set) { case _game_engine_vehicles_warthog: - vehicle = TAG_BLOCK_GET_ELEMENT( - &multiplayer_information->vehicles, - 0, - struct game_globals_vehicle); - object_definition_predict(vehicle->vehicle.index); + game_engine_predict_multiplayer_vehicle(0); break; case _game_engine_vehicles_ghost: - vehicle = TAG_BLOCK_GET_ELEMENT( - &multiplayer_information->vehicles, - 1, - struct game_globals_vehicle); - object_definition_predict(vehicle->vehicle.index); + game_engine_predict_multiplayer_vehicle(1); break; case _game_engine_vehicles_tank: - vehicle = TAG_BLOCK_GET_ELEMENT( - &multiplayer_information->vehicles, - 2, - struct game_globals_vehicle); - object_definition_predict(vehicle->vehicle.index); + game_engine_predict_multiplayer_vehicle(2); break; default: - { - struct tag_block *vehicles = &multiplayer_information->vehicles; - - vehicle = TAG_BLOCK_GET_ELEMENT( - vehicles, - 0, - struct game_globals_vehicle); - object_definition_predict(vehicle->vehicle.index); - vehicle = TAG_BLOCK_GET_ELEMENT( - vehicles, - 1, - struct game_globals_vehicle); - object_definition_predict(vehicle->vehicle.index); - vehicle = TAG_BLOCK_GET_ELEMENT( - vehicles, - 2, - struct game_globals_vehicle); - object_definition_predict(vehicle->vehicle.index); + game_engine_predict_multiplayer_vehicle(0); + game_engine_predict_multiplayer_vehicle(1); + game_engine_predict_multiplayer_vehicle(2); break; } - } object_definition_predict(list_index_to_weapon_definition_index(_weapon_list_frag_grenade)); object_definition_predict(list_index_to_weapon_definition_index(_weapon_list_plasma_grenade)); @@ -7675,32 +7663,16 @@ long game_engine_remap_vehicle( } if (game_engine) { - struct game_globals *game_globals; - struct game_globals_multiplayer_information *multiplayer_information; - struct tag_block *vehicles; - struct game_globals_vehicle *vehicle0; - struct game_globals_vehicle *vehicle1; - struct game_globals_vehicle *vehicle2; - struct game_globals_vehicle *vehicle; - - game_globals = scenario_get_game_globals(); - multiplayer_information = TAG_BLOCK_GET_ELEMENT( - &game_globals->multiplayer_information, - 0, - struct game_globals_multiplayer_information); - vehicle0 = TAG_BLOCK_GET_ELEMENT( - &multiplayer_information->vehicles, - 0, - struct game_globals_vehicle); - vehicles = &multiplayer_information->vehicles; - vehicle1 = TAG_BLOCK_GET_ELEMENT(vehicles, 1, struct game_globals_vehicle); - vehicle2 = TAG_BLOCK_GET_ELEMENT(vehicles, 2, struct game_globals_vehicle); + /* (port: NONE for one the map's globals lack) */ + long vehicle0 = game_engine_multiplayer_vehicle(0); + long vehicle1 = game_engine_multiplayer_vehicle(1); + long vehicle2 = game_engine_multiplayer_vehicle(2); /* (port: and the other types a gametype's sets name, which a map may have: game_engine_variant_vehicle_type) */ - if (result != vehicle0->vehicle.index && - result != vehicle1->vehicle.index && - result != vehicle2->vehicle.index && + if (result != vehicle0 && + result != vehicle1 && + result != vehicle2 && ((game_variant_options_get()->vehicle_set[0] == _game_engine_vehicles_default && game_variant_options_get()->vehicle_set[1] == _game_engine_vehicles_default) || game_engine_variant_vehicle_type(result) == NONE)) @@ -7718,29 +7690,17 @@ long game_engine_remap_vehicle( break; case _game_engine_vehicles_warthog: - vehicle = TAG_BLOCK_GET_ELEMENT( - vehicles, - 0, - struct game_globals_vehicle); - if (vehicle->vehicle.index != result) + if (vehicle0 != result) result = NONE; break; case _game_engine_vehicles_ghost: - vehicle = TAG_BLOCK_GET_ELEMENT( - vehicles, - 1, - struct game_globals_vehicle); - if (vehicle->vehicle.index != result) + if (vehicle1 != result) result = NONE; break; case _game_engine_vehicles_tank: - vehicle = TAG_BLOCK_GET_ELEMENT( - vehicles, - 2, - struct game_globals_vehicle); - if (vehicle->vehicle.index != result) + if (vehicle2 != result) result = NONE; break; } From 56db20924c4c2084dd2db52fb1786b813f3137e4 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:16:13 -0400 Subject: [PATCH 148/187] Audio: a flushed stream's next packet plays from its start A stream's cursor is the place in its first packet not yet played. stream_flush completed a partly played head packet, which leaves the cursor where it was, and let go of the mixer's lock for the game's callback. A mixer pass in that time took the next packet at the old cursor: it played from the middle, or was marked finished at once, and the flush then reported it played (SUCCESS, its whole size) rather than flushed, so the game queued more as it stopped the stream. Completing a packet that was not played to its end now resets the cursor. (cherry picked from commit 3582e112b99d6f47f131ff6ab3439b0439e93c88) (cherry picked from commit dcbffc6a200b525150c7ea25d9091e3f9cffbfdc) --- port/linux/src/dsound_sdl.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 2611ae2b..833e47fa 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -1449,6 +1449,10 @@ static void stream_complete_head(struct sdl_stream *stream, DWORD status, DWORD struct voice_packet *entry = &stream->packets[stream->packet_head]; XMEDIAPACKET packet = entry->packet; + /* (one not played to its end, flushed: the cursor was its place, and the + mixer may take the next while the lock is let go below) */ + if (!entry->finished) + stream->cursor = 0; packet_release(entry); entry->finished = FALSE; stream->packet_head = (stream->packet_head + 1) % MAXIMUM_STREAM_PACKETS; From 89f83cf08d0ee23277c4cacc4aa090e28ed54229 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:17:12 -0400 Subject: [PATCH 149/187] UPnP: no router address in the error A router whose internet address is a private one (another NAT beyond it) was reported with that address in the error text, which goes to the log and can be shown. Addresses are not shown; the error says what it means without it. (cherry picked from commit e59c03e8b399412fb4d4ed6ba7eea1e925451f5b) (cherry picked from commit 2e6e1270f2b7788a09b7e2e3b76878c88f36e281) --- port/linux/src/posix_upnp.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/port/linux/src/posix_upnp.c b/port/linux/src/posix_upnp.c index 30545da7..28e75e07 100644 --- a/port/linux/src/posix_upnp.c +++ b/port/linux/src/posix_upnp.c @@ -151,8 +151,9 @@ static int upnp_find_router(char *error, int error_size) upnp_remove_left_forwardings(); return 1; case UPNP_PRIVATEIP_IGD: + /* (not the address itself: logs and screens show none) */ snprintf(error, (size_t)error_size, - "the router's internet address (%s) is a private one: another NAT is beyond it", wan_address); + "the router's internet address is a private one: another NAT is beyond it"); break; case UPNP_DISCONNECTED_IGD: snprintf(error, (size_t)error_size, "the UPnP router is not connected to the internet"); From f38d526e839dd22ec3ba8554ce58632e50b7ff37 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:29:14 -0400 Subject: [PATCH 150/187] High-res HUD: unfilter each PNG row by its filter The filter was chosen for every byte. Each row now runs one loop for its filter; the pixels are the same (all 261 HUD and title PNGs compared byte for byte). Decoding the 261 PNGs: 659 ms -> 309 ms (M4, without the GL upload). b30, the frame where the HUD first shows (18 textures decoded): 123 ms -> 84 ms of decoding, that frame 122 ms -> 82 ms. (cherry picked from commit 1213d2475bb9d139e3aeb47426da9fc6190c8de9) (cherry picked from commit 02e53d4aa030745162e53877fda910d95353c94a) --- port/linux/src/hud_hires.c | 48 ++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/port/linux/src/hud_hires.c b/port/linux/src/hud_hires.c index 5352c483..439ebdcc 100644 --- a/port/linux/src/hud_hires.c +++ b/port/linux/src/hud_hires.c @@ -185,6 +185,8 @@ static unsigned char *png_decode(const unsigned char *data, unsigned long size, ended when the output is exactly full: all of it is enough) */ if ((result != Z_OK && result != Z_BUF_ERROR) || inflated_size != filtered_size) goto failed; + /* (each row by its filter, the first pixel's 4 bytes, which have none to + their left, apart; the first row has none above: zeroes) */ for (row = 0; row < height; row++) { const unsigned char *line = filtered + row * (stride + 1) + 1; @@ -194,22 +196,38 @@ static unsigned char *png_decode(const unsigned char *data, unsigned long size, if (filter > 4) goto failed; - for (column = 0; column < stride; column++) + if (!above && filter == 2) + filter = 0; /* (up: zero) */ + else if (!above && filter == 4) + filter = 1; /* (Paeth of left, zero and zero: left) */ + switch (filter) { - unsigned char left = column >= 4 ? out[column - 4] : 0; - unsigned char up = above ? above[column] : 0; - unsigned char up_left = above && column >= 4 ? above[column - 4] : 0; - unsigned char predicted; - - switch (filter) - { - case 1: predicted = left; break; - case 2: predicted = up; break; - case 3: predicted = (unsigned char)(((unsigned)left + up) / 2); break; - case 4: predicted = paeth(left, up, up_left); break; - default: predicted = 0; break; - } - out[column] = (unsigned char)(line[column] + predicted); + case 0: + memcpy(out, line, stride); + break; + case 1: + memcpy(out, line, 4); + for (column = 4; column < stride; column++) + out[column] = (unsigned char)(line[column] + out[column - 4]); + break; + case 2: + for (column = 0; column < stride; column++) + out[column] = (unsigned char)(line[column] + above[column]); + break; + case 3: + for (column = 0; column < 4; column++) + out[column] = (unsigned char)(line[column] + (above ? above[column] : 0) / 2); + for (column = 4; column < stride; column++) + out[column] = (unsigned char)(line[column] + + ((unsigned)out[column - 4] + (above ? above[column] : 0)) / 2); + break; + default: + /* (Paeth of zero, up and zero: up) */ + for (column = 0; column < 4; column++) + out[column] = (unsigned char)(line[column] + above[column]); + for (column = 4; column < stride; column++) + out[column] = (unsigned char)(line[column] + paeth(out[column - 4], above[column], above[column - 4])); + break; } } free(compressed); From f7ea5db0e97c425f0913fe90ccd2e4fb630d74a6 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:39:34 -0400 Subject: [PATCH 151/187] Sounds: a range's permutation mask without shifts past its bits A range with no previous permutation (NONE) set the bit NONE as a byte shifted to, 255. (cherry picked from commit a35ddbe6bfcd31d5aa0c501dac52721fc295fa50) (cherry picked from commit cd87b861b1203df530e22e777219255c21408984) --- source/sound/sound_definitions.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/source/sound/sound_definitions.c b/source/sound/sound_definitions.c index bcdc6a35..c8a5ec4a 100644 --- a/source/sound/sound_definitions.c +++ b/source/sound/sound_definitions.c @@ -162,13 +162,19 @@ void try_to_reset_permutations( struct sound_pitch_range *range) { short permutation_count = range->actual_permutation_count; - unsigned long all_permutations_mask = (FLAG(permutation_count) - 1); + /* port: the mask of no more permutations than it has bits, and the + previous one's bit only if there was one (NONE shifted by 255) */ + unsigned long all_permutations_mask = permutation_count >= 32 ? 0xFFFFFFFFUL : + permutation_count > 0 ? (FLAG(permutation_count) - 1) : 0; if ((~range->played_permutation_mask & all_permutations_mask) == 0) { range->played_permutation_mask = 0; - if (permutation_count > 1) - range->played_permutation_mask = FLAG((byte)range->previous_permutation_index); + if (permutation_count > 1 && range->previous_permutation_index >= 0 && + range->previous_permutation_index < 32) + { + range->played_permutation_mask = FLAG(range->previous_permutation_index); + } } return; From acb71c2f884432e9665509347b5cc55a3b510d63 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 04:09:16 -0400 Subject: [PATCH 152/187] Cache file header: its name ends within its field The header's name is read only within its field. (cherry picked from commit d4ff4cc226e25d315ca1db853ebef14953521691) (cherry picked from commit f0dd7c9fa533a95e8e653e203064a985965f4007) --- source/cache/cache_files.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/cache/cache_files.c b/source/cache/cache_files.c index 50be85a3..9db305c5 100644 --- a/source/cache/cache_files.c +++ b/source/cache/cache_files.c @@ -869,7 +869,8 @@ boolean cache_file_header_verify( header->footer_signature != CACHE_FILE_FOOTER_SIGNATURE || header->file_length < 0 || header->file_length > 0x11600000 || - csstrlen(header->name) > 31) + /* port: its name ends within its field (csstrlen read on past it) */ + !memchr(header->name, 0, sizeof(header->name))) { if (fatal) { From eb0f6e07a7da971c5e773e4cddae7717c18e256a Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:50:16 -0400 Subject: [PATCH 153/187] Game speed: a number from 0 to 100 game_speed takes only finite speeds from 0 to 100. (cherry picked from commit 4c6e68a6021204563aedb1f91e9a10d90744b5d8) (cherry picked from commit bdbb182d0fc653960d1baf7a8b46360537eafe32) --- source/game/game_time.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/game/game_time.c b/source/game/game_time.c index 739418be..7a443cc7 100644 --- a/source/game/game_time.c +++ b/source/game/game_time.c @@ -296,6 +296,11 @@ void game_time_set_speed( { match_assert("c:\\halo\\SOURCE\\game\\game_time.c", 562, game_time_globals); + /* port: a speed (a map's script's, game_speed) that is a number, not + negative and not past a hundred: a NaN or an infinity stopped the game's + time for good */ + if (!(speed >= 0.0f && speed <= 100.0f)) + return; game_time_globals->speed = speed; return; From 2db317436693d341e8dcf20d9eb71ac9697e8483 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:49:15 -0400 Subject: [PATCH 154/187] Server: a banned machine's join is refused as banned A machine whose address or hardware id is in bans.txt, or that was dropped for cheating, was told the game is not open when it tried to join again; it is told it is kept out (_rejection_code_blacklisted_machine), as at the ban itself. (cherry picked from commit 73c78610c67cd3827592673f1b06a1d38babe4ac) Adapted: without the dedicated server's drop function it was declared beside (in a server/ folder, not taken), and a machine kept out by a vote (OpenCE's network_votekick.c) is told it is kept out too. (cherry picked from commit c0191dd9beb4bfd26dd73ba0923cb7edeea44d49) --- source/networking/network_server_manager.c | 16 ++++++++++++++++ source/networking/network_server_manager.h | 4 ++++ .../networking/network_server_message_handler.c | 4 +++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index 88730a98..c9910e21 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -2022,6 +2022,18 @@ boolean network_game_server_game_is_valid( return game_is_valid; } +/* port: why network_game_server_accept_client_machine_into_game last +refused a machine: a banned one's, one dropped for cheating, or one kicked +by a vote, is told it is kept out (_rejection_code_blacklisted_machine); any +other, that the game is not open */ +static short network_game_server_refusal_code = _rejection_code_game_is_closed; + +short network_game_server_last_refusal_code( + void) +{ + return network_game_server_refusal_code; +} + boolean network_game_server_accept_client_machine_into_game( struct network_game_server *server, struct network_game_server_client_machine *machine) @@ -2036,6 +2048,7 @@ boolean network_game_server_accept_client_machine_into_game( it by it), not the first free one: another connection's slot gave two machines one index */ machine_index = machine->machine_index; + network_game_server_refusal_code = _rejection_code_game_is_closed; /* port: not a machine of an address dropped for cheating */ { struct transport_address address = { { { 0 } } }; @@ -2048,6 +2061,7 @@ boolean network_game_server_accept_client_machine_into_game( { network_event("refusing a machine @ %s: dropped from this game for cheating", transport_address_to_string(&address)); + network_game_server_refusal_code = _rejection_code_blacklisted_machine; return FALSE; } } @@ -2061,6 +2075,7 @@ boolean network_game_server_accept_client_machine_into_game( network_game_server_hardware_ids[machine_index] : "")) { network_event("refusing a machine @ %s: banned (bans.txt)", transport_address_to_string(&address)); + network_game_server_refusal_code = _rejection_code_blacklisted_machine; return FALSE; } /* (nor one kicked by a vote, for a while: network_votekick.c) */ @@ -2070,6 +2085,7 @@ boolean network_game_server_accept_client_machine_into_game( network_game_server_hardware_ids[machine_index] : "")) { network_event("refusing a machine @ %s: kicked by a vote", transport_address_to_string(&address)); + network_game_server_refusal_code = _rejection_code_blacklisted_machine; return FALSE; } } diff --git a/source/networking/network_server_manager.h b/source/networking/network_server_manager.h index 52fe417d..6810b300 100644 --- a/source/networking/network_server_manager.h +++ b/source/networking/network_server_manager.h @@ -49,6 +49,10 @@ void network_game_generate_join_game_token( void network_game_server_kick_machine( long machine_index, boolean kept_out); +/* port: the rejection code of the last machine +network_game_server_accept_client_machine_into_game refused */ +short network_game_server_last_refusal_code( + void); /* the host's ban command (console.c, hs.c) */ enum { diff --git a/source/networking/network_server_message_handler.c b/source/networking/network_server_message_handler.c index 41d1c874..a268d3e0 100644 --- a/source/networking/network_server_message_handler.c +++ b/source/networking/network_server_message_handler.c @@ -2033,7 +2033,9 @@ static boolean network_game_server_handle_message_client_join_game_request( struct message_server_machine_rejected rejection; struct network_message *reply; - rejection.reason = _network_game_server_rejection_reason_game_not_open; + /* port: a banned machine is told so (it was told the + game is not open) */ + rejection.reason = network_game_server_last_refusal_code(); network_event( "server failed to accept valid client machine '%s' @%s into the game", join_game_request.machine_name, From 6b55f5cca869ad31013c246adbbdf2c5be7195ad Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:54:12 +1300 Subject: [PATCH 155/187] Network version 25 The host's object states now carry a unit's integrated light, with a bit saying it was sent ("Netcode: carry units' integrated lights in the object states"): a client of version 24 would keep a missed or earlier press's light the wrong way, while one of 25 sets it from every state. --- port/linux/include/halo_port_limits.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/port/linux/include/halo_port_limits.h b/port/linux/include/halo_port_limits.h index 1a2f7eb4..3d2ef473 100644 --- a/port/linux/include/halo_port_limits.h +++ b/port/linux/include/halo_port_limits.h @@ -66,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode, in its game's advertisement's reserved bytes (network_server_message_handler.c), which hosts built before there was a version send as zeros: version 0. Raise it with any change to what the machines send each other. */ -#define HALO_PORT_NETWORK_VERSION 24 +#define HALO_PORT_NETWORK_VERSION 25 /* ... the advertisement's reserved bytes: the version (a little-endian word), then flags */ #define HALO_PORT_ADVERTISED_VERSION_OFFSET 0 From b5229d7ad5e5524a98f4a75715563055dc25a3b3 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Sat, 3 Oct 2026 16:32:41 -0500 Subject: [PATCH 156/187] Custom Edition: linear bitmaps with uneven rows draw with the right pitch rasterizer_xbox_bitmap_rebuild_hardware_format pads a linear bitmap's rows to 64 bytes, rounding up; the texture header rounded its pitch down, so a Custom Edition bitmap whose rows were not a whole multiple was drawn skewed. The header now rounds up too (Xbox maps' rows always are a multiple, so they are unchanged). The texture header's pitch only: OpenCE never had the misalignment log the commit's custom_edition_bitmaps.c part removes. (cherry picked from commit 90a057778f76eb96d45a112b95be2a0c1e1d69a2) --- source/cache/xbox_texture_cache.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/cache/xbox_texture_cache.c b/source/cache/xbox_texture_cache.c index 82671304..6249b078 100644 --- a/source/cache/xbox_texture_cache.c +++ b/source/cache/xbox_texture_cache.c @@ -621,8 +621,12 @@ static void texture_cache_initialize_hardware_format( (2 << D3DFORMAT_DIMENSION_SHIFT) | D3DFORMAT_BORDERSOURCE_COLOR | D3DFORMAT_DMACHANNEL_A; + /* port: the pitch rounded up, as rasterizer_xbox_bitmap_rebuild_hardware_format + pads the rows (an Xbox map's linear rows are whole multiples of it; a + Custom Edition map's need not be) */ texture->Size = - ((bitmap_mipmap_get_row_pitch(bitmap, 0) / D3DTEXTURE_PITCH_ALIGNMENT - 1) << D3DSIZE_PITCH_SHIFT) | + (((bitmap_mipmap_get_row_pitch(bitmap, 0) + D3DTEXTURE_PITCH_ALIGNMENT - 1) / D3DTEXTURE_PITCH_ALIGNMENT - 1) + << D3DSIZE_PITCH_SHIFT) | ((bitmap->height - 1) << D3DSIZE_HEIGHT_SHIFT) | (bitmap->width - 1); } From f815e1ccba6faa84bcdb9c90069d3f9db0ce6fc3 Mon Sep 17 00:00:00 2001 From: lantos1618 Date: Tue, 29 Sep 2026 23:27:25 -0400 Subject: [PATCH 157/187] Zoom effect: no ghosting at high resolutions The scope's screen effect (a warp: copies of the screen scaled a little from the center, blended outside the scope's mask) spreads its copies by a radius in the Xbox's pixels. At 640x480 the few copies blended into a radial blur; at several screen pixels to the Xbox's one they stood apart as sharp ghosts. The native builds keep the spread and split it into that many times the passes (halo_screen_scale), each a step of it. (cherry picked from commit b7fa7f742a72924514eef69d50daf31ef7f7f836) (cherry picked from commit d1b40a6fc74ab46641313a6e0095e596494a1e7b) Without its #ifdef HALO_LINUX, which OpenCE's builds never define (it compiled to nothing), and with the copied parameters declared with the others (gnu89). (cherry picked from commit 763567dcc7ad8e6a851fec6bb5cb9f9e10452579) --- port/linux/include/halo_linux_source_fixups.h | 2 ++ port/linux/src/d3d8_gl.c | 8 +++++++ .../xbox/rasterizer_xbox_screen_effect.c | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/port/linux/include/halo_linux_source_fixups.h b/port/linux/include/halo_linux_source_fixups.h index 83c4b95f..3b7ea4b5 100644 --- a/port/linux/include/halo_linux_source_fixups.h +++ b/port/linux/include/halo_linux_source_fixups.h @@ -47,6 +47,8 @@ unsigned char custom_edition_vehicle_placement_allowed(struct scenario_object_da /* the width of the screen the game draws, 480 lines tall: the device's or the display's shape, or 640 (port/linux/src/d3d8_gl.c) */ long halo_screen_width(void); +/* the screen's pixels to the Xbox's one (port/linux/src/d3d8_gl.c) */ +float halo_screen_scale(void); /* takes up a new width between frames (F11); returns the width */ long halo_screen_commit(void); /* the shadow maps' pixels for each of their 128 texels each way, a power of diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 3f1a1bc7..df1d7a1b 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -242,6 +242,14 @@ float halo_screen_pixel_scale(void) return screen_scale[1]; } +/* how many pixels the screen's targets draw to the Xbox's one, the larger +of the two ways (the screen effects' convolutions: rasterizer_xbox_screen_effect.c) */ +float halo_screen_scale(void) +{ + halo_screen_width(); + return screen_scale[0] > screen_scale[1] ? screen_scale[0] : screen_scale[1]; +} + /* display.shadow_resolution: the size the shadow maps are drawn at. Each object's shadow is drawn from above into a 128x128 map, blurred into diff --git a/source/rasterizer/xbox/rasterizer_xbox_screen_effect.c b/source/rasterizer/xbox/rasterizer_xbox_screen_effect.c index 15d82169..68154707 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_screen_effect.c +++ b/source/rasterizer/xbox/rasterizer_xbox_screen_effect.c @@ -492,6 +492,28 @@ void _rasterizer_screen_effect( real_rectangle2d vertex_bounds; unsigned long light_enhancement_input; unsigned long desaturation_input; + struct rasterizer_cinematic_screen_effect_parameters scaled_parameters; + + /* port: the native builds draw the screen at several pixels to the + Xbox's one (halo_screen_scale): a convolution's few copies of the + screen, apart by its radius in the Xbox's pixels, blended into a blur + at 640x480, and at four times the pixels they stand apart as sharp + ghosts (the zoom's warp). The same spread in that many times the + passes, each a step of it, blends them again. */ + if (parameters->convolution_type != _rasterizer_screen_effect_convolution_type_none && !parameters->video_on) + { + long steps = (long)(halo_screen_scale() + 0.999f); + + if (steps > 6) + steps = 6; + if (steps > 1) + { + scaled_parameters = *parameters; + scaled_parameters.convolution_radius /= (real)steps; + parameters = &scaled_parameters; + pass_count *= (short)steps; + } + } if (parameters->video_on) { From 137d15b95f6e5757631ddb2cdf02e44e53df66db Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Thu, 8 Oct 2026 23:20:58 -0500 Subject: [PATCH 158/187] Voice chat: no packet longer than its length's byte The voice messages give a packet's length in a byte, but a frame could be encoded up to 400 bytes: at 48 and 64 kbps, a frame past 255 bytes (a loud one, as Opus's rate varies) had its length cut short, and the host, finding it unequal to what came, dropped it. The packet's limit is now 255, which the encoder fits a louder frame to; 64 kbps's frames are 160 bytes. The voice_audio.h part only (not the commit's d3d8_gl.c comment removal). (cherry picked from commit 003da0187e1bbd52baad8c3f9c3061c2a61ba55b) --- port/linux/src/voice_audio.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/port/linux/src/voice_audio.h b/port/linux/src/voice_audio.h index ecc23328..428e08ba 100644 --- a/port/linux/src/voice_audio.h +++ b/port/linux/src/voice_audio.h @@ -12,8 +12,10 @@ dsound_sdl.c (the mix). /* a frame: 20 ms at 48 kHz, mono */ #define VOICE_FRAME_SAMPLES 960 -/* the longest packet sent or taken (64 kbps is 160 bytes a frame) */ -#define VOICE_MAXIMUM_PACKET 400 +/* the longest packet sent or taken: the voice messages give its length in a +byte (network_voice.c). 64 kbps is 160 bytes a frame; the encoder fits a +louder one to it rather than going over */ +#define VOICE_MAXIMUM_PACKET 255 /* opens the microphone (on another thread: not at once) or closes it; whether it is open */ From c875cbda5d408a5082152668578bac8d92bc6217 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Sun, 4 Oct 2026 00:06:34 -0500 Subject: [PATCH 159/187] Sound: play a Custom Edition sound louder than a channel at full gain Some maps' sounds have gain modifiers above 1 (foundation's Reach sniper rifle fire, 1.5; ten on Hornet's Nest). The game asserted on them (a halt in a debug build; in a release build the volume went on to assert again in dsound_volume_from_gain); it now clamps the gain to 1, as Halo PC plays them. Written for OpenCE's dsound_channel_set_properties: the original change only joined a 32-bit build to its 64-bit build's clamp, which OpenCE does not have. (cherry picked from commit 312960464ca0868746bfea19eb004635a5acbb8e) --- source/sound/sound_dsound_xbox.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/source/sound/sound_dsound_xbox.c b/source/sound/sound_dsound_xbox.c index 7db31f1e..d940b50b 100644 --- a/source/sound/sound_dsound_xbox.c +++ b/source/sound/sound_dsound_xbox.c @@ -2430,12 +2430,16 @@ static void dsound_channel_set_properties( boolean gain_only) { struct sound_channel *channel= channel_get(channel_index); - real gain= dsound_globals.pause_gain*properties->gain; + /* port: a Custom Edition map's sound can be louder than a channel plays + (foundation's Reach sniper rifle fire has a gain modifier of 1.5); played + at full gain, as Halo PC does, rather than halting */ + real prop_gain = PIN(properties->gain, 0.f, 1.f); + real gain= dsound_globals.pause_gain*prop_gain; match_assert( "c:\\halo\\SOURCE\\sound\\sound_dsound_xbox.c", 980, - properties->gain>=0.f && properties->gain<=1.f); + prop_gain>=0.f && prop_gain<=1.f); match_assert( "c:\\halo\\SOURCE\\sound\\sound_dsound_xbox.c", 981, From fee00e9c7919d37575068043dbabdc34a2bec6ce Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:16:23 -0400 Subject: [PATCH 160/187] Particles: a Halo PC map's particle of more bitmaps than a draw takes Particles of one kind are drawn together, at most 8 bitmaps' worth. A Halo PC map's particle may have a bitmap a frame: beavercreek_rev_beta's fluid bursts have up to 56. A debug build halted ("a build_sprites_begin call can accomodate at most 8 bitmaps") when such particles were in view; a release build drew the first 8 bitmaps' sprites and not the others. The builds that play Halo PC's maps now do as a release build does in a debug build too, and log it once. The Xbox's maps' particles stay within 8, and the Xbox build is unchanged. Without its #ifdef HALO_CUSTOM_EDITION, which OpenCE never defines (it compiled to nothing): the log is for Custom Edition maps (custom_edition_cache_tags_loaded), the assertion stays for the Xbox's. (cherry picked from commit afa6413f1e922621cc04e7953e730d5571b3aae6) --- source/render/render_sprite.c | 38 ++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/source/render/render_sprite.c b/source/render/render_sprite.c index 66baec47..12ce6aa3 100644 --- a/source/render/render_sprite.c +++ b/source/render/render_sprite.c @@ -89,6 +89,7 @@ symbols in this file: #include "cache/texture_cache.h" #include "tag_files/tag_files.h" #include "tag_files/tag_groups.h" +#include "custom_edition_cache.h" /* port: port/linux/game/custom_edition_cache.c */ /* ---------- constants */ @@ -777,15 +778,34 @@ static short build_sprite_get_group( break; } - match_vassert( - "c:\\halo\\SOURCE\\render\\render_sprite.c", - 275, - group_index < data->group_count || - data->group_count < MAXIMUM_BUILD_SPRITE_GROUPS, - csprintf( - temporary, - "a build_sprites_begin call can accomodate at most %d bitmaps", - MAXIMUM_BUILD_SPRITE_GROUPS)); + /* port: a Halo PC map's particle may be drawn from more bitmaps than a + call takes (beavercreek_rev_beta's fluid bursts have a bitmap a frame, up + to 56), which halted a debug build: its sprites on the others are not + drawn, as a release build already did, and it is logged once. The Xbox's + maps' particles stay within the limit. */ + if (custom_edition_cache_tags_loaded()) + { + static boolean logged; + + if (group_index >= data->group_count && data->group_count >= MAXIMUM_BUILD_SPRITE_GROUPS && !logged) + { + error(_error_silent, "%s: a particle's sprites on more than %d bitmaps; those past them are not drawn", + tag_get_name(data->bitmap_group_index), MAXIMUM_BUILD_SPRITE_GROUPS); + logged = TRUE; + } + } + else + { + match_vassert( + "c:\\halo\\SOURCE\\render\\render_sprite.c", + 275, + group_index < data->group_count || + data->group_count < MAXIMUM_BUILD_SPRITE_GROUPS, + csprintf( + temporary, + "a build_sprites_begin call can accomodate at most %d bitmaps", + MAXIMUM_BUILD_SPRITE_GROUPS)); + } if (group_index < data->group_count || data->group_count < MAXIMUM_BUILD_SPRITE_GROUPS) From 14ebfae2301b0ba384b75c481eff67fc5d4647c6 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Sun, 4 Oct 2026 18:35:14 -0500 Subject: [PATCH 161/187] Menus: the pointer no longer runs a list through all its items A list whose focus reached its first or last row scrolled on and moved the focus a row in; the pointer hovering that row took the focus back the next frame it moved, so the list ran through everything and the row under the pointer never stayed lit (the game types, the lobby, the internet lobby). A list now scrolls on at its end only when the d-pad (keys, controller, the wheel) put the focus there (ui_widget_port_pointer_focused). (cherry picked from commit e74f5bbb541a480a72871adeb719472908607d12) --- port/linux/game/menu_functions.c | 23 +++++++++++++++++------ source/interface/ui_widget.c | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 8ca3d137..554c2038 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -1506,6 +1506,17 @@ static boolean color_list_initialize(struct widget_instance *list) return TRUE; } +/* (ui_widget.c) whether the pointer moved the menus' focus last */ +boolean ui_widget_port_pointer_focused(void); + +/* Whether a list whose focus is on its first or last row moves on: when the +d-pad put it there. The pointer resting there would make it move every +frame; the wheel scrolls it instead. */ +static boolean list_scrolls_at_end(void) +{ + return !ui_widget_port_pointer_focused(); +} + /* a list of more items than its rows: the row chosen kept off its ends while there are more past them, the list moving instead; the item chosen, or NONE (its buttons) */ @@ -1513,12 +1524,12 @@ static short list_scroll(struct widget_instance *list, short *first, short count { short row = focused_row(list); - if (row == rows - 1 && *first + rows < count) + if (row == rows - 1 && *first + rows < count && list_scrolls_at_end()) { (*first)++; focus_row(list, --row); } - else if (row == 0 && *first > 0) + else if (row == 0 && *first > 0 && list_scrolls_at_end()) { (*first)--; focus_row(list, ++row); @@ -2674,13 +2685,13 @@ static void gametype_list_update(struct widget_instance *list) if (row == list->focused_child && row_index >= 1 && row_index <= GAMETYPE_ROWS) focused = (short)(row_index - 1); } - if (focused == count - 1 && multiplayer.gametype_first + count < multiplayer.bank_count) + if (focused == count - 1 && multiplayer.gametype_first + count < multiplayer.bank_count && list_scrolls_at_end()) { multiplayer.gametype_first++; focus_row(list, (short)focused); focused--; } - else if (focused == 0 && multiplayer.gametype_first > 0) + else if (focused == 0 && multiplayer.gametype_first > 0 && list_scrolls_at_end()) { multiplayer.gametype_first--; focus_row(list, 2); @@ -3425,12 +3436,12 @@ static void lobby_browser_update(struct widget_instance *list) lobby_browser.count = lobby_browser_valid_games(lobby_browser.games, (short)p2p_lobby_games(lobby_browser.games, LOBBY_BROWSER_GAMES)); - if (focused == BROWSER_ROWS - 1 && lobby_browser.first + BROWSER_ROWS < lobby_browser.count) + if (focused == BROWSER_ROWS - 1 && lobby_browser.first + BROWSER_ROWS < lobby_browser.count && list_scrolls_at_end()) { lobby_browser.first++; lobby_browser_focus_row(list, --focused); } - else if (focused == 0 && lobby_browser.first > 0) + else if (focused == 0 && lobby_browser.first > 0 && list_scrolls_at_end()) { lobby_browser.first--; lobby_browser_focus_row(list, ++focused); diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 884a4722..8f1d1298 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -5672,6 +5672,10 @@ static short ui_mouse_presses[UI_MOUSE_MAXIMUM_PRESSES]; static long ui_mouse_press_count = 0; static boolean ui_mouse_hover_pending = FALSE; static boolean ui_mouse_click_pending = FALSE; +/* whether the pointer moved the focus last (the d-pad clears it): a list +then doesn't scroll on at its end, which the pointer, moving, would make it +do every frame (menu_functions.c) */ +static boolean ui_mouse_focused_last = FALSE; static short ui_mouse_hover_x, ui_mouse_hover_y; static short ui_mouse_click_x, ui_mouse_click_y; /* whether the latest pointer read was the touchscreen: the taller legend @@ -6420,6 +6424,7 @@ static void ui_mouse_give_focus( { struct widget_instance *ancestor; + ui_mouse_focused_last = TRUE; if (ui_mouse_widget_has_focus(widget)) return; widget_instance_give_focus_directly(widget_instance_get_topmost_parent(widget), widget); @@ -6433,6 +6438,14 @@ static void ui_mouse_give_focus( return; } +/* port: whether the pointer moved the menus' focus last, rather than the +d-pad (menu_functions.c's lists) */ +boolean ui_widget_port_pointer_focused( + void) +{ + return ui_mouse_focused_last; +} + /* the d-pad buttons that step a widget back and forward */ static void ui_mouse_list_directions( struct widget_instance *widget, @@ -7665,6 +7678,12 @@ static void widget_instance_process_one_event_recursive( "c:\\halo\\SOURCE\\interface\\ui_widget.c", 3067, widget && definition && event && return_widget_deleted); + if (event->type == _event_type_button && + event->data.button.index >= _widget_event_dpad_up && + event->data.button.index <= _widget_event_dpad_right) + { + ui_mouse_focused_last = FALSE; + } if (event->type == _event_type_button && event->data.button.value > 1 && event->controller_index >= 0 && From 02eae329d05531e8d2e4607d54e9ab087d47a800 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Tue, 6 Oct 2026 18:56:31 -0500 Subject: [PATCH 162/187] Pause menu centred on Custom Edition maps Halo PC's pause list (five rows, which Custom Edition maps keep two of) has room for SETTINGS and END GAME: the buttons are centred in it rather than added below, which pushed the column down the box. The Xbox's two-row list grows as before. The menu_tags.c part only, written for OpenCE's tags (no XBOX_POINTER); the weapon HUD's magazine overlays the commit also fixed came with #180. (cherry picked from commit 91566224a8bd493a2a5e4c211a033e0aba66cff2) --- port/linux/game/menu_tags.c | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index 8b1de17c..76207083 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -1531,9 +1531,10 @@ static void *pause_button(struct cache_file_tag_instance *instances, struct ui_w } /* the list's buttons: SETTINGS (and the host's END GAME) put before LEAVE -GAME (quit); returns how many were added */ +GAME (quit); returns how many were added, and how much taller the list +grew for them (growth) */ static long pause_list_patch(struct cache_file_tag_instance *instances, struct ui_widget_definition *list, long quit, - boolean host) + boolean host, short *growth) { struct ui_widget_child_reference *children = list->child_widgets.address; long count = list->child_widgets.count, added = host ? 2 : 1, child; @@ -1575,8 +1576,25 @@ static long pause_list_patch(struct cache_file_tag_instance *instances, struct u } list->child_widgets.address = grown; list->child_widgets.count = count + added; - /* (the list draws within its bounds) */ - list->bounds.y1 = (short)(list->bounds.y1 + added * spacing); + /* (a list with room for them, Halo PC's of five rows that Custom Edition + maps keep two of: the buttons centred in it; else it grows, as the + Xbox's of two rows does, and draws within its bounds) */ + { + short room = (short)(list->bounds.y1 - list->bounds.y0); + short column = (short)((count + added - 1) * spacing + model->bounds.y1 - model->bounds.y0); + + *growth = 0; + if (column <= room) + { + for (child = 0; child < count + added; child++) + grown[child].vertical_offset = (short)((room - column) / 2 + child * spacing); + } + else + { + *growth = (short)(added * PAUSE_BUTTON_SPACING); + list->bounds.y1 = (short)(list->bounds.y1 + added * spacing); + } + } return added; } @@ -1624,6 +1642,7 @@ static void pause_patch(struct cache_file_tag_instance *instances) boolean host = global_network_game_server_get() != NULL; struct tag_block const *screens; long patched_list = NONE, added = 0, buttons = 0, screen; + short grow = 0; boolean box_redrawn = FALSE; if (collection == NONE || quit_function == NONE) @@ -1635,7 +1654,7 @@ static void pause_patch(struct cache_file_tag_instance *instances) struct ui_widget_definition *definition; struct ui_widget_child_reference *children; long child, list_child = NONE, box_child = NONE; - short grow, list_top; + short list_top; if (screen_tag == NONE) continue; @@ -1660,7 +1679,7 @@ static void pause_patch(struct cache_file_tag_instance *instances) quit = pause_quit_button(list, quit_function); if (quit == NONE || patched_list != NONE) continue; - added = pause_list_patch(instances, list, quit, host); + added = pause_list_patch(instances, list, quit, host, &grow); if (!added) return; buttons = list->child_widgets.count; @@ -1678,8 +1697,8 @@ static void pause_patch(struct cache_file_tag_instance *instances) } } /* the stock box: taller, centred where it was, the list with it, and - what is below it moved down; else only what is below the list */ - grow = (short)(added * PAUSE_BUTTON_SPACING); + what is below it moved down; else only what is below the list (as far + as the list grew) */ list_top = children[list_child].vertical_offset; for (child = 0; child < definition->child_widgets.count; child++) { From 5d5b5dd35e9f312ee64ae6b33aaeb59a694e449b Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Sun, 4 Oct 2026 20:57:27 -0500 Subject: [PATCH 163/187] Custom Edition maps: HUD sounds, dialogue variants and camouflage on odd tag data Three engine checks no longer stop the game on data Halo PC tolerated: HUD sounds with no tag, dialogue variants with no dialogue, and non-model shaders under camouflage (an environment shader on a first-person weapon: not drawn while camouflaged rather than misread as a model shader). These three only: OpenCE already had the commit's other repairs (protected maps, node trees, scripts that fail to load, HUD meters and digits, object functions, collision edges, weapon lists, more than 16 dialogue variants), some made another way, and has no 64-bit build for its script stacks. (cherry picked from commit d15b6ca913266b984966897e1be4e24c69a75b30) --- source/interface/hud_sounds.c | 4 +++- source/rasterizer/xbox/rasterizer_xbox_models.c | 6 ++++++ source/units/unit_dialogue.c | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/source/interface/hud_sounds.c b/source/interface/hud_sounds.c index 85ce1263..8cb11070 100644 --- a/source/interface/hud_sounds.c +++ b/source/interface/hud_sounds.c @@ -77,7 +77,9 @@ void hud_play_sound( struct hud_sound_definition const *sound = TAG_BLOCK_GET_ELEMENT(sounds, absolute_sound_index, struct hud_sound_definition); - if (state_flags & sound->state_flags) + /* port: a HUD sound with no tag never plays (a Custom Edition map's + HUD can have one; it asserted here) */ + if (sound->sound.index != NONE && (state_flags & sound->state_flags)) { switch (sound->sound.group_tag) { diff --git a/source/rasterizer/xbox/rasterizer_xbox_models.c b/source/rasterizer/xbox/rasterizer_xbox_models.c index b7eecb48..d1ad6f17 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_models.c +++ b/source/rasterizer/xbox/rasterizer_xbox_models.c @@ -1684,6 +1684,12 @@ void _rasterizer_model_draw( if (local_model_effect_type == _render_model_effect_type_active_camouflage) { + /* port: camouflage draws parts as model shaders. A Custom Edition + map's model can have parts with other shader types (an environment + shader on a first-person weapon); those aren't drawn while + camouflaged instead of being misread. */ + if (shader->base.type != _shader_type_model) + return; match_assert( "c:\\halo\\SOURCE\\rasterizer\\xbox\\rasterizer_xbox_models.c", 729, diff --git a/source/units/unit_dialogue.c b/source/units/unit_dialogue.c index f926fd9f..a67a69a0 100644 --- a/source/units/unit_dialogue.c +++ b/source/units/unit_dialogue.c @@ -1134,7 +1134,10 @@ static long unit_find_dialogue_variant( variant_index, struct unit_dialogue_variant); - if (variant_number == NONE || variant->variant_number == variant_number) + /* port: a variant with no dialogue is skipped (a Custom Edition map's + unit can have one, which asserted when picked) */ + if ((variant_number == NONE || variant->variant_number == variant_number) && + variant->dialogue_index != NONE) { /* port: no more than the array holds (a map's count; released maps have at most 6 variants) */ From f2a0f91e489cc9e8827233317cc76a3f3c746f4d Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:55:28 -0400 Subject: [PATCH 164/187] Antennas and cloth flags follow a jump of more than one unit Their attachment's movement was truncated to whole units before the test, so it took two units of movement in one update, not one, to move the vertices along with it. Visual only. (cherry picked from commit 99c2db2d4da3d6d66b6ec28d459c756aeab55a64) (cherry picked from commit eb0667bcc81ed1d970422c7856162e8aadff6fd7) --- source/objects/widgets/antenna.c | 13 +++++-------- source/objects/widgets/flags.c | 15 ++++++--------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/source/objects/widgets/antenna.c b/source/objects/widgets/antenna.c index 6bb75b7a..e019cc28 100644 --- a/source/objects/widgets/antenna.c +++ b/source/objects/widgets/antenna.c @@ -347,14 +347,11 @@ static void antenna_update_attachment( delta.i = attachment_point->x - antenna->last_attachment_location.x; delta.j = attachment_point->y - antenna->last_attachment_location.y; delta.k = attachment_point->z - antenna->last_attachment_location.z; - /* - * BUG (original): integer truncation makes this an effective two-unit - * threshold. A non-matching correctness fix would compare fabs(delta.i), - * fabs(delta.j), and fabs(delta.k) directly against 1.0f. - */ - if ((real)abs((long)delta.i) > 1.0f || - (real)abs((long)delta.j) > 1.0f || - (real)abs((long)delta.k) > 1.0f) + /* port: the movement compared as it is (it was truncated to whole units + first, so it took two units of movement to move the vertices along) */ + if (fabsf(delta.i) > 1.0f || + fabsf(delta.j) > 1.0f || + fabsf(delta.k) > 1.0f) { short vertex_index; diff --git a/source/objects/widgets/flags.c b/source/objects/widgets/flags.c index aa026105..62ef73c1 100644 --- a/source/objects/widgets/flags.c +++ b/source/objects/widgets/flags.c @@ -476,15 +476,12 @@ void flag_update_attachment( delta.j = attachment_points->y - flag->first_attachment.y; delta.k = attachment_points->z - flag->first_attachment.z; - /* - * BUG (original): integer truncation makes this an effective two-unit - * threshold, as in antenna_update_attachment. A non-matching correctness - * fix would compare fabs(delta.i), fabs(delta.j), and fabs(delta.k) - * directly against 1.0f. - */ - if ((real)abs((long)delta.i) > 1.0f || - (real)abs((long)delta.j) > 1.0f || - (real)abs((long)delta.k) > 1.0f) + /* port: the movement compared as it is, as in antenna_update_attachment + (it was truncated to whole units first, so it took two units of movement + to move the vertices along) */ + if (fabsf(delta.i) > 1.0f || + fabsf(delta.j) > 1.0f || + fabsf(delta.k) > 1.0f) { short x; From 985cb7c14dbd2ec7e8b2f19f80fc339ea68c5de8 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 05:22:45 -0400 Subject: [PATCH 165/187] Android updater: download bounded in size A release's download and the app extracted from it stop at 256 MB (a release is about 35 MB), so a wrong or hostile answer cannot fill the phone's storage. The size cap only: OpenCE's updater reads its build number from a "build-N" tag with Integer.parseInt, so the version check the commit adds for its own "v" tags is not needed here. (cherry picked from commit fc92683b63ab6ca2725d0bfce0abeb8264c15a7c) --- .../app/src/main/java/com/halo/decomp/Updater.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/port/android/app/src/main/java/com/halo/decomp/Updater.java b/port/android/app/src/main/java/com/halo/decomp/Updater.java index 7a35a362..266a98e9 100644 --- a/port/android/app/src/main/java/com/halo/decomp/Updater.java +++ b/port/android/app/src/main/java/com/halo/decomp/Updater.java @@ -50,6 +50,8 @@ final class Updater { private static final String REPOSITORY = "OpenCommunityEdition/OpenCE"; private static final String USER_AGENT = "halo-ce-universal-updater"; private static final int TIMEOUT_MILLISECONDS = 20000; + /** the most a download (a release's zip, about 35 MB) or the app in it may be */ + private static final long MAXIMUM_UPDATE_SIZE = 256L * 1024 * 1024; private Updater() { } @@ -302,8 +304,10 @@ final class Updater { int count; while ((count = in.read(buffer)) > 0) { - out.write(buffer, 0, count); received += count; + if (received > MAXIMUM_UPDATE_SIZE) + throw new IOException("the download is larger than expected"); + out.write(buffer, 0, count); if (received - reported >= 256 * 1024 || received == total) { progress.report(received, total); reported = received; @@ -327,10 +331,15 @@ final class Updater { continue; try (OutputStream out = new FileOutputStream(apk)) { byte[] buffer = new byte[65536]; + long written = 0; int count; - while ((count = in.read(buffer)) > 0) + while ((count = in.read(buffer)) > 0) { + written += count; + if (written > MAXIMUM_UPDATE_SIZE) + throw new IOException("the app in the download is larger than expected"); out.write(buffer, 0, count); + } } return; } From 20e678f75750b749c774b9c35ed02ee322947a33 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:50:26 -0400 Subject: [PATCH 166/187] Keep the back button and gesture for the game on Android android:enableOnBackInvokedCallback="false" keeps them KEYCODE_BACK, which SDL passes to the game as B. With predictive back, the default once the app targets Android 16, the system would take them and the game would not see them. (cherry picked from commit f83c1711c3581963d6167321e1157bda60f7a132) (cherry picked from commit 40063dcb519e6a46515c7c93cc9142fe51a53bc7) --- port/android/app/src/main/AndroidManifest.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/port/android/app/src/main/AndroidManifest.xml b/port/android/app/src/main/AndroidManifest.xml index cccdd89c..ee8f91c5 100644 --- a/port/android/app/src/main/AndroidManifest.xml +++ b/port/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,10 @@ + + android:hardwareAccelerated="true" + android:enableOnBackInvokedCallback="false"> From 03a5891a2b137b2340d601803055b655721348bc Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:56:15 -0400 Subject: [PATCH 167/187] Menus: display.menus takes "PC" in any case, and says what it needs "PC" or "Pc" fell back to the Xbox menus without a word. Any case now works, and a value that is neither "xbox" nor "pc" is named in the log. With csstrcasecmp, as the port's other files compare names, in OpenCE's check (which also skips a multiplayer map with no pause menu). (cherry picked from commit f12bb1ba0c3eb6b83d58850821b525fedbcf5e7a) --- port/linux/game/menu_tags.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index 76207083..d622a7fa 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -1717,6 +1717,23 @@ static void pause_patch(struct cache_file_tag_instance *instances) platform_log("menus: the pause menu has SETTINGS%s", host ? " and END GAME" : ""); } +/* whether display.menus asks for the PC version's menus: "pc" in any case +(a value neither "pc" nor "xbox" is said once in the log, and is the Xbox's) */ +static boolean menus_pc_chosen(void) +{ + static boolean said; + char const *value = config_string("display.menus"); + + if (!csstrcasecmp(value, "pc")) + return TRUE; + if (csstrcasecmp(value, "xbox") && !said) + { + platform_log("menus: display.menus \"%s\" is not \"xbox\" or \"pc\": the Xbox's", value); + said = TRUE; + } + return FALSE; +} + void menu_tags_loaded( char const *map_name) { @@ -1728,7 +1745,7 @@ void menu_tags_loaded( /* (ui.map, and a multiplayer map: its pause menu's SETTINGS) */ if ((game_map && tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE) || - strcmp(config_string("display.menus"), "pc")) + !menus_pc_chosen()) { return; } From 234d4c22954a6a9f33593b4c56b9c2f5e4b682ef Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Thu, 8 Oct 2026 14:39:42 -0500 Subject: [PATCH 168/187] AI profiler: meters averaged right The AI profiler's meters took the oldest sample out of their running sum but never put the new one in, so their averages were wrong; the actor line's units also lacked their tab and a separator. The ai_profile.c fixes only, from a commit that is otherwise a rewording of the AI code's comments. (cherry picked from commit 95b838d13e276e41aa37f4646ce4acf5d18b8639) --- source/ai/ai_profile.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/source/ai/ai_profile.c b/source/ai/ai_profile.c index fd67d200..abe0b1b8 100644 --- a/source/ai/ai_profile.c +++ b/source/ai/ai_profile.c @@ -272,9 +272,9 @@ void ai_profile_update( meter->history_sum -= meter->history[meter->history_next_index]; } meter->history[meter->history_next_index] = meter->current_value; - /* BUG (original): January and October subtract the evicted sample but - * never add the new sample to history_sum. A corrected build should add - * current_value here before computing the average. */ + /* port: the new sample counted too (the original only took the old one + off, so the averages were wrong) */ + meter->history_sum += meter->current_value; meter->history_next_index++; meter->history_count = MAX(meter->history_count, meter->history_next_index); meter->history_next_index %= AI_METER_HISTORY_TICKS; @@ -460,8 +460,7 @@ static void ai_profile_render_actors( { short tab_stops[] = {150, 300}; - /* Preserve January's missing tab marker and final unit-count separator. */ - sprintf(profilestring, "actors %d/%d/%d|units %d/%d%d", + sprintf(profilestring, "actors %d/%d/%d|tunits %d/%d/%d", ai_profile.meters[_ai_meter_actors_active].current_value, ai_profile.meters[_ai_meter_actors_updated].current_value, ai_profile.meters[_ai_meter_actors].current_value, From 42cfa397f76aaeee7d2549ced7ab0502a21cf0d7 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:52:00 -0400 Subject: [PATCH 169/187] AI debugging checks: the vehicle exit time The unit activation check compared the last vehicle's index where it meant the time the unit left it, so it could report a false "unit activation logic error" just after a vehicle exit. Messages only. The actors.c line only; the commit's other fix, the movement point's y printed as z, came with "Log and error messages print what their formats ask for". (cherry picked from commit e3089220a6bfa8479ba2e334a0070f97c97dd851) --- source/ai/actors.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/ai/actors.c b/source/ai/actors.c index 9afd0077..b8a422a3 100644 --- a/source/ai/actors.c +++ b/source/ai/actors.c @@ -696,10 +696,8 @@ static void actor_verify_unit_activation( struct unit_datum *unit = unit_get(unit_index); struct actor_datum *actor = actor_get(actor_index); - /* (as the original game, kept: the vehicle's index is compared with the time, where - the time of the last vehicle exit was meant) */ if (unit->object.parent_object_index == NONE && - unit->unit.last_vehicle_index + 30 < game_time_get()) + unit->unit.game_time_at_last_vehicle_exit + 30 < game_time_get()) { if (actor->meta.dormant != !TEST_FLAG(object_header->flags, _object_header_active_bit)) { From 90f3fdf25f699c4e3e89605cf7241e9555d14ba6 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:54:11 -0400 Subject: [PATCH 170/187] Director debug controls: each variable's own hyper scale The debug camera controls applied the height variable's hyper-scale setting to every variable. Camera debugging only. (cherry picked from commit 3db16e4499c7aa8ee9c82f28c3454009f877bccc) (cherry picked from commit a5074861b2f9682e791adfd587c3a2b43e2f6daf) --- source/camera/director.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/source/camera/director.c b/source/camera/director.c index d1672b9e..bd71809d 100644 --- a/source/camera/director.c +++ b/source/camera/director.c @@ -505,10 +505,7 @@ static void director_process_variables( &variables[variable_index]; struct director_variable_instance *instance = &director->debug_variables[variable_index]; - /* BUG (original): January tests the height definition for every - * variable. A corrected build should test definition->has_hyper_scale. - */ - real hyper_scale = variables[_variable_height].has_hyper_scale + real hyper_scale = definition->has_hyper_scale ? director->debug_input_scale : 1.f; real velocity_scale = From 5ed36d17f2613ebf7a105fc5a613664688e595b2 Mon Sep 17 00:00:00 2001 From: xshxdex98 Date: Sun, 4 Oct 2026 17:33:59 -0500 Subject: [PATCH 171/187] CE maps: an index past a tag block's end no longer stops a debug build Halo PC reads a Custom Edition map's tags unchecked, so maps made for it can hold an index past a block's end that never mattered there. OpenCE already gives such an index the empty data and logs it, as it does for a tag data's offset, but a debug build still stopped at the block's assertions first (foundation@ce, 13 seconds in). With a Custom Edition map loaded they are skipped, as tag_data_get_pointer's are; Xbox maps keep their checks. Only the assertions: OpenCE had the rest of the commit's change, made another way (tag_empty_data). (cherry picked from commit c953c65409ff9f3f230849bc6fc287727e3e3213) --- source/tag_files/tag_groups.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/source/tag_files/tag_groups.c b/source/tag_files/tag_groups.c index 7730cef8..7acc2f47 100644 --- a/source/tag_files/tag_groups.c +++ b/source/tag_files/tag_groups.c @@ -122,12 +122,19 @@ void *tag_block_get_element_with_size( match_assert("c:\\halo\\SOURCE\\tag_files\\tag_groups.c", 3085, block->count>=0); match_assert("c:\\halo\\SOURCE\\tag_files\\tag_groups.c", 3086, !block->definition || block->definition->element_size==element_size); - match_vassert("c:\\halo\\SOURCE\\tag_files\\tag_groups.c", 3089, index>=0 && indexcount, - csprintf(temporary, - "#%d is not a valid %s index in [#0,#%d)", - index, - block->definition ? block->definition->name : "", block->count)); - match_assert("c:\\halo\\SOURCE\\tag_files\\tag_groups.c", 3090, block->address); + /* port: as tag_data_get_pointer, a Custom Edition map's index past a + block's end (which Halo PC never checked: foundation@ce, 13 seconds in) + gets the empty data below without an assertion. This build's maps keep + theirs */ + if (!custom_edition_cache_tags_loaded()) + { + match_vassert("c:\\halo\\SOURCE\\tag_files\\tag_groups.c", 3089, index>=0 && indexcount, + csprintf(temporary, + "#%d is not a valid %s index in [#0,#%d)", + index, + block->definition ? block->definition->name : "", block->count)); + match_assert("c:\\halo\\SOURCE\\tag_files\\tag_groups.c", 3090, block->address); + } /* port: an element past the block (an index a map's data gave, which nothing checked) is the empty data (tag_empty_data), not whatever lies past the block */ From 7124534a4bbf19c12b1219f329063052114defab Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:42:05 +1300 Subject: [PATCH 172/187] Memory leaks: render targets, gamepads, Android audio stream --- port/android/guest/runtime/guest_host.h | 1 + port/android/guest/runtime/guest_sdl.c | 5 + port/android/guest/runtime/guest_start.c | 2 + port/android/guest/runtime/guest_thread.c | 12 ++ port/android/host/host_sdl.c | 86 +++++++++-- port/android/host/host_thread.c | 4 + port/android/host_imports.list | 1 + port/android/include/halo_android_abi.h | 3 +- port/linux/game/menu_tags.c | 22 ++- port/linux/src/d3d8_gl.c | 138 +++++++++++++++++- port/linux/src/gl.h | 4 + port/linux/src/sdl_platform.c | 10 ++ port/linux/src/updater.c | 1 + port/linux/src/xgpu_text.c | 12 +- source/bungie_net/common/thread_win32.c | 12 ++ .../network/transport_endpoint_winsock.c | 3 + source/cache/cache_files_windows.c | 8 +- source/interface/progress_bar.c | 6 + source/memory/texture_page.c | 7 +- source/rasterizer/xbox/rasterizer_xbox.c | 1 + 20 files changed, 311 insertions(+), 27 deletions(-) diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h index 5667fb2a..60c5d259 100644 --- a/port/android/guest/runtime/guest_host.h +++ b/port/android/guest/runtime/guest_host.h @@ -82,6 +82,7 @@ int host_sdl_gamepad_axis(unsigned int gamepad, int axis); int host_sdl_gamepad_button(unsigned int gamepad, int button); int host_sdl_gamepad_type(unsigned int gamepad); int host_sdl_rumble_gamepad(unsigned int gamepad, unsigned int low, unsigned int high, unsigned int milliseconds); +void host_sdl_close_gamepad(unsigned int gamepad); /* callback: void (*)(void *userdata, unsigned int stream, int additional, int total), called on the audio thread */ unsigned int host_sdl_open_audio_stream(unsigned int device, const void *spec, unsigned int callback, unsigned int userdata); diff --git a/port/android/guest/runtime/guest_sdl.c b/port/android/guest/runtime/guest_sdl.c index 21197ba1..bd7b309b 100644 --- a/port/android/guest/runtime/guest_sdl.c +++ b/port/android/guest/runtime/guest_sdl.c @@ -216,6 +216,11 @@ bool SDL_RumbleGamepad(SDL_Gamepad *gamepad, Uint16 low, Uint16 high, Uint32 mil return host_sdl_rumble_gamepad((unsigned int)gamepad, low, high, milliseconds) != 0; } +void SDL_CloseGamepad(SDL_Gamepad *gamepad) +{ + host_sdl_close_gamepad((unsigned int)gamepad); +} + /* ---------- audio */ SDL_AudioStream *SDL_OpenAudioDeviceStream(SDL_AudioDeviceID device, const SDL_AudioSpec *spec, diff --git a/port/android/guest/runtime/guest_start.c b/port/android/guest/runtime/guest_start.c index e0beba77..8242ec50 100644 --- a/port/android/guest/runtime/guest_start.c +++ b/port/android/guest/runtime/guest_start.c @@ -25,6 +25,7 @@ extern void (*__guest_init_end[])(void); void __guest_start(const struct halo_guest_boot *boot); void __guest_thread_start(unsigned int thread); unsigned int __guest_thread_attach(void); +void __guest_thread_detach(void); void __guest_thread_initialize_main(void); extern char **__environ; @@ -41,6 +42,7 @@ const struct halo_guest_header __guest_header = (uint32_t)__guest_start, (uint32_t)__guest_thread_start, (uint32_t)__guest_thread_attach, + (uint32_t)__guest_thread_detach, (uint32_t)__guest_init_start, (uint32_t)__guest_init_end, }; diff --git a/port/android/guest/runtime/guest_thread.c b/port/android/guest/runtime/guest_thread.c index 56223a1a..13fe28a0 100644 --- a/port/android/guest/runtime/guest_thread.c +++ b/port/android/guest/runtime/guest_thread.c @@ -125,6 +125,18 @@ unsigned int __guest_thread_attach(void) return (unsigned int)thread; } +/* a host thread that ran guest code (__guest_thread_attach) is ending: +its struct pthread goes */ +void __guest_thread_detach(void) +{ + struct guest_thread *thread = (struct guest_thread *)host_get_tp(); + + if (!thread || thread == &main_thread) + return; + host_set_tp(0); + thread_free(thread); +} + int pthread_create(pthread_t *restrict result, const pthread_attr_t *restrict attributes, void *(*start)(void *), void *restrict argument) { diff --git a/port/android/host/host_sdl.c b/port/android/host/host_sdl.c index e994539d..ff858945 100644 --- a/port/android/host/host_sdl.c +++ b/port/android/host/host_sdl.c @@ -32,6 +32,8 @@ struct handle { int type; void *object; + /* an audio stream with a callback: its binding (below) */ + void *binding; }; static struct handle handles[HANDLE_COUNT]; @@ -59,6 +61,7 @@ static uint32_t handle_new(int type, void *object) { handles[index].type = type; handles[index].object = object; + handles[index].binding = NULL; pthread_mutex_unlock(&handle_lock); return index; } @@ -81,6 +84,30 @@ static void *handle_get(uint32_t handle, int type) return object; } +/* the object of a handle, which the handle no longer refers to; NULL when +there is none. *binding gets what handle_new's caller stored (audio) */ +static void *handle_release(uint32_t handle, int type, void **binding) +{ + void *object = NULL; + + if (binding) + *binding = NULL; + if (handle == 0 || handle >= HANDLE_COUNT) + return NULL; + pthread_mutex_lock(&handle_lock); + if (handles[handle].type == type) + { + object = handles[handle].object; + if (binding) + *binding = handles[handle].binding; + handles[handle].type = _handle_free; + handles[handle].object = NULL; + handles[handle].binding = NULL; + } + pthread_mutex_unlock(&handle_lock); + return object; +} + /* ---------- general */ int host_sdl_init(uint32_t flags) @@ -217,6 +244,15 @@ uint32_t host_sdl_gamepad_from_id(uint32_t id) return handle_new(_handle_gamepad, SDL_GetGamepadFromID((SDL_JoystickID)id)); } +/* (the event thread, when the controller goes: sdl_platform.c) */ +void host_sdl_close_gamepad(uint32_t gamepad) +{ + SDL_Gamepad *object = handle_release(gamepad, _handle_gamepad, NULL); + + if (object) + SDL_CloseGamepad(object); +} + int host_sdl_gamepad_axis(uint32_t gamepad, int axis) { SDL_Gamepad *object = handle_get(gamepad, _handle_gamepad); @@ -263,6 +299,8 @@ struct audio_binding pthread_cond_t requested; pthread_cond_t done; int pending; + /* the stream is destroyed: the thread frees the binding and ends */ + int quit; int additional; int total; unsigned char *buffer; @@ -282,8 +320,10 @@ static void *audio_thread(void *context) { int additional, total; - while (!binding->pending) + while (!binding->pending && !binding->quit) pthread_cond_wait(&binding->requested, &binding->lock); + if (binding->quit) + break; additional = binding->additional; total = binding->total; pthread_mutex_unlock(&binding->lock); @@ -294,6 +334,12 @@ static void *audio_thread(void *context) binding->pending = 0; pthread_cond_signal(&binding->done); } + pthread_mutex_unlock(&binding->lock); + pthread_cond_destroy(&binding->done); + pthread_cond_destroy(&binding->requested); + pthread_mutex_destroy(&binding->lock); + SDL_free(binding->buffer); + SDL_free(binding); return NULL; } @@ -350,7 +396,8 @@ uint32_t host_sdl_open_audio_stream(uint32_t device, const void *spec, uint32_t return stream ? handle_new(_handle_audio, stream) : 0; } binding = SDL_calloc(1, sizeof(*binding)); - + if (!binding) + return 0; binding->callback = callback; binding->userdata = userdata; pthread_mutex_init(&binding->lock, NULL); @@ -365,7 +412,19 @@ uint32_t host_sdl_open_audio_stream(uint32_t device, const void *spec, uint32_t } /* the device starts paused, so no callback can run before this */ binding->handle = handle_new(_handle_audio, stream); - if (callback && host_native_thread_create(audio_thread, binding, 256 * 1024) != 0) + if (!binding->handle) + { + SDL_DestroyAudioStream(stream); + pthread_cond_destroy(&binding->done); + pthread_cond_destroy(&binding->requested); + pthread_mutex_destroy(&binding->lock); + SDL_free(binding); + return 0; + } + pthread_mutex_lock(&handle_lock); + handles[binding->handle].binding = binding; + pthread_mutex_unlock(&handle_lock); + if (host_native_thread_create(audio_thread, binding, 256 * 1024) != 0) host_fatal("cannot start the audio thread"); return binding->handle; } @@ -403,19 +462,26 @@ int host_sdl_get_audio_stream_available(uint32_t stream) return object ? SDL_GetAudioStreamAvailable(object) : -1; } -/* (a stream opened without a callback only: one with a callback has a -thread the guest's callbacks run on) */ +/* (a stream with a callback has a thread the guest's callbacks run on: +once no callback can run any more, it is told to end, and frees the +binding and its stack as it goes) */ void host_sdl_destroy_audio_stream(uint32_t stream) { - SDL_AudioStream *object = handle_get(stream, _handle_audio); + void *context; + SDL_AudioStream *object = handle_release(stream, _handle_audio, &context); + struct audio_binding *binding = context; if (!object) return; - pthread_mutex_lock(&handle_lock); - handles[stream].type = _handle_free; - handles[stream].object = NULL; - pthread_mutex_unlock(&handle_lock); + /* (returns once a callback that is running has finished) */ SDL_DestroyAudioStream(object); + if (binding) + { + pthread_mutex_lock(&binding->lock); + binding->quit = 1; + pthread_cond_signal(&binding->requested); + pthread_mutex_unlock(&binding->lock); + } } /* ---------- the clipboard (internet play's invite links) */ diff --git a/port/android/host/host_thread.c b/port/android/host/host_thread.c index bc939b8b..1d391456 100644 --- a/port/android/host/host_thread.c +++ b/port/android/host/host_thread.c @@ -131,6 +131,10 @@ static void *thread_main(void *context) free(context); host_debug_thread_started(); start.function(start.argument); + /* (a thread the guest made has let its struct pthread go by now; one + of the host's that ran guest code, the audio thread, has it still) */ + if (guest_tp) + ((guest_function)(uintptr_t)host_image.header->thread_detach)(0, 0, 0, 0); host_debug_thread_exited(); guest_tp = 0; diff --git a/port/android/host_imports.list b/port/android/host_imports.list index 058c8b97..41d52cfc 100644 --- a/port/android/host_imports.list +++ b/port/android/host_imports.list @@ -39,6 +39,7 @@ host_sdl_gamepad_axis host_sdl_gamepad_button host_sdl_gamepad_type host_sdl_rumble_gamepad +host_sdl_close_gamepad host_sdl_scancode_name host_sdl_scancode_from_name host_sdl_open_audio_stream diff --git a/port/android/include/halo_android_abi.h b/port/android/include/halo_android_abi.h index baa28789..3b9c2963 100644 --- a/port/android/include/halo_android_abi.h +++ b/port/android/include/halo_android_abi.h @@ -35,7 +35,7 @@ keeps its heaps low in the address space and fills it upwards */ #define HALO_GUEST_WINDOW_SIZE 0x08000000u #define HALO_GUEST_MAGIC 0x4f4c4148u /* 'HALO' */ -#define HALO_GUEST_ABI_VERSION 1 +#define HALO_GUEST_ABI_VERSION 2 /* at HALO_GUEST_IMAGE_BASE */ struct halo_guest_header @@ -49,6 +49,7 @@ struct halo_guest_header uint32_t start; /* void __guest_start(struct halo_guest_boot *) */ uint32_t thread_start; /* void __guest_thread_start(uint32_t thread) */ uint32_t thread_attach; /* uint32_t __guest_thread_attach(void) */ + uint32_t thread_detach; /* void __guest_thread_detach(void) */ uint32_t init_array_start; /* void (*)(void) entries, 4 bytes each */ uint32_t init_array_end; }; diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index d622a7fa..06af9f8d 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -901,7 +901,18 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index) } bitmap->width = (short)width; bitmap->height = (short)height; - menu_tags.bitmaps = realloc(menu_tags.bitmaps, (menu_tags.bitmap_count + 1) * sizeof(*menu_tags.bitmaps)); + { + struct bitmap_data **bitmaps = realloc(menu_tags.bitmaps, + (menu_tags.bitmap_count + 1) * sizeof(*menu_tags.bitmaps)); + + if (!bitmaps) + { + rasterizer_bitmap_delete(bitmap); + problem(source->file, source->line, "out of memory for the bitmaps of", source->name); + return group; + } + menu_tags.bitmaps = bitmaps; + } menu_tags.bitmaps[menu_tags.bitmap_count++] = bitmap; /* (none without a renderer: debug.null_renderer) */ if (bitmap->hardware_format) @@ -1031,8 +1042,15 @@ static void handler_build(struct ui_widget_event_handler_reference *handler, str static void setting_add(struct halo_menu_widget const *source, long definition_index) { struct pc_menu_setting *setting; + struct pc_menu_setting *settings = realloc(menu_tags.settings, + (menu_tags.setting_count + 1) * sizeof(*menu_tags.settings)); - menu_tags.settings = realloc(menu_tags.settings, (menu_tags.setting_count + 1) * sizeof(*menu_tags.settings)); + if (!settings) + { + problem(source->file, source->line, "out of memory for the settings of", source->name); + return; + } + menu_tags.settings = settings; setting = &menu_tags.settings[menu_tags.setting_count++]; memset(setting, 0, sizeof(*setting)); setting->definition_index = definition_index; diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index df1d7a1b..d78df080 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -419,6 +419,17 @@ static struct program_entry *program_buckets[PROGRAM_BUCKETS]; /* ---------- render targets */ +/* what a render target's scale follows (render_target_get): an entry whose +scale no longer fits is freed (render_targets_evict) */ +enum render_target_kind +{ + _render_target_plain, + /* the screen's targets and the secondary target: screen_scale's */ + _render_target_screen, + /* a shadow map: halo_shadow_map_scale's */ + _render_target_shadow_map, +}; + struct render_target_entry { struct render_target_entry *next; @@ -429,6 +440,7 @@ struct render_target_entry /* the back buffer or its depth buffer, which the 3D view is drawn into: multisampled with multisampling (render_target_multisample) */ BOOL screen_buffer; + enum render_target_kind kind; }; /* every draw looks up its targets and whether its textures are render @@ -455,6 +467,8 @@ struct framebuffer_entry static struct render_target_entry *render_targets; static struct framebuffer_entry *framebuffers; +static void render_targets_evict(void); + /* ---------- the device */ #ifdef HALO_ANDROID @@ -1200,8 +1214,9 @@ static struct render_target_entry *render_target_get(const D3DSurface *surface) if (!surface || !surface->Data) return NULL; float scale[2] = { 1.0f, 1.0f }; + enum render_target_kind kind = _render_target_plain; - /* (the entries are never freed) */ + /* (an entry lives until its scale no longer fits: render_targets_evict) */ screen = halo_screen_width(); for (slot = 0; slot < RECENT_RENDER_TARGET_COUNT; slot++) { @@ -1224,11 +1239,13 @@ static struct render_target_entry *render_target_get(const D3DSurface *surface) { scale[0] = screen_scale[0]; scale[1] = screen_scale[1]; + kind = _render_target_screen; } else if (!depth && surface_is_shadow_map(surface)) { scale[0] = (float)halo_shadow_map_scale(); scale[1] = scale[0]; + kind = _render_target_shadow_map; } for (entry = *render_target_bucket(surface->Data); entry; entry = entry->next_in_bucket) { @@ -1246,6 +1263,7 @@ static struct render_target_entry *render_target_get(const D3DSurface *surface) entry->target.depth = depth; entry->target.scale[0] = scale[0]; entry->target.scale[1] = scale[1]; + entry->kind = kind; entry->target.gl_width = (unsigned long)(width * scale[0] + 0.5f); entry->target.gl_height = (unsigned long)(height * scale[1] + 0.5f); glGenTextures(1, &entry->target.texture); @@ -1935,7 +1953,7 @@ long halo_screen_commit(void) supersampling changes the scale below) */ anti_aliasing_read(); /* display.shadow_resolution, if it has changed: the maps' entries at - the old scale stay (render_target_get), but are no longer found */ + the old scale are freed (render_targets_evict), new ones made at the new */ if (shadow_scale && shadow_scale_read_at != config_changes()) { long shadow = shadow_scale_choose(); @@ -1945,7 +1963,7 @@ long halo_screen_commit(void) { platform_log("shadow maps: %ldx%ld", SHADOW_MAP_SIZE * shadow, SHADOW_MAP_SIZE * shadow); shadow_scale = shadow; - memset(recent_render_targets, 0, sizeof(recent_render_targets)); + render_targets_evict(); } } if (!screen_width) @@ -1966,6 +1984,8 @@ long halo_screen_commit(void) d3d8_surface_resize(&device.depth_buffer, D3DFMT_LIN_D24S8, (unsigned long)width, SCREEN_HEIGHT); } #endif + /* (the entries of the old size and scale, which nothing finds now) */ + render_targets_evict(); } return screen_width; } @@ -2939,6 +2959,107 @@ struct mip_composite static struct mip_composite *mip_composites; +/* an entry render_target_get no longer finds: the screen's targets at +another scale or screen width than now, the shadow maps at another +display.shadow_resolution */ +static BOOL render_target_stale(const struct render_target_entry *entry) +{ + switch (entry->kind) + { + case _render_target_screen: + return entry->target.scale[0] != screen_scale[0] || entry->target.scale[1] != screen_scale[1] || + (entry->target.height == SCREEN_HEIGHT && entry->target.width != (unsigned long)halo_screen_width()); + case _render_target_shadow_map: + return entry->target.scale[0] != (float)halo_shadow_map_scale(); + default: + return FALSE; + } +} + +/* frees the framebuffers made of a texture, or with renderbuffers, of a +multisampled renderbuffer, which is going */ +static void framebuffers_forget(GLuint attachment, BOOL renderbuffers) +{ + struct framebuffer_entry *entry, **link = &framebuffers; + + while ((entry = *link) != NULL) + { + if (entry->renderbuffers == renderbuffers && (entry->color == attachment || entry->depth == attachment)) + { + *link = entry->next; + glDeleteFramebuffers(1, &entry->framebuffer); + free(entry); + } + else + { + link = &entry->next; + } + } +} + +/* frees an entry taken out of render_targets: its GL objects, the +framebuffers made of them, and what remembered them */ +static void render_target_delete(struct render_target_entry *entry) +{ + struct render_target_entry **link; + struct mip_composite *composite; + + for (link = render_target_bucket(entry->target.data); *link != entry; link = &(*link)->next_in_bucket) + ; + *link = entry->next_in_bucket; + framebuffers_forget(entry->target.texture, FALSE); + if (entry->target.multisample) + { + framebuffers_forget(entry->target.multisample, TRUE); + glDeleteRenderbuffers(1, &entry->target.multisample); + } + /* (a composite that copied from the texture copies again, rather than + take a new texture with the same name for it) */ + for (composite = mip_composites; composite; composite = composite->next) + { + unsigned long level; + + for (level = 0; level < composite->rendered_levels; level++) + { + if (composite->level_sources[level] == entry->target.texture) + { + composite->rendered_levels = 0; + break; + } + } + } + glDeleteTextures(1, &entry->target.texture); + free(entry); +} + +/* (between frames, when the screen's or the shadow maps' scale has +changed: halo_screen_commit) frees the entries render_target_get no longer +finds, which a frame at the old scale left */ +static void render_targets_evict(void) +{ + struct render_target_entry *entry, **link = &render_targets; + BOOL any = FALSE; + + while ((entry = *link) != NULL) + { + if (render_target_stale(entry)) + { + *link = entry->next; + render_target_delete(entry); + any = TRUE; + } + else + { + link = &entry->next; + } + } + if (any) + { + memset(recent_render_targets, 0, sizeof(recent_render_targets)); + xgpu_gl_state_invalidate(); + } +} + #ifdef HALO_ANDROID static GLuint framebuffer_get(GLuint color, GLuint depth); @@ -4511,9 +4632,14 @@ static void immediate_emit(void) if (device.immediate_count == device.immediate_capacity) { - device.immediate_capacity = device.immediate_capacity ? device.immediate_capacity * 2 : 256; - device.immediate_vertices = realloc(device.immediate_vertices, - device.immediate_capacity * floats * sizeof(float)); + unsigned long capacity = device.immediate_capacity ? device.immediate_capacity * 2 : 256; + float *vertices = realloc(device.immediate_vertices, capacity * floats * sizeof(float)); + + /* (out of memory: the vertex is dropped) */ + if (!vertices) + return; + device.immediate_vertices = vertices; + device.immediate_capacity = capacity; } memcpy(device.immediate_vertices + device.immediate_count * floats, device.attributes, floats * sizeof(float)); device.immediate_count++; diff --git a/port/linux/src/gl.h b/port/linux/src/gl.h index e0a58157..652f903c 100644 --- a/port/linux/src/gl.h +++ b/port/linux/src/gl.h @@ -87,6 +87,7 @@ this list to generate the guest's entry points */ X(glBlitFramebuffer) \ X(glDrawBuffers) \ X(glGenRenderbuffers) \ + X(glDeleteRenderbuffers) \ X(glBindRenderbuffer) \ X(glRenderbufferStorageMultisample) \ X(glFramebufferRenderbuffer) \ @@ -198,6 +199,7 @@ this list to generate the guest's entry points */ X(glBlitFramebuffer) \ X(glDrawBuffers) \ X(glGenRenderbuffers) \ + X(glDeleteRenderbuffers) \ X(glBindRenderbuffer) \ X(glRenderbufferStorageMultisample) \ X(glFramebufferRenderbuffer) \ @@ -319,6 +321,7 @@ pointers, sees the declarations without these aliases */ #define glBlitFramebuffer halo_glBlitFramebuffer #define glDrawBuffers halo_glDrawBuffers #define glGenRenderbuffers halo_glGenRenderbuffers +#define glDeleteRenderbuffers halo_glDeleteRenderbuffers #define glBindRenderbuffer halo_glBindRenderbuffer #define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample #define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer @@ -428,6 +431,7 @@ pointers, sees the declarations without these aliases */ #define glBlitFramebuffer halo_glBlitFramebuffer #define glDrawBuffers halo_glDrawBuffers #define glGenRenderbuffers halo_glGenRenderbuffers +#define glDeleteRenderbuffers halo_glDeleteRenderbuffers #define glBindRenderbuffer halo_glBindRenderbuffer #define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample #define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 550c53f8..cff77c4d 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -1604,6 +1604,16 @@ void platform_pump_events(void) } #endif break; + case SDL_EVENT_GAMEPAD_REMOVED: + /* (SDL keeps a gamepad open until it is closed, even once the + controller has gone) */ + { + SDL_Gamepad *gamepad = SDL_GetGamepadFromID(event.gdevice.which); + + if (gamepad) + SDL_CloseGamepad(gamepad); + } + break; default: break; } diff --git a/port/linux/src/updater.c b/port/linux/src/updater.c index bd2fd116..4ad5e587 100644 --- a/port/linux/src/updater.c +++ b/port/linux/src/updater.c @@ -414,6 +414,7 @@ static int updater_download_zip(const char *zip_path, char *error, size_t error_ thread = SDL_CreateThread(updater_download_thread, "update download", &download); if (!thread) { + SDL_DestroyMutex(download.lock); snprintf(error, error_size, "could not start the download"); return 0; } diff --git a/port/linux/src/xgpu_text.c b/port/linux/src/xgpu_text.c index 23beb89c..83a84b3f 100644 --- a/port/linux/src/xgpu_text.c +++ b/port/linux/src/xgpu_text.c @@ -26,7 +26,15 @@ void xgpu_text_append(struct xgpu_text *text, const char *format, ...) text->length += (unsigned long)needed; return; } - text->capacity = (text->capacity + (unsigned long)needed + 1) * 2; - text->buffer = realloc(text->buffer, text->capacity); + { + unsigned long capacity = (text->capacity + (unsigned long)needed + 1) * 2; + char *buffer = realloc(text->buffer, capacity); + + /* (out of memory: the text stays as it is, short) */ + if (!buffer) + return; + text->buffer = buffer; + text->capacity = capacity; + } } } diff --git a/source/bungie_net/common/thread_win32.c b/source/bungie_net/common/thread_win32.c index 9f437516..088c1e62 100644 --- a/source/bungie_net/common/thread_win32.c +++ b/source/bungie_net/common/thread_win32.c @@ -140,9 +140,16 @@ boolean create_thread( else { CloseHandle(reference->handle); + reference->handle = NULL; + reference->in_use = FALSE; reference = NULL; } } + else if (reference) + { + reference->in_use = FALSE; + reference = NULL; + } *thread_reference = reference; return success; @@ -196,9 +203,14 @@ boolean create_mutex( thread_globals.mutex_index++); reference->handle = CreateMutexA(NULL, FALSE, reference->name); if (reference->handle) + { success = TRUE; + } else + { + reference->in_use = FALSE; reference = NULL; + } } *mutex_reference = reference; diff --git a/source/bungie_net/network/transport_endpoint_winsock.c b/source/bungie_net/network/transport_endpoint_winsock.c index a95816ef..e815b2a1 100644 --- a/source/bungie_net/network/transport_endpoint_winsock.c +++ b/source/bungie_net/network/transport_endpoint_winsock.c @@ -1505,7 +1505,10 @@ struct transport_endpoint *accept_endpoint( ep->flags |= 1; } else + { + closesocket(socket); listening_endpoint->error = _transport_error_out_of_memory; + } } else { diff --git a/source/cache/cache_files_windows.c b/source/cache/cache_files_windows.c index 29ded990..da92f265 100644 --- a/source/cache/cache_files_windows.c +++ b/source/cache/cache_files_windows.c @@ -1684,7 +1684,13 @@ static struct cached_map_file *cached_map_file_get( static void cached_map_file_invalidate( short map_file_index) { - cached_map_file_get(map_file_index)->file = INVALID_HANDLE_VALUE; + struct cached_map_file *map_file = cached_map_file_get(map_file_index); + + if (map_file->file != INVALID_HANDLE_VALUE) + { + CloseHandle(map_file->file); + } + map_file->file = INVALID_HANDLE_VALUE; return; } diff --git a/source/interface/progress_bar.c b/source/interface/progress_bar.c index 2559f3c9..1e0a3ac9 100644 --- a/source/interface/progress_bar.c +++ b/source/interface/progress_bar.c @@ -2066,6 +2066,9 @@ static void progress_bar_render( IDirect3DDevice8_SetTextureStageState(global_d3d_device, stage, D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP); IDirect3DDevice8_SetTexture(global_d3d_device, stage, &back_buffer_texture); } + /* (back_buffer_texture has what it needs of the surface; the + reference IDirect3DDevice8_GetBackBuffer gave is given back) */ + IDirect3DSurface8_Release(back_buffer); if (progress<0.9f) draw_fade_layer(0.f, 0.f, 0.9f); @@ -2221,6 +2224,9 @@ static void progress_bar_make_stuff_ready( IDirect3DDevice8_SetRenderTarget(global_d3d_device, back_buffer, depth_buffer); IDirect3DDevice8_Clear(global_d3d_device, 0, NULL, 0xf0, 0, 0.f, 0); IDirect3DDevice8_SetRenderTarget(global_d3d_device, front_buffer, depth_buffer); + IDirect3DSurface8_Release(back_buffer); + IDirect3DSurface8_Release(front_buffer); + IDirect3DSurface8_Release(depth_buffer); } return; diff --git a/source/memory/texture_page.c b/source/memory/texture_page.c index ae75bc4b..4991dc65 100644 --- a/source/memory/texture_page.c +++ b/source/memory/texture_page.c @@ -145,11 +145,8 @@ struct texture_page *texture_page_new( return texture_page; } - { - struct data_array *textures = texture_page->textures; - texture_page = NULL; - match_free("c:\\halo\\SOURCE\\memory\\texture_page.c", 56, textures); - } + match_free("c:\\halo\\SOURCE\\memory\\texture_page.c", 56, texture_page); + texture_page = NULL; } return texture_page; diff --git a/source/rasterizer/xbox/rasterizer_xbox.c b/source/rasterizer/xbox/rasterizer_xbox.c index fe416c72..30ac278e 100644 --- a/source/rasterizer/xbox/rasterizer_xbox.c +++ b/source/rasterizer/xbox/rasterizer_xbox.c @@ -2547,6 +2547,7 @@ void _rasterizer_present( error(_error_silent, "### ERROR rasterizer_present: failed to get backbuffer surface"); success = FALSE; } + IDirect3DSurface8_Release(d3d_surface); } else { From 124cdd0b1706945ab40ed7610ce11392ba5b961c Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:06:39 +1300 Subject: [PATCH 173/187] Any controller's menus: helpers placed and declared as the file's (#228) The new helpers sat between widget_takes_events_of_controller's comment and the function, so the comment now heads it again; pc_menu_split_players is declared with the file's other port prototypes rather than as an extern among the functions, the players' count is read with players_port_local_player_count (safe before the players' globals are made), the comment names process_ui_widgets, and the test widget_takes_any_controller already makes is not made twice. --- source/interface/ui_widget.c | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index acec08d2..53133aca 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -1367,6 +1367,14 @@ static boolean ui_widget_load_children_recursive( /* port: whether the tag is one of the menus' (port/linux/game/menu_tags.c) */ boolean pc_menu_tag( long tag_index); +/* port: whether this machine has, or is adding, a second player: co-op, +split screen, the lobby's (port/linux/game/menu_functions.c) */ +unsigned char pc_menu_split_players( + void); +/* port: local_player_count, 0 before the players' globals are made +(source/game/players.c) */ +short players_port_local_player_count( + void); /* port: where in its widget, and how large, the menus draw a frame of ui.map's that they scale (port/linux/game/menu_tags.c) */ boolean pc_menu_frame_placement( @@ -7636,27 +7644,18 @@ static void widget_instance_tab_to_previous_valid_widget( return; } -/* port: whether a widget of the local player (NONE: any) takes the -controller's events. In co-op's menus (Multiplayer's CO-OP CAMPAIGN, -port/linux/game/menu_functions.c) the screens it shares with one player's -campaign, New Game's levels and the difficulty, are player 1's (their rows -the first controller's), and either player's controller uses them: player 1's -is the one that chose co-op, player 2's the one that chose their profile */ -/* port: whether this machine has, or is adding, a second player -(port/linux/game/menu_functions.c): co-op, split screen, the lobby's */ -extern unsigned char pc_menu_split_players(void); - /* port: with one person playing, any controller drives the first player's menus, not only the first port's: a phone can list a device of its own (its touch controls) before the gamepad, which then reads port 2, and a player picks up whichever pad is at hand. Its screens read every controller's -events (ui_widgets_update), and take them (below). With two or more -players, each controller keeps to its own player's menus */ +events (process_ui_widgets), and take them +(widget_takes_events_of_controller). With two or more players, each +controller keeps to its own player's menus */ static boolean widget_takes_any_controller( struct widget_instance const *widget) { return widget->local_player_index == 0 && !pc_menu_split_players() && - (we_are_at_the_main_menu || local_player_count() <= 1); + (we_are_at_the_main_menu || players_port_local_player_count() <= 1); } /* port: the controller whose events a screen reads (NONE: every one's) */ @@ -7666,6 +7665,12 @@ static short widget_event_controller( return widget_takes_any_controller(widget) ? NONE : widget->local_player_index; } +/* port: whether a widget of the local player (NONE: any) takes the +controller's events. In co-op's menus (Multiplayer's CO-OP CAMPAIGN, +port/linux/game/menu_functions.c) the screens it shares with one player's +campaign, New Game's levels and the difficulty, are player 1's (their rows +the first controller's), and either player's controller uses them: player 1's +is the one that chose co-op, player 2's the one that chose their profile */ static boolean widget_takes_events_of_controller( struct widget_instance const *widget, short controller_index) @@ -7674,8 +7679,8 @@ static boolean widget_takes_events_of_controller( if (widget->local_player_index == NONE || widget->local_player_index == controller_index) return TRUE; - if (widget->local_player_index == 0 && controller_index > 0 && - controller_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS && widget_takes_any_controller(widget)) + if (controller_index > 0 && controller_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS && + widget_takes_any_controller(widget)) { return TRUE; } From 691532327eb374a5ab927777fe9a5233ca155aa7 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:09:24 +1300 Subject: [PATCH 174/187] Frame throttle sleep: a port comment, no unused precache test (#229) The sleep's comment is marked as the port's and says what the spin cost (a core held busy, not system calls: the clock is read through the vDSO on Linux and Android), and the precache test the sleep no longer depends on is gone rather than cast to void. --- source/main/main.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/source/main/main.c b/source/main/main.c index 4a1f096d..3d70fbb8 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -2698,7 +2698,6 @@ void main_rasterizer_throttle( unsigned long start_milliseconds; short lapsed_frames; boolean did_throttle; - boolean precache_in_progress; boolean synchronized; char const *description; @@ -2713,18 +2712,16 @@ void main_rasterizer_throttle( if ((__int64)rasterizer_globals.frame_and_vertical_blank_index < target_index) { start_milliseconds = system_milliseconds(); - precache_in_progress = cache_files_precache_in_progress(); did_throttle = TRUE; profile_idle_start(); while ((__int64)rasterizer_globals.frame_and_vertical_blank_index < target_index) { - /* the native builds sleep between looks: the Xbox spun here, - and on a port each look at the clock is a system call, so a - game held at 30 frames a second (display.interpolation off) - spent half its game thread asking the time. A millisecond is - well inside a 33 ms frame (Windows sets its timer to 1 ms: + /* port: a sleep between looks, not only while a map + precaches: the Xbox spun here, which on a port holds a core + busy for most of each frame of a game held at 30 frames a + second (display.interpolation off). A millisecond is well + inside a 33 ms frame (Windows sets its timer to 1 ms: win32_posix.c) */ - (void)precache_in_progress; Sleep(1); if (system_milliseconds() > start_milliseconds + 1000) { From a58fd479e51eec4a57d27940f2c61bf78bd1f36b Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:14:51 +1300 Subject: [PATCH 175/187] Item resting surface: a port comment, its helpers with the private code (#231) The helpers' comment is marked as the port's and says plainly what the short holds; their definitions move to the file's private code, and the plane the item is moved onto is indented inside its new surface check. --- source/items/items.c | 89 ++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/source/items/items.c b/source/items/items.c index c32d4efb..942008ac 100644 --- a/source/items/items.c +++ b/source/items/items.c @@ -102,10 +102,9 @@ enum /* ---------- prototypes */ -/* the surface an item rests on is a signed short in the datum. a collision -surface past 32767 is negative there (39748 is kept as its low 16 bits). -read back unsigned, it is that surface. none stays none, and 65535 is -none's pattern, so a surface that high is not recorded */ +/* port: the surface an item rests on, kept in a short of the datum: a +collision surface past 32767 (a large map's) is read back unsigned, NONE +stays NONE, and a surface of 65535 or more (NONE's bits) is not kept */ static long item_rested_surface_index( struct item_datum const *item); static struct collision_surface const *item_rested_surface( @@ -119,35 +118,6 @@ static void item_set_rested_surface( static struct profile_section item_update_section = { "item_update", NONE, TRUE }; static real const item_maximum_impact_velocity = 0.1f; -static long item_rested_surface_index( - struct item_datum const *item) -{ - if (item->item.rested_surface_index == NONE) - return NONE; - return (unsigned short)item->item.rested_surface_index; -} - -static struct collision_surface const *item_rested_surface( - struct item_datum const *item) -{ - struct collision_bsp const *bsp = global_collision_bsp_get(); - long surface_index = item_rested_surface_index(item); - - if (!bsp || surface_index < 0 || surface_index >= bsp->surfaces.count) - return NULL; - return TAG_BLOCK_GET_ELEMENT(&bsp->surfaces, surface_index, struct collision_surface); -} - -static void item_set_rested_surface( - struct item_datum *item, - long surface_index) -{ - if (surface_index < 0 || surface_index >= 65535) - item->item.rested_surface_index = NONE; - else - item->item.rested_surface_index = (short)surface_index; -} - /* ---------- public code */ void items_initialize( @@ -323,6 +293,35 @@ void item_detonate( /* ---------- private code */ +static long item_rested_surface_index( + struct item_datum const *item) +{ + if (item->item.rested_surface_index == NONE) + return NONE; + return (unsigned short)item->item.rested_surface_index; +} + +static struct collision_surface const *item_rested_surface( + struct item_datum const *item) +{ + struct collision_bsp const *bsp = global_collision_bsp_get(); + long surface_index = item_rested_surface_index(item); + + if (!bsp || surface_index < 0 || surface_index >= bsp->surfaces.count) + return NULL; + return TAG_BLOCK_GET_ELEMENT(&bsp->surfaces, surface_index, struct collision_surface); +} + +static void item_set_rested_surface( + struct item_datum *item, + long surface_index) +{ + if (surface_index < 0 || surface_index >= 65535) + item->item.rested_surface_index = NONE; + else + item->item.rested_surface_index = (short)surface_index; +} + static void item_adjust_for_angular_velocity_change( long item_index) { @@ -400,19 +399,19 @@ void item_accelerate( surface = item_rested_surface(item); if (surface) { - bsp3d_get_plane_from_designator( - &collision_bsp->bsp3d, - surface->plane_designator, - &plane); + bsp3d_get_plane_from_designator( + &collision_bsp->bsp3d, + surface->plane_designator, + &plane); - distance_above = - 0.05f - (plane3d_distance_to_point(&plane, &marker.matrix.position)); - point_from_line3d( - &marker.matrix.position, - &plane.n, - distance_above, - &new_position); - object_translate(item_index, &new_position, NULL); + distance_above = + 0.05f - (plane3d_distance_to_point(&plane, &marker.matrix.position)); + point_from_line3d( + &marker.matrix.position, + &plane.n, + distance_above, + &new_position); + object_translate(item_index, &new_position, NULL); } } From 30771b1b2cc91e49bf9a1a26789fac130b819c51 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:20:36 +1300 Subject: [PATCH 176/187] Structure triangle limit: port comments (#232) The raised limits' and the walk's comments are marked as the port's, with the unverified HAC2 and MCC comparison left out; the walk's says why the index is compared whole, and the once-only log of a frame reaching the limit gets a comment of its own. --- source/rasterizer/rasterizer.h | 6 +++--- source/render/render.h | 5 ++--- source/structures/structure_visibility.c | 5 +++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/source/rasterizer/rasterizer.h b/source/rasterizer/rasterizer.h index 2d006764..f5740383 100644 --- a/source/rasterizer/rasterizer.h +++ b/source/rasterizer/rasterizer.h @@ -33,9 +33,9 @@ enum RASTERIZER_MAXIMUM_DEBUG_VERTICES = 393216, RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS = 384, RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS2 = 32, - /* the structure list can ask for 32768 triangles. a request that would - fill this buffer is refused, so the room above 32768 is what a frame - still has for lights, shadows, and contrails */ + /* port: the structure's 32768 triangles (render.h) and, past them, as + much again as the Xbox's 16384 left a frame for its lights, shadows and + contrails (a request that would fill the buffer is refused) */ RASTERIZER_MAXIMUM_DYNAMIC_TRIANGLES = 49152, RASTERIZER_MAXIMUM_DYNAMIC_TRIANGLE_BUFFERS = 1024, /* four per particle (build_sprites_begin), for the native builds' larger diff --git a/source/render/render.h b/source/render/render.h index c3c2b3f5..fe115cbe 100644 --- a/source/render/render.h +++ b/source/render/render.h @@ -20,8 +20,7 @@ enum { MAXIMUM_RENDERED_DISTANT_LIGHTS = 2, MAXIMUM_RENDERED_POINT_LIGHTS = 2, - /* retail stops a frame at 16384 structure triangles. 32768 is the - HAC2 and MCC count */ + /* port: twice the Xbox's 16384 structure triangles a frame */ MAXIMUM_RENDERED_ENVIRONMENT_SURFACES = 32768, MAXIMUM_RENDERED_CLUSTERS = 128, MAXIMUM_SURFACES_PER_STRUCTURE = 0x20000, @@ -96,7 +95,7 @@ struct render_globals struct rendered_cluster rendered_clusters[MAXIMUM_RENDERED_CLUSTERS]; short rendered_cluster_count; unsigned long environment_surface_flags[MAXIMUM_SURFACES_PER_STRUCTURE]; - /* a short wraps at 32768, so the raised triangle count lives in a long */ + /* port: a long, as a short would wrap at the raised count */ long environment_surface_count; long environment_surface_indices[MAXIMUM_RENDERED_ENVIRONMENT_SURFACES]; }; diff --git a/source/structures/structure_visibility.c b/source/structures/structure_visibility.c index 4fa14099..069c53bc 100644 --- a/source/structures/structure_visibility.c +++ b/source/structures/structure_visibility.c @@ -470,8 +470,8 @@ static void structure_visibility_traverse_subclusters( } surface_index_buffer = TAG_BLOCK_GET_ELEMENT(&subcluster->surface_indices, 0, long); - /* a short index stops, and then never finishes, once a subcluster - passes 32767 surfaces */ + /* port: compared whole: a short index wraps past 32767 surfaces + and the walk never ends */ for (surface_list_index = 0; surface_list_index < subcluster->surface_indices.count; surface_index_buffer++, surface_list_index++) @@ -2002,6 +2002,7 @@ void structure_visibility_compute( structure_visibility_traverse_surface_lists(structure); } + /* port: a frame that reaches the structure triangles' limit is said once */ if (render.environment_surface_count >= MAXIMUM_RENDERED_ENVIRONMENT_SURFACES && !warned_about_environment_surfaces) { From b5a4870d05fe172e75e3dcd1b3b3e305e84aa214 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:23:16 +1300 Subject: [PATCH 177/187] Saved profile's controls: the comment wrapped as the file's (#227) --- source/interface/player_ui.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/interface/player_ui.c b/source/interface/player_ui.c index e2d42e12..87da69a5 100644 --- a/source/interface/player_ui.c +++ b/source/interface/player_ui.c @@ -805,8 +805,9 @@ boolean player_ui_save_profile( player_profile_save( player_ui_globals.edit_profile_index, &player_ui_globals.edit_profile.current.player); - /* port: a local player playing with the profile gets its changes now (its button and stick - layouts, sensitivity, inversion), not when the profile is next chosen */ + /* port: a local player playing with the profile gets its + changes now (its button and stick layouts, sensitivity, + inversion), not when the profile is next chosen */ { short local_player_index; From 8618c2919a117c7b192179c12d3fa366cc924807 Mon Sep 17 00:00:00 2001 From: justinfilip Date: Fri, 9 Oct 2026 21:41:04 -0500 Subject: [PATCH 178/187] Respawn a large co-op game beside the largest group of teammates. Five or more players come back with that group, beside someone there who is safe to stand next to. A fight in the whole group waits ten seconds. Fewer players still follow the host. --- source/game/players.c | 229 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 208 insertions(+), 21 deletions(-) diff --git a/source/game/players.c b/source/game/players.c index 51dbc730..6d81ff0c 100644 --- a/source/game/players.c +++ b/source/game/players.c @@ -2522,8 +2522,15 @@ that was on another BSP. */ static boolean players_coop_room_to_spawn( void); +static long player_spawnable_beside( + struct player_datum const *player); static boolean player_place_beside_teammate( long player_index); +static long players_coop_respawn_unit( + void); +static boolean players_coop_respawn_beside( + long player_index, + long anchor_unit_index); /* How long into a level the extra players wait before spawning. The opening cutscene starts a tick or so in, and the first player may still be somewhere @@ -2577,8 +2584,14 @@ to respawn beside */ /* how often the respawn's safety test runs while someone is dead */ #define COOP_RESPAWN_CHECK_TICKS (TICKS_PER_SECOND / 2) /* after waiting this long for a safe teammate, the dead respawn beside any -teammate standing on the ground */ +teammate standing on the ground. A large game waits this long for someone +safe inside the largest group, then uses that group anyway */ #define COOP_RESPAWN_FALLBACK_TICKS (10 * TICKS_PER_SECOND) +/* this many players in a campaign game is massive co-op. Respawns go to the +largest group, and a dead host uses that same choice */ +#define COOP_MAJORITY_PLAYERS 5 +/* teammates within this distance (world units) count as one group */ +#define COOP_MAJORITY_RADIUS 30.0f /* TRUE if an enemy projectile (a grenade, a plasma bolt) is flying near the unit. Players' own shots and needles stuck in a body don't count. */ @@ -2646,7 +2659,8 @@ static boolean players_coop_unit_safe( /* the unit of a living player that passes `test`, the host's first (the team follows the host: its players are the host machine's local ones), or -NONE */ +NONE. A game of COOP_MAJORITY_PLAYERS or more respawns with +players_coop_respawn_unit instead. */ static long players_coop_unit_where( boolean (*test)(long unit_index)) { @@ -2668,6 +2682,143 @@ static long players_coop_unit_where( return found_index; } +/* how many players are in this campaign game, not counting someone who quit */ +static short players_coop_session_count( + void) +{ + struct data_iterator iterator; + struct player_datum *player; + short count = 0; + + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL) + { + if (!player->quit_out_of_game) + count++; + } + + return count; +} + +/* TRUE when respawns use the largest group */ +static boolean players_coop_use_majority( + void) +{ + return players_coop_session_count() >= COOP_MAJORITY_PLAYERS; +} + +/* TRUE when a player can be stood beside: on foot, or in a vehicle a player +drives. An AI vehicle (Silent Cartographer's Pelican) is not */ +static boolean players_coop_unit_spawnable( + long unit_index) +{ + long player_index = unit_get(unit_index)->unit.player_index; + + return player_index != NONE && player_spawnable_beside(player_get(player_index)) != NONE; +} + +/* how many listed positions are within radius of the one at index, including +itself */ +static short players_coop_near_count( + real_point3d const *positions, + short count, + short index, + real radius_squared) +{ + short other_index; + short near_count = 0; + + for (other_index = 0; other_index < count; other_index++) + { + if (distance_squared3d(&positions[index], &positions[other_index]) <= radius_squared) + near_count++; + } + + return near_count; +} + +/* who a network co-op respawn is placed beside. Fewer than +COOP_MAJORITY_PLAYERS uses the host's teammate (players_coop_unit_where): +a safe one, or after COOP_RESPAWN_FALLBACK_TICKS one on the ground. A larger +game uses the largest group on the ground. The respawn is beside someone in +that group who is safe to spawn next to. When that whole group is in a +fight, it waits out COOP_RESPAWN_FALLBACK_TICKS and then uses the group +anyway. A safe player outside the group does not win. A dead host has no +unit, so the same count applies to them. NONE while the group is still +unsafe, or while nobody is on the ground. */ +static long players_coop_respawn_unit( + void) +{ + long unit_indices[NETWORK_GAME_MAXIMUM_PLAYER_COUNT]; + real_point3d positions[NETWORK_GAME_MAXIMUM_PLAYER_COUNT]; + struct data_iterator iterator; + struct player_datum *player; + real radius_squared = COOP_MAJORITY_RADIUS * COOP_MAJORITY_RADIUS; + short count = 0; + short anchor_index = 0; + short anchor_count = 0; + short safe_index = -1; + short safe_count = 0; + short index; + + if (!players_coop_use_majority()) + { + long host_index = players_coop_unit_where(players_coop_unit_safe); + + if (host_index == NONE && + game_time_get() + 1 - players_coop_state.respawn_wait_since >= COOP_RESPAWN_FALLBACK_TICKS) + { + host_index = players_coop_unit_where(players_coop_unit_grounded); + } + return host_index; + } + + data_iterator_new(&iterator, player_data); + while ((player = data_iterator_next(&iterator)) != NULL && count < NETWORK_GAME_MAXIMUM_PLAYER_COUNT) + { + if (player->unit_index == NONE || !players_coop_unit_grounded(player->unit_index)) + continue; + unit_indices[count] = player->unit_index; + positions[count] = object_get(player->unit_index)->object.bounding_sphere_center; + count++; + } + if (count == 0) + return NONE; + for (index = 0; index < count; index++) + { + short near_count = players_coop_near_count(positions, count, index, radius_squared); + + if (near_count > anchor_count) + { + anchor_count = near_count; + anchor_index = index; + } + } + for (index = 0; index < count; index++) + { + short near_count; + + if (distance_squared3d(&positions[anchor_index], &positions[index]) > radius_squared || + !players_coop_unit_safe(unit_indices[index]) || + !players_coop_unit_spawnable(unit_indices[index])) + { + continue; + } + near_count = players_coop_near_count(positions, count, index, radius_squared); + if (near_count > safe_count) + { + safe_count = near_count; + safe_index = index; + } + } + if (safe_index >= 0) + return unit_indices[safe_index]; + if (game_time_get() + 1 - players_coop_state.respawn_wait_since >= COOP_RESPAWN_FALLBACK_TICKS) + return unit_indices[anchor_index]; + + return NONE; +} + /* the teammate a network co-op respawn is for, while player_spawn makes the player (players_respawn_network_coop), else NONE */ static long players_coop_spawn_beside_index = NONE; @@ -2700,12 +2851,14 @@ static struct player_starting_location const *players_coop_spawn_location( return location; } -/* Network co-op respawn: the dead come back beside the first teammate who -is safe, or after COOP_RESPAWN_FALLBACK_TICKS beside one at least on the -ground, so a long fight can't keep them out. The campaign's own test -(players_respawn_coop) is map-wide (any projectile or enemy attack -anywhere), which with many players spread out would almost never pass. -Returns whether everyone waiting came back. */ +/* Network co-op respawn: the dead come back beside the first safe teammate, +the host's before the others, or after COOP_RESPAWN_FALLBACK_TICKS beside +one at least on the ground, so a long fight can't keep them out. With +COOP_MAJORITY_PLAYERS or more they come back beside the largest group, and +a dead host does too. The campaign's own test (players_respawn_coop) is +map-wide (any projectile or enemy attack anywhere), which with many players +spread out would almost never pass. Returns whether everyone waiting came +back. */ static boolean players_respawn_network_coop( void) { @@ -2720,12 +2873,7 @@ static boolean players_respawn_network_coop( walks all the map's projectiles, so twice a second is enough */ if (game_time_get() % COOP_RESPAWN_CHECK_TICKS != 0) return FALSE; - safe_unit_index = players_coop_unit_where(players_coop_unit_safe); - if (safe_unit_index == NONE && - game_time_get() + 1 - players_coop_state.respawn_wait_since >= COOP_RESPAWN_FALLBACK_TICKS) - { - safe_unit_index = players_coop_unit_where(players_coop_unit_grounded); - } + safe_unit_index = players_coop_respawn_unit(); if (safe_unit_index == NONE) return FALSE; data_iterator_new(&iterator, player_data); @@ -2742,14 +2890,10 @@ static boolean players_respawn_network_coop( players_coop_spawn_beside_index = NONE; if (player->unit_index == NONE) result = FALSE; - /* (beside the safe teammate, else any with room; with none, left - behind the teammate, where player_spawn put them) */ - else if (!player_teleport(iterator.datum_index, safe_unit_index, - &object_get(safe_unit_index)->object.bounding_sphere_center) && - !player_place_beside_teammate(iterator.datum_index)) - { + /* (beside that teammate, else somewhere in their group; with no + room, left behind them, where player_spawn put them) */ + else if (!players_coop_respawn_beside(iterator.datum_index, safe_unit_index)) result = FALSE; - } } if (result) players_coop_state.respawn_wait_since = 0; @@ -3040,6 +3184,49 @@ static boolean player_place_beside_teammate( return FALSE; } +/* Moves a respawned player beside the chosen teammate. In a massive game +the other spots tried are only teammates in that same group, so a failed +spot beside the anchor does not send them to a player off on their own. +Smaller games still try any teammate with room. FALSE leaves the player +where player_spawn put them, behind the anchor. */ +static boolean players_coop_respawn_beside( + long player_index, + long anchor_unit_index) +{ + real_point3d anchor_position; + struct data_iterator iterator; + struct player_datum *other; + real radius_squared; + + if (player_get(player_index)->unit_index == NONE) + return FALSE; + anchor_position = object_get(anchor_unit_index)->object.bounding_sphere_center; + if (player_teleport(player_index, anchor_unit_index, &anchor_position)) + return TRUE; + if (!players_coop_use_majority()) + return player_place_beside_teammate(player_index); + + radius_squared = COOP_MAJORITY_RADIUS * COOP_MAJORITY_RADIUS; + data_iterator_new(&iterator, player_data); + while ((other = data_iterator_next(&iterator)) != NULL) + { + if (iterator.datum_index == player_index || other->unit_index == NONE || + other->unit_index == anchor_unit_index || player_spawnable_beside(other) == NONE) + { + continue; + } + if (distance_squared3d(&anchor_position, + &object_get(other->unit_index)->object.bounding_sphere_center) <= radius_squared && + player_teleport(player_index, other->unit_index, + &object_get(other->unit_index)->object.bounding_sphere_center)) + { + return TRUE; + } + } + + return FALSE; +} + void players_note_checkpoint( void) { From 867425241c71154922abd40b7bec8e2561e26fe7 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:51:47 +1300 Subject: [PATCH 179/187] HALO_GLES for the embedded SMAA data, the desktop check without all three (#123) embed_assets.py still wrapped the SMAA shader and textures in #ifndef HALO_ANDROID while xgpu_post.c now uses them under #ifndef HALO_GLES, and wsl_desktop_check.sh took out only -DHALO_ANDROID, so its desktop branches would have been checked with the guest's and the ES renderer's defines still set. The Android README's doubled blank line goes too. --- port/android/README.md | 1 - tools/embed_assets.py | 4 ++-- tools/wsl_desktop_check.sh | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/port/android/README.md b/port/android/README.md index 39bb8939..e0435eb8 100644 --- a/port/android/README.md +++ b/port/android/README.md @@ -397,7 +397,6 @@ Android build defines all of (`tools/android_build.py`): - `HALO_GLES`: the OpenGL ES renderer; - `HALO_ANDROID`: the app (its display, input, files and lifecycle). - The x86 inline assembly is replaced by C (refer to [port/linux/README.md](../linux/README.md#game-source-changes)). These changes are in `#ifdef HALO_ARM64_GUEST`: diff --git a/tools/embed_assets.py b/tools/embed_assets.py index 0b0f6c19..bb225eda 100644 --- a/tools/embed_assets.py +++ b/tools/embed_assets.py @@ -211,8 +211,8 @@ def main() -> None: lines.append("") # SMAA's shader, as text a GLSL compiler takes (ASCII, ending in a NUL), # and its lookup textures (xgpu_post.c); each of size 0 that the - # checkout does not have. Android has no SMAA. - lines.append("#ifndef HALO_ANDROID") + # checkout does not have. The OpenGL ES renderer has no SMAA. + lines.append("#ifndef HALO_GLES") present = dict(smaa_files()) for name, symbol in SMAA_FILES: data = (ROOT / SMAA_ASSETS / name).read_bytes() if name in present else b"" diff --git a/tools/wsl_desktop_check.sh b/tools/wsl_desktop_check.sh index 4a442ad6..73d0ca55 100644 --- a/tools/wsl_desktop_check.sh +++ b/tools/wsl_desktop_check.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Compiles the given platform files (port/linux/src) as the Android guest -# build does, but without HALO_ANDROID, syntax only: the desktop branches of -# the shared code. Warnings are off, but a call of an undeclared function is -# still an error. Only files that the guest build compiles can be checked +# build does, but without HALO_ARM64_GUEST, HALO_GLES and HALO_ANDROID, +# syntax only: the desktop branches of the shared code. Warnings are off, +# but a call of an undeclared function is still an error. Only files that the guest build compiles can be checked # (not memory_watch.c or posix_*). Run in ~/halo-build/src after a build. set -uo pipefail cd "$HOME/halo-build/src" @@ -12,7 +12,7 @@ for file in "$@"; do command=$(ninja -t commands "$object" | tail -1) if [ -z "$command" ]; then echo "no command for $object"; status=1; continue; fi # (the command chains the compile with the assembly steps: keep the compile) - command=$(echo "$command" | sed -E 's/ && .*//; s/ -DHALO_ANDROID(=[^ ]*)?//g; s/ -w( |$)/ /g; s/ -o [^ ]+//; s/ -MMD//; s/ -MF [^ ]+//') + command=$(echo "$command" | sed -E 's/ && .*//; s/ -DHALO_(ARM64_GUEST|GLES|ANDROID)(=[^ ]*)?//g; s/ -w( |$)/ /g; s/ -o [^ ]+//; s/ -MMD//; s/ -MF [^ ]+//') # (__i386__: the prefix header accepts only the 32-bit x86 or Android targets) if eval "$command -D__i386__ -fsyntax-only -w -Werror=implicit-function-declaration"; then echo "ok $file"; else echo "FAILED $file"; status=1; fi done From b1a71709efda382d0fcecdc1ec53b91a8536bb5b Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:23:40 +1300 Subject: [PATCH 180/187] FOV and viewmodel settings: one unit, port marks, the lens flare cull (#234) render_fov.c, viewmodel_fov.c and viewmodel_visibility.c are one unit, port/linux/game/view_fov.c, with a header comment as the others have, real for float, _pi for its own pi, the rasterizer's target by name, and no isfinite shim in math.h (the angles' range test already refuses NaN). The game code's hooks are marked "port:", their include at the end of each block, and rasterizer_lights.c's loops declare before they act (C89). The first-person weapon's lens flares are not culled by the world's frustum (main's new off-screen cull): they are drawn at the weapon's own projection, which display.viewmodel_fov can make wider than the world's. HALO_VIEWMODEL_VIS is HALO_VIEWMODEL_VISIBLE; the settings' descriptions and help lines say "stock" view and fit their boxes; Video Setup's XML is written again by ce_menus.py; the README's field of view text is a section of its own. --- ...ile_edit.video_settings.fov_viewmodels.xml | 4 +- ...tup.player_profile_edit.video_settings.xml | 3 +- port/linux/README.md | 44 +-- port/linux/game/render_fov.c | 136 --------- port/linux/game/render_fov.h | 12 - port/linux/game/view_fov.c | 269 ++++++++++++++++++ port/linux/game/view_fov.h | 31 ++ port/linux/game/viewmodel_fov.c | 85 ------ port/linux/game/viewmodel_fov.h | 6 - port/linux/game/viewmodel_visibility.c | 23 -- port/linux/game/viewmodel_visibility.h | 6 - port/linux/include/math.h | 6 - port/linux/src/port_config.c | 17 +- source/interface/first_person_weapons.c | 3 +- source/interface/hud_draw.c | 10 +- source/interface/hud_weapon.c | 5 +- source/main/main.c | 3 +- source/rasterizer/rasterizer_lights.c | 35 ++- .../rasterizer_transparent_geometry.c | 5 +- .../rasterizer/xbox/rasterizer_xbox_models.c | 5 +- source/render/render_particles.c | 3 +- source/render/render_sprite.c | 4 +- tools/port_settings.py | 6 +- 23 files changed, 390 insertions(+), 331 deletions(-) delete mode 100644 port/linux/game/render_fov.c delete mode 100644 port/linux/game/render_fov.h create mode 100644 port/linux/game/view_fov.c create mode 100644 port/linux/game/view_fov.h delete mode 100644 port/linux/game/viewmodel_fov.c delete mode 100644 port/linux/game/viewmodel_fov.h delete mode 100644 port/linux/game/viewmodel_visibility.c delete mode 100644 port/linux/game/viewmodel_visibility.h diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml index 1cea9b97..faf580b8 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml @@ -67,8 +67,8 @@ - - + + diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml index 7d4d8d36..0173ec76 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml @@ -155,7 +155,6 @@ - + - diff --git a/port/linux/README.md b/port/linux/README.md index 0c621fde..bbfcd2aa 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -316,21 +316,6 @@ from the next main menu. Each setting has an environment variable. The environment variable changes the setting for one start of the game. It has priority over the file. -Video Setup's **FOV AND VIEWMODELS** category offers optional world and -weapon field of view settings and first person visibility. **DEFAULT** keeps -the authored world projection. **DEFAULT** for the weapon keeps its original -view, and viewmodels start **ON**. A chosen weapon angle is that FOV. OK -saves and applies changes. Cancel discards them. The menu offers 80 to 150 -degrees in steps of five. The config file accepts 20 to 150 degrees, -including fractional values, or zero for Default. -Angles are horizontal at 16:9; other aspect ratios retain the same vertical -view. World FOV applies to on-foot first-person views, preserving authored -vehicle, death and cinematic cameras. Completed scopes keep their native -view with wider FOV; a narrower FOV also narrows the scope. Aiming reticles -scale with the world projection, while scope artwork keeps its layout. -Viewmodel FOV also applies to attached visuals. Hiding viewmodels changes -only their display; firing, animation, sound and world lights continue. - | Setting | Default | Environment variable | Function | | --- | --- | --- | --- | | `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at the mode of `display.resolution` (the nearest the display has), or at its desktop mode. `"borderless"`: a window over the whole desktop, whose mode does not change. `"windowed"`: a window of `display.window_size`. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. | @@ -345,9 +330,9 @@ only their display; firing, animation, sound and world lights continue. | `debug.gpu_flush_draws` | `-1` | `HALO_GPU_FLUSH_DRAWS` | Flush the GPU's pipeline every this many draws. `-1`: every 3 on Intel graphics with Mesa's driver, which can otherwise hang in the game's long runs of small draws and reset the desktop's graphics too. `0`: never. | | `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". | | `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". | -| `display.fov` | `0.0` | `HALO_FOV` | On-foot first-person horizontal FOV at 16:9, from 20 to 150 degrees. `0` keeps the authored view. Video Setup's FOV and Viewmodels screen sets it. | -| `display.viewmodel_fov` | `0.0` | `HALO_VIEWMODEL_FOV` | Weapon and hands horizontal FOV at 16:9, 20 to 150 degrees. `0` is Default and keeps the original weapon view. A chosen angle is that FOV. | -| `display.viewmodel_visible` | `true` | `HALO_VIEWMODEL_VIS` | Show first-person weapons, hands and attached visuals. This does not change gameplay or other players' models. | +| `display.fov` | `0.0` | `HALO_FOV` | The first-person view's field of view on foot, in degrees across at 16:9, from 20 to 150. `0`: the stock view. Refer to "Field of view". | +| `display.viewmodel_fov` | `0.0` | `HALO_VIEWMODEL_FOV` | The first-person weapon's and hands' field of view, in degrees across at 16:9, from 20 to 150. `0`: the weapon's stock view, also when `display.fov` widens the world. Refer to "Field of view". | +| `display.viewmodel_visible` | `true` | `HALO_VIEWMODEL_VISIBLE` | `true`: the first-person weapon, hands and what is attached to them are drawn. `false`: they are not; firing, animation, sound and lights go on, and other players' models are drawn. | | `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. | | `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the resolution the game draws at, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. | | `display.shadow_resolution` | `128` | `HALO_SHADOW_RESOLUTION` | The size of the maps that the shadows of the objects are drawn in, in pixels each way: `128`, `256`, `512` or `1024` (other values go down to one of these). The game draws the shadow of each object into a map of 128x128 pixels, blurs it and projects it onto the ground. On a large screen, the edges of these shadows show steps that move when the object moves. A larger map makes the edges smooth; the blur is made wider to match, so the shadows are as soft as on the Xbox. Each doubling adds two passes of the blur. `128`: as on the Xbox. | @@ -489,6 +474,29 @@ the world (such as `rasterizer_wireframe`). Refer to `NETCODE.md`. The frame rate shows at the bottom right of the screen. It is the mean over half a second. +## Field of view + +Video Setup's FOV AND VIEWMODELS screen sets the field of view of the +first-person view (`display.fov`) and of the weapon and hands +(`display.viewmodel_fov`), from 80 to 150 degrees in steps of 5, and can +hide the weapon (`display.viewmodel_visible`). DEFAULT, the setting's `0`, +keeps the stock view. `config.toml` takes any angle from 20 to 150. + +- An angle is across the screen at 16:9. Another shape keeps the same + angle up and down, as the stock view does. +- The field of view applies on foot in first person only. Vehicles, death, + cinematics and scripted cameras keep their own view. +- A scope's zoom levels keep their stock view: the extra width fades out as + the zoom comes in. A view narrower than the stock one narrows the zoom + too. +- The reticles scale with the view, so that they stay on the aim. The + scopes' pictures keep their place. +- By default the weapon keeps its stock view when the world is wider: + arms and a gun right against the camera stretch at a wide angle. + +Only this machine's view changes. Nothing the machines send each other +changes, so players with different settings play together. + ## System link The Xbox game lets 16 players on 4 machines play a system link game. This diff --git a/port/linux/game/render_fov.c b/port/linux/game/render_fov.c deleted file mode 100644 index cfb65b6e..00000000 --- a/port/linux/game/render_fov.c +++ /dev/null @@ -1,136 +0,0 @@ -/* FOV in horizontal degrees at 16:9. The observer, its transitions and the -network's camera values remain authored; only the local render view changes. -Default follows the original projection exactly. */ -#include "cseries.h" -#include "render_fov.h" -#include "camera/director.h" -#include "cutscene/cinematics.h" -#include "game/players.h" -#include "objects/objects.h" -#include "units/units.h" -#include "units/unit_definitions.h" -#include "items/weapons.h" -#include "render/render_cameras.h" - -#include - -double config_real(const char *name); -unsigned long config_changes(void); - -static real reticle_scales[MAXIMUM_LOCAL_PLAYERS]; -static real authored_vertical[MAXIMUM_LOCAL_PLAYERS]; - -static float render_fov_adjust(short local_player_index, float native_vertical_field_of_view) -{ - static boolean initialized; - static unsigned long read_at; - static real requested_tangent; - unsigned long changes = config_changes(); - long unit_index, weapon_index; - struct unit_datum *unit; - real unzoomed_angle, unzoomed_tangent, native_tangent, adjusted_tangent; - - if (!initialized || read_at != changes) - { - double degrees = config_real("display.fov"); - - initialized = TRUE; - read_at = changes; - requested_tangent = isfinite(degrees) && degrees >= 20.0 && degrees <= 150.0 ? - (real)(tan(degrees * 3.14159265358979323846 / 360.0) * (9.0 / 16.0)) : 0.0f; - } - if (!requested_tangent || local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || - cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) || - director_get_perspective(local_player_index) != _director_perspective_first_person || - director_inhibited_facing(local_player_index)) - { - return native_vertical_field_of_view; - } - unit_index = player_control_get_unit_index(local_player_index); - if (unit_index == NONE) - return native_vertical_field_of_view; - unit = unit_try_and_get(unit_index); - if (!unit || unit->object.parent_object_index != NONE || TEST_FLAG(unit->object.damage_flags, _object_dead_bit)) - return native_vertical_field_of_view; - - unzoomed_angle = unit_definition_get(unit->definition_index)->unit.camera_field_of_view; - if (!(unzoomed_angle >= 0.001f && unzoomed_angle <= _pi / 2.0f) || - !(native_vertical_field_of_view > 0.0f && native_vertical_field_of_view < _pi)) - { - return native_vertical_field_of_view; - } - unzoomed_tangent = 0.75f * render_camera_get_adjusted_field_of_view_tangent(unzoomed_angle); - native_tangent = tanf(native_vertical_field_of_view * 0.5f); - if (requested_tangent < unzoomed_tangent) - { - /* A narrow base view must not become wider when zoom is pressed. */ - adjusted_tangent = native_tangent * (requested_tangent / unzoomed_tangent); - } - else - { - real blend = 1.0f; - - weapon_index = unit_inventory_get_weapon(unit_index, unit->unit.current_weapon_index); - if (weapon_index != NONE) - { - real scoped_angle = weapon_get_field_of_view(weapon_index, unzoomed_angle, 0); - - if (scoped_angle > 0.0f && scoped_angle < unzoomed_angle) - { - real scoped_tangent = 0.75f * render_camera_get_adjusted_field_of_view_tangent(scoped_angle); - /* The observer's atan/tan round trip can land just above - the endpoint. Keep a completed scope's original angle exactly. */ - if (native_tangent <= scoped_tangent * 1.000001f) - return native_vertical_field_of_view; - - /* Fade the extra width along the observer's existing scope - transition. Every completed scope level keeps its stock view. */ - blend = PIN((native_tangent - scoped_tangent) / - (unzoomed_tangent - scoped_tangent), 0.0f, 1.0f); - } - } - if (blend == 0.0f) - return native_vertical_field_of_view; - adjusted_tangent = native_tangent + (requested_tangent - unzoomed_tangent) * blend; - } - return 2.0f * atanf(adjusted_tangent); -} - -float render_fov_vertical(short local_player_index, float native_vertical_field_of_view) -{ - real adjusted = render_fov_adjust(local_player_index, native_vertical_field_of_view); - - if (local_player_index >= 0 && local_player_index < MAXIMUM_LOCAL_PLAYERS) - { - real scale = 1.0f; - - if (adjusted != native_vertical_field_of_view && - native_vertical_field_of_view > 0.0f && native_vertical_field_of_view < _pi && - adjusted > 0.0f && adjusted < _pi) - { - scale = tanf(native_vertical_field_of_view * 0.5f) / tanf(adjusted * 0.5f); - if (!isfinite(scale) || scale <= 0.0f) scale = 1.0f; - } - /* Record the projection used for this view, including its native zoom - * transition. A temporary viewmodel projection must not affect the HUD. */ - reticle_scales[local_player_index] = scale; - /* A widened world view. The weapon can return to this frame's own angle. */ - authored_vertical[local_player_index] = - adjusted != native_vertical_field_of_view ? native_vertical_field_of_view : 0.0f; - } - return adjusted; -} - -float render_fov_authored_vertical(short local_player_index) -{ - if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS) - return 0.0f; - return authored_vertical[local_player_index]; -} - -float render_fov_reticle_scale(short local_player_index) -{ - if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || - reticle_scales[local_player_index] == 0.0f) return 1.0f; - return reticle_scales[local_player_index]; -} diff --git a/port/linux/game/render_fov.h b/port/linux/game/render_fov.h deleted file mode 100644 index 024a98b8..00000000 --- a/port/linux/game/render_fov.h +++ /dev/null @@ -1,12 +0,0 @@ -/* A local display adjustment, after the observer and before either render -camera's projection and visibility frustum are built. */ -#ifndef RENDER_FOV_H -#define RENDER_FOV_H - -float render_fov_vertical(short local_player_index, float native_vertical_field_of_view); -/* HUD pixel ratio relative to the native projection of the last local view. */ -float render_fov_reticle_scale(short local_player_index); -/* The view's own vertical angle when the world FOV replaced it, else 0. */ -float render_fov_authored_vertical(short local_player_index); - -#endif diff --git a/port/linux/game/view_fov.c b/port/linux/game/view_fov.c new file mode 100644 index 00000000..c6f3f6ac --- /dev/null +++ b/port/linux/game/view_fov.c @@ -0,0 +1,269 @@ +/* +VIEW_FOV.C + +The first-person view's field of view (display.fov, display.viewmodel_fov) +and whether the weapon is drawn (display.viewmodel_visible), the local +render view only: the observer, its transitions and what the network sends +keep the game's own camera. Each angle is horizontal at 16:9 (the vertical +angle is kept, so other shapes of window widen or narrow as the stock view +does), and 0 keeps the stock view. Called from the game's camera +(main.c's set_window_camera_values), its HUD (hud_draw.c) and the passes +that draw the first-person weapon (rasterizer and render sources). +*/ + +#include "cseries.h" +#include "view_fov.h" +#include "camera/director.h" +#include "cutscene/cinematics.h" +#include "game/players.h" +#include "items/weapons.h" +#include "objects/objects.h" +#include "rasterizer/rasterizer.h" +#include "rasterizer/xbox/rasterizer_xbox_internal.h" +#include "render/render.h" +#include "render/render_cameras.h" +#include "render/render_cameras_internal.h" +#include "units/unit_definitions.h" +#include "units/units.h" + +#include + +/* (the window's rasterizer target, as the rasterizer's sources number it) */ +enum +{ + _rasterizer_target_render_primary = 0 +}; + +double config_real(const char *name); +int config_boolean(const char *name); +unsigned long config_changes(void); + +/* ---------- the world's field of view */ + +static real reticle_scales[MAXIMUM_LOCAL_PLAYERS]; +static real authored_vertical[MAXIMUM_LOCAL_PLAYERS]; + +/* the view's vertical angle for display.fov: the game's own in a vehicle, +dead, in a cinematic, a scripted camera or a view the director holds, and +at a scope's full zoom; between, the zoom's own transition */ +static real render_fov_adjust(short local_player_index, real native_vertical_field_of_view) +{ + static boolean initialized; + static unsigned long read_at; + static real requested_tangent; + unsigned long changes = config_changes(); + long unit_index, weapon_index; + struct unit_datum *unit; + real unzoomed_angle, unzoomed_tangent, native_tangent, adjusted_tangent; + + if (!initialized || read_at != changes) + { + double degrees = config_real("display.fov"); + + initialized = TRUE; + read_at = changes; + requested_tangent = degrees >= 20.0 && degrees <= 150.0 ? + (real)(tan(degrees * _pi / 360.0) * (9.0 / 16.0)) : 0.0f; + } + if (!requested_tangent || local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || + cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) || + director_get_perspective(local_player_index) != _director_perspective_first_person || + director_inhibited_facing(local_player_index)) + { + return native_vertical_field_of_view; + } + unit_index = player_control_get_unit_index(local_player_index); + if (unit_index == NONE) + return native_vertical_field_of_view; + unit = unit_try_and_get(unit_index); + if (!unit || unit->object.parent_object_index != NONE || TEST_FLAG(unit->object.damage_flags, _object_dead_bit)) + return native_vertical_field_of_view; + + unzoomed_angle = unit_definition_get(unit->definition_index)->unit.camera_field_of_view; + if (!(unzoomed_angle >= 0.001f && unzoomed_angle <= _pi / 2.0f) || + !(native_vertical_field_of_view > 0.0f && native_vertical_field_of_view < _pi)) + { + return native_vertical_field_of_view; + } + unzoomed_tangent = 0.75f * render_camera_get_adjusted_field_of_view_tangent(unzoomed_angle); + native_tangent = tanf(native_vertical_field_of_view * 0.5f); + if (requested_tangent < unzoomed_tangent) + { + /* (a view narrower than the stock one: the zoom narrows it further) */ + adjusted_tangent = native_tangent * (requested_tangent / unzoomed_tangent); + } + else + { + real blend = 1.0f; + + weapon_index = unit_inventory_get_weapon(unit_index, unit->unit.current_weapon_index); + if (weapon_index != NONE) + { + real scoped_angle = weapon_get_field_of_view(weapon_index, unzoomed_angle, 0); + + if (scoped_angle > 0.0f && scoped_angle < unzoomed_angle) + { + real scoped_tangent = 0.75f * render_camera_get_adjusted_field_of_view_tangent(scoped_angle); + /* (a scope's full zoom keeps its stock angle exactly: the + observer's arctangent and tangent can land just above it) */ + if (native_tangent <= scoped_tangent * 1.000001f) + return native_vertical_field_of_view; + + /* (the extra width fades out along the scope's own + transition, so every zoom level reached is the stock view) */ + blend = PIN((native_tangent - scoped_tangent) / + (unzoomed_tangent - scoped_tangent), 0.0f, 1.0f); + } + } + if (blend == 0.0f) + return native_vertical_field_of_view; + adjusted_tangent = native_tangent + (requested_tangent - unzoomed_tangent) * blend; + } + return 2.0f * atanf(adjusted_tangent); +} + +real render_fov_vertical(short local_player_index, real native_vertical_field_of_view) +{ + real adjusted = render_fov_adjust(local_player_index, native_vertical_field_of_view); + + if (local_player_index >= 0 && local_player_index < MAXIMUM_LOCAL_PLAYERS) + { + real scale = 1.0f; + + if (adjusted != native_vertical_field_of_view && + native_vertical_field_of_view > 0.0f && native_vertical_field_of_view < _pi && + adjusted > 0.0f && adjusted < _pi) + { + scale = tanf(native_vertical_field_of_view * 0.5f) / tanf(adjusted * 0.5f); + if (!(scale > 0.0f && scale < 1000.0f)) + scale = 1.0f; + } + /* (the view's projection, the zoom's transition too, for the HUD: + the weapon's own projection, viewmodel_projection_begin, is not it) */ + reticle_scales[local_player_index] = scale; + /* (a widened view's own angle, which the weapon keeps by default) */ + authored_vertical[local_player_index] = + adjusted != native_vertical_field_of_view ? native_vertical_field_of_view : 0.0f; + } + return adjusted; +} + +real render_fov_authored_vertical(short local_player_index) +{ + if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS) + return 0.0f; + return authored_vertical[local_player_index]; +} + +real render_fov_reticle_scale(short local_player_index) +{ + if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || + reticle_scales[local_player_index] == 0.0f) + { + return 1.0f; + } + return reticle_scales[local_player_index]; +} + +/* ---------- the first-person weapon's */ + +/* how deep the weapon's passes nest, and whether the outermost changed the +view (the cameras and frustums it saved) */ +static unsigned long projection_depth; +static boolean projection_applied; +static struct render_camera saved_render_camera, saved_rasterizer_camera; +static struct render_frustum saved_render_frustum, saved_rasterizer_frustum; + +static real viewmodel_vertical(void) +{ + static boolean initialized; + static unsigned long read_at; + static real angle; + unsigned long changes = config_changes(); + + if (!initialized || read_at != changes) + { + double degrees = config_real("display.viewmodel_fov"); + + angle = degrees >= 20.0 && degrees <= 150.0 ? + (real)(2.0 * atan(tan(degrees * _pi / 360.0) * (9.0 / 16.0))) : 0.0f; + initialized = TRUE; + read_at = changes; + } + return angle; +} + +void viewmodel_projection_begin(void) +{ + real angle; + real_rectangle2d render_bounds, rasterizer_bounds; + + if (projection_depth++ != 0) + return; + projection_applied = FALSE; + /* (display.viewmodel_fov's angle; by default the view's own, from before + display.fov widened it: arms and a gun right against the camera stretch + at a wide angle) */ + angle = viewmodel_vertical(); + if (!angle) + angle = render_fov_authored_vertical(render.local_player_index); + if (!angle || render.local_player_index < 0 || render.local_player_index >= MAXIMUM_LOCAL_PLAYERS || + global_window_parameters.rasterizer_target != _rasterizer_target_render_primary || + cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) || + director_get_perspective(render.local_player_index) != _director_perspective_first_person || + director_inhibited_facing(render.local_player_index)) + { + return; + } + + saved_render_camera = render.camera; + saved_render_frustum = render.frustum; + saved_rasterizer_camera = global_window_parameters.camera; + saved_rasterizer_frustum = global_window_parameters.frustum; + /* (the frustums' own bounds, the view's crop of a split screen: the + visibility and the projection change together, the weapon's pose and + markers do not; get_projection_bounds' slopes are not bounds) */ + render_bounds = saved_render_frustum.frustum_bounds; + rasterizer_bounds = saved_rasterizer_frustum.frustum_bounds; + render.camera.vertical_field_of_view = angle; + global_window_parameters.camera.vertical_field_of_view = angle; + render_camera_build_frustum(&render.camera, &render_bounds, &render.frustum, TRUE); + render_camera_build_frustum(&global_window_parameters.camera, &rasterizer_bounds, + &global_window_parameters.frustum, TRUE); + projection_applied = TRUE; + rasterizer_set_frustum_z(0.0f, 0.0f); +} + +void viewmodel_projection_end(void) +{ + if (!projection_depth || --projection_depth || !projection_applied) + return; + render.camera = saved_render_camera; + render.frustum = saved_render_frustum; + global_window_parameters.camera = saved_rasterizer_camera; + global_window_parameters.frustum = saved_rasterizer_frustum; + projection_applied = FALSE; + rasterizer_set_frustum_z(0.0f, 0.0f); +} + +/* ---------- whether the first-person weapon is drawn */ + +boolean viewmodel_is_visible(void) +{ + static boolean initialized, visible; + static unsigned long read_at; + unsigned long changes = config_changes(); + + if (!initialized || read_at != changes) + { + visible = config_boolean("display.viewmodel_visible") != 0; + initialized = TRUE; + read_at = changes; + } + return visible; +} + +boolean viewmodel_draws_geometry(boolean first_person) +{ + return !first_person || viewmodel_is_visible(); +} diff --git a/port/linux/game/view_fov.h b/port/linux/game/view_fov.h new file mode 100644 index 00000000..6f410e0a --- /dev/null +++ b/port/linux/game/view_fov.h @@ -0,0 +1,31 @@ +/* +VIEW_FOV.H + +The first-person view's field of view and weapon (view_fov.c). +*/ + +#ifndef VIEW_FOV_H +#define VIEW_FOV_H + +/* the vertical angle the local player's world view is drawn at (display.fov, +after the observer and before the projection and the visibility frustum +are built), from the game's own */ +real render_fov_vertical(short local_player_index, real native_vertical_field_of_view); +/* how much the reticle shrinks or grows to stay on its aim at that view (the +last local view's own projection against the game's): 1 for the stock view */ +real render_fov_reticle_scale(short local_player_index); +/* the view's own vertical angle when display.fov widened it, else 0 */ +real render_fov_authored_vertical(short local_player_index); + +/* the first-person weapon's projection (display.viewmodel_fov) around its +passes: balanced and nestable, the current local render view only */ +void viewmodel_projection_begin(void); +void viewmodel_projection_end(void); + +/* whether the first-person weapon, hands and attached visuals are drawn +(display.viewmodel_visible); its firing, sounds and lights go on either way */ +boolean viewmodel_is_visible(void); +/* whether a pass draws: not one of the first-person weapon's that is hidden */ +boolean viewmodel_draws_geometry(boolean first_person); + +#endif diff --git a/port/linux/game/viewmodel_fov.c b/port/linux/game/viewmodel_fov.c deleted file mode 100644 index d049a836..00000000 --- a/port/linux/game/viewmodel_fov.c +++ /dev/null @@ -1,85 +0,0 @@ -#include "cseries.h" -#include "viewmodel_fov.h" -#include "render_fov.h" -#include "camera/director.h" -#include "cutscene/cinematics.h" -#include "game/players.h" -#include "render/render.h" -#include "render/render_cameras.h" -#include "render/render_cameras_internal.h" -#include "rasterizer/rasterizer.h" -#include "rasterizer/xbox/rasterizer_xbox_internal.h" -#include - -double config_real(const char *name); -unsigned long config_changes(void); - -static unsigned projection_depth; -static boolean projection_applied; -static struct render_camera saved_render_camera, saved_rasterizer_camera; -static struct render_frustum saved_render_frustum, saved_rasterizer_frustum; - -static real viewmodel_vertical(void) -{ - static boolean initialized; - static unsigned long read_at; - static real angle; - unsigned long changes = config_changes(); - if (!initialized || read_at != changes) - { - double degrees = config_real("display.viewmodel_fov"); - angle = isfinite(degrees) && degrees >= 20.0 && degrees <= 150.0 ? - (real)(2.0 * atan(tan(degrees * 3.14159265358979323846 / 360.0) * (9.0 / 16.0))) : 0.0f; - initialized = TRUE; - read_at = changes; - } - return angle; -} - -void viewmodel_projection_begin(void) -{ - real angle; - real_rectangle2d render_bounds, rasterizer_bounds; - if (projection_depth++ != 0) return; - projection_applied = FALSE; - angle = viewmodel_vertical(); - /* Default keeps the weapon at this frame's own angle. A chosen angle - is that FOV and is left as it is. A wide world view stretches arms - and a gun that sit against the camera, so Default does not follow it. */ - if (!angle) - angle = render_fov_authored_vertical(render.local_player_index); - if (!angle || render.local_player_index < 0 || render.local_player_index >= MAXIMUM_LOCAL_PLAYERS || - global_window_parameters.rasterizer_target != 0 /* primary view */ || - cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) || - director_get_perspective(render.local_player_index) != _director_perspective_first_person || - director_inhibited_facing(render.local_player_index)) return; - - saved_render_camera = render.camera; - saved_render_frustum = render.frustum; - saved_rasterizer_camera = global_window_parameters.camera; - saved_rasterizer_frustum = global_window_parameters.frustum; - /* Retain cropped/tiled viewport bounds. CPU visibility and GPU projection - change together; pose, weapon markers and gameplay values stay put. */ - /* The builder takes normalized crop bounds. get_projection_bounds instead - returns signed view-space ray slopes, which would invert/rescale the view. */ - render_bounds = saved_render_frustum.frustum_bounds; - rasterizer_bounds = saved_rasterizer_frustum.frustum_bounds; - render.camera.vertical_field_of_view = angle; - global_window_parameters.camera.vertical_field_of_view = angle; - render_camera_build_frustum(&render.camera, &render_bounds, &render.frustum, TRUE); - render_camera_build_frustum(&global_window_parameters.camera, &rasterizer_bounds, - &global_window_parameters.frustum, TRUE); - projection_applied = TRUE; - rasterizer_set_frustum_z(0.0f, 0.0f); -} - -void viewmodel_projection_end(void) -{ - if (!projection_depth || --projection_depth || !projection_applied) return; - render.camera = saved_render_camera; - render.frustum = saved_render_frustum; - global_window_parameters.camera = saved_rasterizer_camera; - global_window_parameters.frustum = saved_rasterizer_frustum; - projection_applied = FALSE; - rasterizer_set_frustum_z(0.0f, 0.0f); -} diff --git a/port/linux/game/viewmodel_fov.h b/port/linux/game/viewmodel_fov.h deleted file mode 100644 index f3fc9471..00000000 --- a/port/linux/game/viewmodel_fov.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef VIEWMODEL_FOV_H -#define VIEWMODEL_FOV_H -/* Balanced, nestable scopes change only the current local render view. */ -void viewmodel_projection_begin(void); -void viewmodel_projection_end(void); -#endif diff --git a/port/linux/game/viewmodel_visibility.c b/port/linux/game/viewmodel_visibility.c deleted file mode 100644 index 09f8d860..00000000 --- a/port/linux/game/viewmodel_visibility.c +++ /dev/null @@ -1,23 +0,0 @@ -#include "viewmodel_visibility.h" - -int config_boolean(const char *name); -unsigned long config_changes(void); - -int viewmodel_is_visible(void) -{ - static int initialized, visible; - static unsigned long read_at; - unsigned long changes = config_changes(); - if (!initialized || read_at != changes) - { - visible = config_boolean("display.viewmodel_visible"); - initialized = 1; - read_at = changes; - } - return visible; -} - -int viewmodel_draws_geometry(int first_person) -{ - return !first_person || viewmodel_is_visible(); -} diff --git a/port/linux/game/viewmodel_visibility.h b/port/linux/game/viewmodel_visibility.h deleted file mode 100644 index 40e08b6b..00000000 --- a/port/linux/game/viewmodel_visibility.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef VIEWMODEL_VISIBILITY_H -#define VIEWMODEL_VISIBILITY_H -/* Display only: weapon updates, sounds, gameplay and world lights continue. */ -int viewmodel_is_visible(void); -int viewmodel_draws_geometry(int first_person); -#endif diff --git a/port/linux/include/math.h b/port/linux/include/math.h index 84d84dc3..63f92edc 100644 --- a/port/linux/include/math.h +++ b/port/linux/include/math.h @@ -11,12 +11,6 @@ strnlen() cannot collide with the game's own), which hides glibc's M_*. #include_next -/* glibc hides the C99 classification macros in the game's ISO C89 mode. -Use the compiler's classification, not a call to a nonexistent libm symbol. */ -#ifndef isfinite -#define isfinite(value) __builtin_isfinite(value) -#endif - #ifndef M_PI #define M_E 2.71828182845904523536 #define M_LOG2E 1.44269504088896340736 diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index d4457cc3..83254e68 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -113,15 +113,16 @@ static const struct config_setting config_settings[] = "where the last tick left it: the view turns the frame the mouse moves,\n" "not up to two ticks (66 ms) later." }, { "display.fov", _config_real, "0.0", "HALO_FOV", _environment_value, _platform_all, - "On-foot first-person horizontal FOV at 16:9, in degrees (20-150);\n" - "0 keeps the authored view. Other cameras keep their own FOV." }, + "The first-person view's field of view on foot, in degrees across at\n" + "16:9 (20 to 150); 0 keeps the stock view. Vehicles, cinematics and\n" + "scripted cameras keep their own." }, { "display.viewmodel_fov", _config_real, "0.0", "HALO_VIEWMODEL_FOV", _environment_value, _platform_all, - "Weapon and hands horizontal FOV at 16:9, in degrees (20 to 150);\n" - "0 is Default and keeps the original weapon view. A chosen angle is\n" - "that FOV. Attached visuals use the same projection." }, - { "display.viewmodel_visible", _config_boolean, "true", "HALO_VIEWMODEL_VIS", _environment_value, _platform_all, - "Draw the first-person weapon, hands and attached visuals. Turning\n" - "this off does not change firing, animation, sound or world lights." }, + "The first-person weapon's and hands' field of view, in degrees across\n" + "at 16:9 (20 to 150); 0 keeps the weapon's stock view, also when\n" + "display.fov widens the world." }, + { "display.viewmodel_visible", _config_boolean, "true", "HALO_VIEWMODEL_VISIBLE", _environment_value, _platform_all, + "Draw the first-person weapon, hands and what is attached to them.\n" + "Off, they are not drawn; firing, animation, sound and lights go on." }, { "display.high_res_hud", _config_boolean, "true", "HALO_HIGH_RES_HUD", _environment_value, _platform_all, "Draw the HUD (meters, counters, panels, motion sensor, reticles,\n" "waypoints, scopes) from the high-res assets (8x the maps' bitmaps);\n" diff --git a/source/interface/first_person_weapons.c b/source/interface/first_person_weapons.c index de7df2fa..733f71cf 100644 --- a/source/interface/first_person_weapons.c +++ b/source/interface/first_person_weapons.c @@ -100,7 +100,6 @@ symbols in this file: #define REAL_MATH_EXTERNAL_REAL_LOCAL_RANDOM #define REAL_MATH_EXTERNAL_REAL_LOCAL_RANDOM_RANGE #include "interface/first_person_weapons.h" -#include "viewmodel_visibility.h" #include "cache/predicted_resources.h" #include "camera/director.h" @@ -125,6 +124,7 @@ symbols in this file: #include "sound/game_sound.h" #include "sound/sound_manager.h" #include "units/units.h" +#include "view_fov.h" /* port: port/linux/game/view_fov.c */ /* ---------- constants */ @@ -470,6 +470,7 @@ long first_person_weapon_get_local_index( void first_person_weapon_draw( void) { + /* port: not drawn with display.viewmodel_visible off (view_fov.c) */ if (!viewmodel_is_visible()) return; if (render.local_player_index!=NONE) diff --git a/source/interface/hud_draw.c b/source/interface/hud_draw.c index ff62d705..4e6436f2 100644 --- a/source/interface/hud_draw.c +++ b/source/interface/hud_draw.c @@ -95,7 +95,6 @@ symbols in this file: #include "game/players.h" #include "interface/hud_definitions.h" #include "interface/hud_draw.h" -#include "render_fov.h" #include "interface/interface.h" #include "interface/unit_hud_interface_definition.h" #include "items/weapon_definitions.h" @@ -107,6 +106,7 @@ symbols in this file: #include "units/units.h" #include "cache_file_formats.h" /* port: port/linux/game/cache_file_formats.c */ #include "custom_edition_cache.h" +#include "view_fov.h" /* port: port/linux/game/view_fov.c */ /* ---------- constants */ @@ -397,7 +397,7 @@ static void hud_draw_bitmap_internal( real_vector2d const *xy_scale, real theta, pixel32 color, - real reticle_scale); + real reticle_scale); /* port: (display.fov, view_fov.c) */ static void hud_draw_bitmap_with_meter( void *meter_parameters, struct bitmap_data const *bitmap, @@ -1592,8 +1592,9 @@ static void hud_draw_bitmap_internal( real center_y = (render.camera.window_bounds.y1 + render.camera.window_bounds.y0) / 2 - render.camera.viewport_bounds.y0; - /* Transform the complete authored reticle about its aiming anchor: - * offsets and separate sprites must shrink along with their artwork. */ + /* port: the reticle scaled about the view's centre, where it aims, + with display.fov (view_fov.c): its offsets and separate pieces + shrink with its picture */ for (vertex_index = 0; vertex_index < 4; vertex_index++) { vertices[vertex_index].position.x = center_x + @@ -1687,6 +1688,7 @@ static void hud_draw_bitmap_with_meter( &xy_scale, theta, color, + /* port: a reticle at the centre keeps to its aim (view_fov.c) */ is_crosshair_bitmap && absolute_placement->corner == _hud_anchor_center ? render_fov_reticle_scale(render.local_player_index) : 1.0f); diff --git a/source/interface/hud_weapon.c b/source/interface/hud_weapon.c index 058a4bfc..4462270e 100644 --- a/source/interface/hud_weapon.c +++ b/source/interface/hud_weapon.c @@ -1292,6 +1292,7 @@ static void crosshairs_draw( color, in_multiplayer, interface_bitmap, + /* port: not scaled with display.fov (view_fov.c) */ FALSE); } else @@ -1308,7 +1309,9 @@ static void crosshairs_draw( color, in_multiplayer, bitmap_group->type == _bitmap_group_type_interface_bitmaps, - /* Scope artwork keeps its authored layout. */ + /* port: a reticle, scaled with display.fov + (view_fov.c); a scope's picture keeps its + place */ state_index != _crosshair_state_zoom); } } diff --git a/source/main/main.c b/source/main/main.c index 3ea113d5..db4a3210 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -393,7 +393,7 @@ symbols in this file: #include "text/font_group.h" #include "tag_files/files.h" #include "custom_edition_cache.h" /* port: custom_edition_level_name */ -#include "render_fov.h" /* port: local world FOV, before projection and culling */ +#include "view_fov.h" /* port: display.fov, before the projection and culling */ #if defined(HALO_WINDOWS) || defined(HALO_ANDROID) || defined(__linux__) #define HALO_NATIVE_BUILD_INFO 1 @@ -1152,6 +1152,7 @@ void set_window_camera_values( window->rasterizer_camera.position = observer->position; window->rasterizer_camera.forward = observer->forward; window->rasterizer_camera.up = observer->up; + /* port: display.fov's view, the local render view only (view_fov.c) */ window->rasterizer_camera.vertical_field_of_view = render_fov_vertical(window->local_player_index, 2.0f * arctangent( 0.75f * render_camera_get_adjusted_field_of_view_tangent( diff --git a/source/rasterizer/rasterizer_lights.c b/source/rasterizer/rasterizer_lights.c index ef91f729..61662307 100644 --- a/source/rasterizer/rasterizer_lights.c +++ b/source/rasterizer/rasterizer_lights.c @@ -81,8 +81,6 @@ symbols in this file: /* ---------- headers */ #include "cseries.h" -#include "viewmodel_fov.h" -#include "viewmodel_visibility.h" #include "errors.h" #include "bitmaps/bitmaps.h" #include "interface/hud_draw.h" @@ -104,6 +102,7 @@ symbols in this file: #include #include "rasterizer/xbox/rasterizer_xbox.h" #include "main/main.h" +#include "view_fov.h" /* port: port/linux/game/view_fov.c */ /* ---------- constants */ @@ -548,7 +547,8 @@ void rasterizer_lens_flare_submit( 268, (parameters->compressed_window_index&_lens_flare_window_index_mask)==global_window_parameters.window_index); - if (!viewmodel_draws_geometry(parameters->compressed_window_index & _lens_flare_first_person_weapon_flag)) + /* port: not the first-person weapon's with it hidden (view_fov.c) */ + if (!viewmodel_draws_geometry((parameters->compressed_window_index & _lens_flare_first_person_weapon_flag) != 0)) return; if (rasterizer_debug_options.draw_lens_flares && !screenshot_in_progress() && global_window_parameters.rasterizer_target==_rasterizer_target_render_primary) @@ -568,12 +568,15 @@ void rasterizer_lens_flare_submit( screen): its occlusion test would count no pixels, so it would not be drawn, and a level of many lights (Halo PC's maps) tested hundreds a frame, each a draw of its own (the sphere takes in the - test's offset point and size, rasterizer_lens_flares_submit_occlusion_tests) */ + test's offset point and size, rasterizer_lens_flares_submit_occlusion_tests). + Not the first-person weapon's: it is drawn at the weapon's own + projection (display.viewmodel_fov, view_fov.c), not this frustum's */ if ((parameters->definition->far_fade_distance==0.0f || camera_distancedefinition->far_fade_distance) && (parameters->compressed_light_color&LENS_FLARE_LIGHT_COLOR_ALPHA_MASK)>0 && + ((parameters->compressed_window_index&_lens_flare_first_person_weapon_flag) || render_frustum_sphere_visible(&global_window_parameters.frustum, ¶meters->position, - 3.0f * parameters->definition->occlusion_radius)) + 3.0f * parameters->definition->occlusion_radius))) { struct rasterizer_lens_flare_submit_parameters *lens_flare_parameters= lens_flare_parameters_get((short)local_lens_flare_count++); @@ -861,11 +864,15 @@ void rasterizer_lens_flares_submit_occlusion_tests( if ((lens_flare_parameters->compressed_window_index & _lens_flare_window_index_mask) == global_window_parameters.window_index) { - boolean first_person = (lens_flare_parameters->compressed_window_index & _lens_flare_first_person_weapon_flag) != 0; - if (first_person) viewmodel_projection_begin(); + boolean first_person = (lens_flare_parameters->compressed_window_index & + _lens_flare_first_person_weapon_flag) != 0; real occlusion_radius = definition->occlusion_radius; real_point3d occlusion_point; + /* port: the first-person weapon's projection (view_fov.c) */ + if (first_person) + viewmodel_projection_begin(); + switch (definition->occlusion_offset_direction) { case _lens_flare_occlusion_offset_direction_toward_viewer: @@ -900,7 +907,8 @@ void rasterizer_lens_flares_submit_occlusion_tests( occlusion_radius, lens_flare_index); - if (first_person) viewmodel_projection_end(); + if (first_person) + viewmodel_projection_end(); } } @@ -935,10 +943,14 @@ void rasterizer_lens_flares_draw( if ((lens_flare_parameters->compressed_window_index & _lens_flare_window_index_mask) == global_window_parameters.window_index) { - boolean first_person = (lens_flare_parameters->compressed_window_index & _lens_flare_first_person_weapon_flag) != 0; - if (first_person) viewmodel_projection_begin(); + boolean first_person = (lens_flare_parameters->compressed_window_index & + _lens_flare_first_person_weapon_flag) != 0; struct lens_flare_definition *definition = lens_flare_parameters->definition; + /* port: the first-person weapon's projection (view_fov.c) */ + if (first_person) + viewmodel_projection_begin(); + if (lens_flare_parameters->internal__occlusion_pixels > 0 && LENS_FLARE_LIGHT_COLOR_ALPHA(lens_flare_parameters->compressed_light_color) > 0 && definition->reflections.count > 0) @@ -1180,7 +1192,8 @@ void rasterizer_lens_flares_draw( } } - if (first_person) viewmodel_projection_end(); + if (first_person) + viewmodel_projection_end(); } } diff --git a/source/rasterizer/rasterizer_transparent_geometry.c b/source/rasterizer/rasterizer_transparent_geometry.c index f5534842..f5154ee3 100644 --- a/source/rasterizer/rasterizer_transparent_geometry.c +++ b/source/rasterizer/rasterizer_transparent_geometry.c @@ -83,7 +83,6 @@ symbols in this file: /* ---------- headers */ #include "cseries.h" -#include "viewmodel_fov.h" #include "cseries/errors.h" #include "rasterizer.h" #include "rasterizer_console_vars.h" @@ -93,6 +92,7 @@ symbols in this file: #include "rasterizer/xbox/rasterizer_xbox_state.h" #include "shaders/shader_definitions.h" #include "shaders/shaders.h" +#include "view_fov.h" /* port: port/linux/game/view_fov.c */ /* ---------- constants */ @@ -587,6 +587,7 @@ void rasterizer_transparent_geometry_draw( global_window_parameters.rasterizer_target==_rasterizer_target_render_primary); if (!first_person_flag) { + /* port: the first-person weapon's projection (view_fov.c) */ viewmodel_projection_begin(); rasterizer_set_stencil_mode(0); rasterizer_set_frustum_z( @@ -615,7 +616,7 @@ void rasterizer_transparent_geometry_draw( rasterizer_debug_options.transparent_pixel_counter_active = FALSE; if (first_person_flag) { - viewmodel_projection_end(); + viewmodel_projection_end(); /* port: (view_fov.c) */ rasterizer_set_frustum_z(0.0f, 0.0f); } } diff --git a/source/rasterizer/xbox/rasterizer_xbox_models.c b/source/rasterizer/xbox/rasterizer_xbox_models.c index e2135da0..f205c9f8 100644 --- a/source/rasterizer/xbox/rasterizer_xbox_models.c +++ b/source/rasterizer/xbox/rasterizer_xbox_models.c @@ -111,7 +111,6 @@ symbols in this file: #include "rasterizer/rasterizer_models.h" #include "rasterizer/rasterizer_transparent_geometry.h" #include "render/render.h" -#include "viewmodel_fov.h" #include "shaders/shader_definitions.h" #include "shaders/shaders.h" @@ -124,6 +123,7 @@ symbols in this file: #include "rasterizer/xbox/rasterizer_xbox_draw_primitives.h" #include "rasterizer/xbox/rasterizer_xbox_internal.h" #include "rasterizer/xbox/rasterizer_xbox_models.h" +#include "view_fov.h" /* port: port/linux/game/view_fov.c */ /* ---------- constants */ @@ -616,7 +616,7 @@ void _rasterizer_model_end( !local_do_not_change_z_stencil_states) { rasterizer_set_stencil_mode(RASTERIZER_STENCIL_MODE_REJECT); - viewmodel_projection_end(); + viewmodel_projection_end(); /* port: (view_fov.c) */ rasterizer_set_frustum_z(0.0f, 0.0f); } local_parameters = NULL; @@ -645,6 +645,7 @@ void _rasterizer_model_begin( !do_not_change_z_stencil_states) { rasterizer_set_stencil_mode(RASTERIZER_STENCIL_MODE_WRITE); + /* port: the first-person weapon's projection (view_fov.c) */ viewmodel_projection_begin(); rasterizer_set_frustum_z( rasterizer_globals.first_person_weapon_near_clip_distance, diff --git a/source/render/render_particles.c b/source/render/render_particles.c index 9d71f4e2..cfce7009 100644 --- a/source/render/render_particles.c +++ b/source/render/render_particles.c @@ -22,7 +22,6 @@ symbols in this file: #include "effects/particles.h" #include "game/players.h" #include "interface/first_person_weapons.h" -#include "viewmodel_visibility.h" #include "render.h" #include "render_cameras_internal.h" #include "render_particles.h" @@ -30,6 +29,7 @@ symbols in this file: #include "tag_files/tag_groups.h" #include "units/unit_definitions.h" #include "units/units.h" +#include "view_fov.h" /* port: port/linux/game/view_fov.c */ /* ---------- constants */ @@ -160,6 +160,7 @@ void render_particles( boolean owned_by_local_player = particle->local_player_index == local_player_index; + /* port: not the first-person weapon's with it hidden (view_fov.c) */ if (render_location_visible(&particle->location) && viewmodel_draws_geometry(owned_by_local_player && TEST_FLAG(particle->flags, _particle_datum_dont_draw_third_person_bit)) && diff --git a/source/render/render_sprite.c b/source/render/render_sprite.c index f3c31a95..a622cb63 100644 --- a/source/render/render_sprite.c +++ b/source/render/render_sprite.c @@ -76,7 +76,6 @@ symbols in this file: /* ---------- headers */ #include "cseries/cseries.h" -#include "viewmodel_fov.h" #include "bitmaps/bitmap_group.h" #include "cseries/errors.h" #include "interface/hud_draw.h" @@ -91,6 +90,7 @@ symbols in this file: #include "tag_files/tag_files.h" #include "tag_files/tag_groups.h" #include "custom_edition_cache.h" /* port: port/linux/game/custom_edition_cache.c */ +#include "view_fov.h" /* port: port/linux/game/view_fov.c */ /* ---------- constants */ @@ -215,6 +215,7 @@ void build_sprites_begin( !TEST_FLAG(flags, _build_sprites_valid_bit)); data->bitmap_group_index = bitmap_group_index; + /* port: the first-person weapon's projection (view_fov.c) */ if (TEST_FLAG(flags, _build_sprites_first_person_bit)) viewmodel_projection_begin(); data->flags = flags; @@ -286,6 +287,7 @@ void build_sprites_end( } SET_FLAG(data->flags, _build_sprites_valid_bit, FALSE); + /* port: (view_fov.c) */ if (TEST_FLAG(data->flags, _build_sprites_first_person_bit)) viewmodel_projection_end(); return; diff --git a/tools/port_settings.py b/tools/port_settings.py index 0fb941f9..944783f6 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -191,15 +191,15 @@ _video["rows"] = [row for row in _video["rows"] if row[1] not in _graphics] _video["categories"] = [ ("GRAPHICS:", "video_settings/graphics", "The HUD, text, anti-aliasing, shadows and\nlighting."), ("FOV AND VIEWMODELS:", "video_settings/fov_viewmodels", - "Optional field of view and first-person weapon\ndisplay settings. Defaults keep the original view."), + "The field of view and the first-person\nweapon. Their defaults keep the stock view."), ] SCREENS["video_settings/fov_viewmodels"] = { "screen": "fov_viewmodel_settings_screen", "header": _video["header"], "spacing": 30, "rows": [ ("FOV:", "display.fov", [("DEFAULT", "0")] + [(str(n), str(n)) for n in range(80, 151, 5)], - "On-foot horizontal FOV at 16:9, in degrees.\nDefault keeps the authored view and scopes.", None), + "On foot, horizontal at 16:9, in degrees.\nDefault keeps the stock view.", None), ("VIEWMODEL FOV:", "display.viewmodel_fov", [("DEFAULT", "0")] + [(str(n), str(n)) for n in range(80, 151, 5)], - "Weapon and hands horizontal FOV at 16:9, in degrees.\nA number sets that FOV. Default keeps the original weapon view.", None), + "The weapon and hands, horizontal at 16:9.\nDefault keeps the weapon's stock view.", None), ("VIEWMODELS:", "display.viewmodel_visible", ON_OFF, "Draw first-person weapons, hands and attached\nvisuals. Gameplay and sound continue when off.", None), ], From 085512fe41b1c02c22924918f61e055e51d6398e Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:40:43 +1300 Subject: [PATCH 181/187] Profiling build: built in CI, its header with the port's, port marks (#223) CI builds the profiling flavour too (tools/ci_build.py linux profile: a debug build with configure.py --profile), so a change that breaks it is seen; nothing else compiles it. profile_sections.h, which only the port's builds use, moves from source/cseries to port/linux/include, and the hooks in the game's sources (the map load, the console, the main loop's exit) and their includes say they are the port's. The build scripts' import lines are wrapped as the files' others. --- .github/workflows/build.yml | 5 +++++ port/linux/game/network_distributed.c | 2 +- port/linux/game/network_objects.c | 2 +- .../linux/include}/profile_sections.h | 4 +++- source/game/game.c | 7 ++++--- source/game/game_time.c | 2 +- source/hs/hs.c | 2 +- source/main/console.c | 1 + source/main/main.c | 5 +++-- tools/android_build.py | 4 ++-- tools/ci_build.py | 13 +++++++++---- tools/windows_build.py | 6 +++--- 12 files changed, 34 insertions(+), 19 deletions(-) rename {source/cseries => port/linux/include}/profile_sections.h (82%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff1ac56b..392a5e08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,6 +73,11 @@ jobs: - name: Debug run: python tools/ci_build.py linux debug + # (the profiling build, configure.py --profile, which no other step + # compiles) + - name: Profile + run: python tools/ci_build.py linux profile + - name: Release run: python tools/ci_build.py linux release diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index a7cdc4f2..46126a41 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -73,7 +73,7 @@ machine (their datum identifiers need not be). #include "network_coop.h" #include "network_distributed.h" #include "network_voice.h" -#include "cseries/profile_sections.h" +#include "profile_sections.h" #include #include diff --git a/port/linux/game/network_objects.c b/port/linux/game/network_objects.c index 557202d3..64273967 100644 --- a/port/linux/game/network_objects.c +++ b/port/linux/game/network_objects.c @@ -64,7 +64,7 @@ same datum index (identifier and all), so that any message can name one: #include "cutscene/cinematics.h" #include "network_coop.h" #include "network_distributed.h" -#include "cseries/profile_sections.h" +#include "profile_sections.h" #include diff --git a/source/cseries/profile_sections.h b/port/linux/include/profile_sections.h similarity index 82% rename from source/cseries/profile_sections.h rename to port/linux/include/profile_sections.h index 627017c7..5184b872 100644 --- a/source/cseries/profile_sections.h +++ b/port/linux/include/profile_sections.h @@ -1,7 +1,9 @@ /* PROFILE_SECTIONS.H -Wrap game CPU sections without changing normal builds. +The profiling build's CPU sections around the game's steps (configure.py +--profile, port/linux/src/profile_trace.c): in a normal build each wraps +its statement and leaves nothing else. */ #ifndef __PROFILE_SECTIONS_H diff --git a/source/game/game.c b/source/game/game.c index 7c6cfdc3..21407601 100644 --- a/source/game/game.c +++ b/source/game/game.c @@ -183,9 +183,9 @@ struct game_options; #include "units/vehicles.h" #include "network_coop.h" /* port: port/linux/game/network_coop.c */ #ifdef HALO_PROFILE -#include "profile_console.h" +#include "profile_console.h" /* port: port/linux/game/profile_console.c */ #endif -#include "cseries/profile_sections.h" +#include "profile_sections.h" /* port: port/linux/include/profile_sections.h */ /* network_game_globals.c's */ boolean network_game_distributed_client(void); @@ -609,7 +609,8 @@ boolean game_load( struct game_options *options) { #ifdef HALO_PROFILE - /* (every map load passes here: main_new_map's and a network game's, + /* port: the profiling build's recording learns of the map (every map + load passes here: main_new_map's and a network game's, network_game_manager.c) */ profile_console_map_loaded(options->map_name); #endif diff --git a/source/game/game_time.c b/source/game/game_time.c index 47fcbf6e..5de8ef02 100644 --- a/source/game/game_time.c +++ b/source/game/game_time.c @@ -75,7 +75,7 @@ symbols in this file: #include "networking/network_game_globals.h" #include "networking/network_server_manager.h" #include "saved games/game_state.h" -#include "cseries/profile_sections.h" +#include "profile_sections.h" /* port: port/linux/include/profile_sections.h */ /* port/linux/game/network_distributed.c's */ void network_distributed_tick(void); diff --git a/source/hs/hs.c b/source/hs/hs.c index 6e848ebe..213fd10d 100644 --- a/source/hs/hs.c +++ b/source/hs/hs.c @@ -2842,7 +2842,7 @@ symbols in this file: #include "units/vehicles.h" #include "coop_scripts.h" /* port: port/linux/game/coop_scripts.c */ #ifdef HALO_PROFILE -#include "profile_console.h" +#include "profile_console.h" /* port: port/linux/game/profile_console.c */ #endif /* ---------- constants */ diff --git a/source/main/console.c b/source/main/console.c index 737ce97c..8db63a2d 100644 --- a/source/main/console.c +++ b/source/main/console.c @@ -130,6 +130,7 @@ void console_open( { console_globals.input_state.result[0] = '\0'; console_globals.active = terminal_gets_begin(&console_globals.input_state); + /* port: not in the profiling build (as in console_update) */ #ifndef HALO_PROFILE profile_global_enable = FALSE; #endif diff --git a/source/main/main.c b/source/main/main.c index 1b9ceef9..2189b49a 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -381,9 +381,9 @@ symbols in this file: #include "networking/network_server_manager.h" /* port: a co-op game's level won */ #include "network_coop.h" /* port: port/linux/game/network_coop.c */ #include "network_voice.h" /* port: port/linux/game/network_voice.c */ -#include "cseries/profile_sections.h" +#include "profile_sections.h" /* port: port/linux/include/profile_sections.h */ #ifdef HALO_PROFILE -#include "profile_trace.h" +#include "profile_trace.h" /* port: port/linux/src/profile_trace.c */ #endif #include "camera/director.h" #include "camera/observer.h" @@ -2382,6 +2382,7 @@ static void main_exit( void) { #ifdef HALO_PROFILE + /* port: the profiling build's recording written out before the game goes */ profile_trace_shutdown(); #endif switch (main_globals.connection) diff --git a/tools/android_build.py b/tools/android_build.py index b148e1f9..7c73739f 100644 --- a/tools/android_build.py +++ b/tools/android_build.py @@ -26,8 +26,8 @@ from pathlib import Path from typing import Any, Dict, List, Optional from .linux_build import (LINUX_PROFILE, MINIUPNPC_DEFINES, MINIUPNPC_DIR, MUSL_MATH_DIR, XDK_INCLUDE, - compile_launcher, configuration_defines, game_defines_and_includes, game_sources, miniupnpc_sources, - musl_math_sources, opus_cflags, opus_sources, pgo_mode, pgo_profile, + compile_launcher, configuration_defines, game_defines_and_includes, game_sources, + miniupnpc_sources, musl_math_sources, opus_cflags, opus_sources, pgo_mode, pgo_profile, profile_use_flags, updater_defines, xdk_headers) from .embed_assets import hud_assets_build, hud_configure_inputs from .ninja_syntax import Writer diff --git a/tools/ci_build.py b/tools/ci_build.py index fa130aab..56a749f0 100644 --- a/tools/ci_build.py +++ b/tools/ci_build.py @@ -4,12 +4,14 @@ python tools/ci_build.py linux debug python tools/ci_build.py android release + python tools/ci_build.py linux profile Builds are portable (any x86-64 processor), so they run on other computers. Debug builds skip link-time and profile-guided optimisation, which only make the build slower; release builds use both, as a local release build does (profile-guided optimisation needs clang 22 or later, -and is skipped with an older one). CI_COMPILER_LAUNCHER (ccache, say) is +and is skipped with an older one). A profile build is a debug build with +configure.py --profile, so that the profiling build is built too. CI_COMPILER_LAUNCHER (ccache, say) is passed on as --compiler-launcher. A build of the main branch gets the run's number (HALO_BUILD_NUMBER), which its release is named after and the self-updater compares. @@ -46,7 +48,7 @@ def run(command, cwd=ROOT): def main() -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("platform", choices=sorted(OUTPUTS)) - parser.add_argument("config", choices=["debug", "release"]) + parser.add_argument("config", choices=["debug", "release", "profile"]) args = parser.parse_args() configure = [sys.executable, "configure.py", "--portable"] @@ -54,6 +56,8 @@ def main() -> int: configure.append("--release") else: configure += ["--lto=off", "--pgo=off"] + if args.config == "profile": + configure.append("--profile") launcher = os.environ.get("CI_COMPILER_LAUNCHER") if launcher: configure += ["--compiler-launcher", launcher] @@ -70,8 +74,9 @@ def main() -> int: # same name: release is signed with the debug key, not debuggable) run(["ninja", "android"]) gradlew = "gradlew.bat" if os.name == "nt" else "./gradlew" - run([gradlew, "--console=plain", "-q", f"assemble{args.config.capitalize()}"], cwd=ROOT / "port/android") - outputs = [APKS[args.config]] + variant = "release" if args.config == "release" else "debug" + run([gradlew, "--console=plain", "-q", f"assemble{variant.capitalize()}"], cwd=ROOT / "port/android") + outputs = [APKS[variant]] else: run(["ninja", args.platform]) outputs = OUTPUTS[args.platform] diff --git a/tools/windows_build.py b/tools/windows_build.py index 57441a3b..d0818a71 100644 --- a/tools/windows_build.py +++ b/tools/windows_build.py @@ -19,9 +19,9 @@ from pathlib import Path from typing import Any, Dict, List, Optional from .linux_build import (LINUX_PROFILE, MINIUPNPC_DIR, OPTIMISATION, WINDOWS_PROFILE, XDK_INCLUDE, lto_mode, - march_flag, miniupnpc_sources, pgo_mode, compile_launcher, configuration_defines, game_defines_and_includes, - game_sources, musl_math_cflags, musl_math_sources, opus_cflags, opus_sources, pgo_profile, - profile_use_flags, xdk_headers) + march_flag, miniupnpc_sources, pgo_mode, compile_launcher, configuration_defines, + game_defines_and_includes, game_sources, musl_math_cflags, musl_math_sources, opus_cflags, + opus_sources, pgo_profile, profile_use_flags, xdk_headers) from .embed_assets import hud_assets_build, hud_configure_inputs from .ninja_syntax import Writer From c44930abbe019efacad70672495b2caecee56b58 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:13:14 +1300 Subject: [PATCH 182/187] Single-player SETTINGS: placed by REVERT TO SAVED's tag, main's centring kept (#67) SETTINGS goes before REVERT TO SAVED found by its tag (restart_at_save_point_button: it opens its confirmation, so no function names it), not at the second row, so that a map whose list has it elsewhere gets it in its place, and one that has it not is left alone. The rows are spaced over their own span only when the list grew for it, and the list then keeps its height; a list with room keeps the centring pause_list_patch now gives it. Gamepads' OK no longer copies the changed bytes into the player's profile itself: saving the profile makes it the player's (#227). The edited profile's index has its comment, and the README says what the list does. --- port/linux/README.md | 9 ++++---- port/linux/game/menu_functions.c | 38 ++++---------------------------- port/linux/game/menu_tags.c | 33 ++++++++++++++++++++------- source/interface/player_ui.c | 2 +- 4 files changed, 35 insertions(+), 47 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index 96c4800e..6abf475f 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -291,10 +291,11 @@ stay. The Xbox's pause box is drawn taller to hold them (a redraw, what is below its list moves down. The few pictures of the settings that come from the main menu's map are not drawn there. -In a single-player campaign, the pause menu has SETTINGS too, after RESUME -GAME, its rows closer in the same box. It opens the same settings, while the -game stays paused, with only Controls, Gamepads, Mouse, Audio and Video -Setup; Gamepads' OK saves the profile at once. +In a single-player campaign, the pause menu has SETTINGS too, before REVERT +TO SAVED, in the same box: a list with room for it centres its rows, and +one without keeps its size, its rows closer. It opens the same settings, +while the game stays paused, with only Controls, Gamepads, Mouse, Audio and +Video Setup; Gamepads' OK saves the profile at once. The menus are XML files in `port/assets/menus` (`tools/ce_menus.py` writes them from the PC version's tags), which the game contains. To change them, diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 63d6d82d..7f9cb55e 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -3952,53 +3952,23 @@ static boolean profile_save_changes(struct widget_instance *widget, boolean *wid return TRUE; } -/* only what changed from before to after, into the local player's active -profile, which the campaign saves its progress from */ -static void active_profile_take_changes(short local_player, long index, struct player_profile const *before, - struct player_profile const *after) -{ - struct player_profile active; - byte const *from = (byte const *)before; - byte const *to = (byte const *)after; - byte *changed = (byte *)&active; - long offset; - - player_ui_get_active_player_profile(local_player, &active); - for (offset = 0; offset < (long)sizeof(active); offset++) - { - if (to[offset] != from[offset]) - changed[offset] = to[offset]; - } - player_ui_set_active_player_profile(local_player, index, &active); -} - /* "port profile settings save" (Gamepads' OK in a single-player campaign: menu_tags.c's pause_settings_patch): the profile saved at once, not on -Settings' OK, its changes made the player's own, and edited again from what -was saved, for Settings to go on with */ +Settings' OK, so that the campaign's next save of the player's profile +keeps it; saving makes it the player's own (player_ui_save_profile), and it +is edited again from what was saved, for Settings to go on with */ static boolean profile_settings_save(struct widget_instance *widget) { - struct player_profile *edited = player_ui_get_edit_player_profile(); long index = player_ui_get_edit_profile_index(); - struct player_profile before, after; - short local_player; - if (edited) - before = *edited; settings_each(screen_of(widget), setting_changed_save); - if (!edited || !player_ui_edit_profile_is_dirty()) + if (!player_ui_get_edit_player_profile() || !player_ui_edit_profile_is_dirty()) return TRUE; - after = *edited; if (!player_ui_save_profile()) { platform_log("menus: could not save the profile's changes"); return campaign_fail(); } - for (local_player = 0; local_player < MAXIMUM_LOCAL_PLAYERS; local_player++) - { - if (player_ui_get_active_player_profile_index(local_player) == index) - active_profile_take_changes(local_player, index, &before, &after); - } player_ui_begin_editing_profile(index); return TRUE; } diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index 2d27db79..5a1e5a7b 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -1748,9 +1748,10 @@ static void pause_patch(struct cache_file_tag_instance *instances) /* ---------- the single-player campaign's pause menu -Its list gets the same SETTINGS (pause_list_patch) after RESUME GAME, a copy -of REVERT TO SAVED; its box, beside the mission objectives', keeps its size, -the rows closer. Settings there has only the items that work in a game +Its list gets the same SETTINGS (pause_list_patch) before REVERT TO SAVED, a +copy of it; a list with room centres its rows (Halo PC's), and one that has +none keeps its size, its rows closer, as its box beside the mission +objectives' does. Settings there has only the items that work in a game (pause_settings_patch), and Gamepads' OK saves the profile itself. */ #define PAUSE_SETTINGS_LIST "main_menu/settings_select/player_setup/player_profile_edit/profile_edit_select_list" @@ -1766,7 +1767,8 @@ static char const *const pause_settings_hidden_items[] = "main_menu/settings_select/player_setup/player_profile_edit/about_item", }; -/* the list's rows spaced evenly over the span its rows had */ +/* the list's rows spaced evenly over the span its rows had (a list that grew +for SETTINGS, back to its own size) */ static void pause_list_fit(struct ui_widget_definition *list, short span) { struct ui_widget_child_reference *children = list->child_widgets.address; @@ -1859,7 +1861,8 @@ static void pause_campaign_patch(struct cache_file_tag_instance *instances) { struct ui_widget_definition *list; struct ui_widget_child_reference const *rows; - short span, grow; + short span, grow, bottom; + long revert; if (children[child].widget_tag.index == NONE) continue; @@ -1867,10 +1870,24 @@ static void pause_campaign_patch(struct cache_file_tag_instance *instances) if (list->type != _widget_type_column_list || list->child_widgets.count < 2) continue; rows = list->child_widgets.address; - span = (short)(rows[list->child_widgets.count - 1].vertical_offset - rows[0].vertical_offset); - if (pause_list_patch(instances, list, 1, FALSE, &grow)) + /* (REVERT TO SAVED, by its tag: it opens its confirmation rather + than running a function, and a map's list need not have it second) */ + for (revert = 0; revert < list->child_widgets.count; revert++) { - pause_list_fit(list, span); + if (tag_name_ends(rows[revert].widget_tag.index, "\\restart_at_save_point_button")) + break; + } + if (revert == list->child_widgets.count) + return; + span = (short)(rows[list->child_widgets.count - 1].vertical_offset - rows[0].vertical_offset); + bottom = list->bounds.y1; + if (pause_list_patch(instances, list, revert, FALSE, &grow)) + { + if (grow) + { + pause_list_fit(list, span); + list->bounds.y1 = bottom; + } pause_settings_patch(); platform_log("menus: the pause menu has SETTINGS"); } diff --git a/source/interface/player_ui.c b/source/interface/player_ui.c index b7aefcc1..76b7d158 100644 --- a/source/interface/player_ui.c +++ b/source/interface/player_ui.c @@ -367,7 +367,7 @@ struct player_profile *player_ui_get_edit_player_profile( return result; } -/* port */ +/* port: the saved game file being edited (NONE for none) */ long player_ui_get_edit_profile_index( void) { From 231708d2e0ea651687373925cf434d520f45c877 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:33:53 +1300 Subject: [PATCH 183/187] CI's profiling build only when a run asks for it (#223) Every push built the profiling flavour as a build of its own, which costs runner minutes for a check few changes need. It now builds only in a run started by hand (workflow_dispatch) with its new profile box ticked; tools/ci_build.py linux profile still builds it locally. --- .github/workflows/build.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 392a5e08..52ad1b6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,11 @@ on: paths-ignore: - "**.md" workflow_dispatch: + inputs: + profile: + description: "Also build the profiling flavour (configure.py --profile) on Linux" + type: boolean + default: false concurrency: group: build-${{ github.ref }} @@ -74,8 +79,10 @@ jobs: run: python tools/ci_build.py linux debug # (the profiling build, configure.py --profile, which no other step - # compiles) + # compiles: only when a run started by hand asks for it, as it costs a + # build of its own) - name: Profile + if: github.event_name == 'workflow_dispatch' && inputs.profile run: python tools/ci_build.py linux profile - name: Release From bd98c8fb20c1638bb66e22c94f61b739dcb45033 Mon Sep 17 00:00:00 2001 From: MrBruh <4706866+cybersecurity@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:00:38 +1300 Subject: [PATCH 184/187] AGENTS.md, and how the menus are written again AGENTS.md is a guide for coding agents working in the repository: where things are, how to build and test each port, the defines (and that HALO_LINUX and HALO_CUSTOM_EDITION are never set), the rules for the game's sources, the SDK declarations, the netcode's version, the menus, art and credit, Custom Edition maps, settings, vendored code and licences, and how commits and pull requests are made, with an index of the other documents. port/assets/menus/README.md says how tools/ce_menus.py writes the menu files again, which it points to. --- AGENTS.md | 209 ++++++++++++++++++++++++++++++++++++ port/assets/menus/README.md | 32 ++++++ 2 files changed, 241 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..d7078466 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,209 @@ +# Notes for agents + +This repository ports the Halo: Combat Evolved decompilation (Xbox build +01.01.14.2342, `cachebeta.exe`) to Linux, Windows and Android. The game's C +sources are the decompilation; the port adds a platform layer that +implements the Xbox APIs the game calls, and features the Xbox game did not +have (a new netcode, internet play, the PC version's menus, Custom Edition +maps, touch controls). The project is published as OpenCE. + +Start with [README.md](README.md). The other documents are listed at the +end. + +## Layout + +| Path | Contents | +| --- | --- | +| `source/` | The game: 466 C files of the decompilation, compiled as they are with few changes (each marked, below) | +| `port/linux/src/` | The platform layer the three ports share: Direct3D 8 on OpenGL (`d3d8_gl.c`), DirectSound and XInput on SDL3, files, threads, memory, sockets, settings (`port_config.c`), internet play (`p2p*.c`) | +| `port/linux/game/` | The port's game-side code, compiled as the game's sources are: the distributed netcode (`network_*.c`), co-op, the menus (`menu_tags.c`, `menu_functions.c`), Custom Edition maps (`custom_edition_*.c`, `cache_file_formats.c`), the tag validator (`tag_validate.c`, `tag_schema_*.c`), touch, FOV and more | +| `port/linux/include/` | The port's headers: the limits (`halo_port_limits.h`, with `HALO_PORT_NETWORK_VERSION`), capacities (`halo_port_capacity.h`), the prefix header, `halo_linux_source_fixups.h` (port functions the game's sources call) | +| `port/include/xdk/` | Stand-ins for the Xbox SDK's headers (declarations only) | +| `port/windows/` | The Windows build's own files (`win32_*.c`, headers) | +| `port/android/` | The Android app: the guest runtime, the host library (`host/`), the Java app (`app/`), host imports (`host_imports.list`) | +| `port/assets/` | What the builds embed or ship: high-res HUD (`hud/`), fonts, titles, menus (`menus/`), icons, network brokers | +| `port/third_party/` | Vendored libraries, each with a README naming its upstream, version and checksum | +| `port/tools/` | Stand-alone tools (`cache_file_report.c`) | +| `tools/` | Build scripts (`linux_build.py`, `windows_build.py`, `android_build.py`, `ci_build.py`), generators (`ce_menus.py`, `port_settings.py`, `hud_assets.py`, `title_assets.py`, `embed_assets.py`, `xdk_headers.py`) and tests | +| `docs/` | Longer design notes (`custom_edition_caches.md`) | +| `pgo/` | Profile-guided optimisation profiles | +| `assets/` | Game data for local runs (gitignored: `maps/`, `custom_maps/`) | +| `.github/workflows/` | CI: builds every platform on each push, publishes releases from `main` | + +## Building + +```sh +python configure.py # debug build; add --release, --portable, --profile, --pgo=off, --lto=off +ninja linux # build/linux/halo +ninja android # the Android guest image and native libraries +ninja android_apk # the APK (Gradle) +ninja windows # on Windows only +python tools/ci_build.py linux release # what CI builds +``` + +- `configure.py` writes `build.ninja`; `configure_args` at its top records + the options it was run with. Restore them after trying other options. +- The Linux and Windows builds are 32-bit x86 with clang, in MSVC's ABI + (`-fms-extensions -fshort-wchar -malign-double -fcommon`), C89 (`gnu89`): + declarations come before statements. +- Every native build compiles without fused multiply-add and with the + vendored musl maths (`halo_math.h`), so that every machine of a network + game computes the same results. Do not call the system's maths functions. +- Android: the game is an ILP32 AArch64 guest (`arm64_32`) in a 64-bit app. + Guest code can call only the host imports declared in + `port/android/host_imports.list` and `port/android/guest/runtime/guest_host.h`, + and only the SDL functions `guest_sdl.c` bridges. Run `ninja android` + after changing anything it compiles: `port/linux/src`, `port/linux/game`, + `port/android`, `source/`, the build scripts. + +### Defines + +| Define | Set by | Meaning | +| --- | --- | --- | +| `HALO_ANDROID` | Android | The app: its display, input, files and lifecycle | +| `HALO_GLES` | Android | The OpenGL ES renderer | +| `HALO_ARM64_GUEST` | Android | The guest's ABI (ILP32 AArch64) | +| `HALO_WINDOWS` | Windows (`halo_windows_prefix.h`) | The Windows build | +| `HALO_RELEASE` | `--release` | No assertions checked | +| `HALO_PROFILE` | `--profile` | The profiling build; a normal build must be unchanged by it | + +`HALO_LINUX` and `HALO_CUSTOM_EDITION` are **not** defined by any build: +code guarded by them compiles to nothing. Custom Edition support is a +setting (`game.custom_edition`), checked at run time with +`custom_edition_cache_tags_loaded()`. + +## Testing + +```sh +python -m pytest -q tools/harness tools/test_touch_menu.py # the engine's functions in a fake world +python -m pytest -q tools/test_linux_port.py # the build, the maps' checks (needs assets/maps) +python -m pytest -q tools/test_cache_file_formats.py tools/test_bmp_files.py tools/test_profile.py +python tools/test_touch_layout.py # the Android touch layout (JDK 17+) +python tools/test_light_storage.py +python tools/test_death_timing.py +``` + +`tools/harness/README.md` explains how to add an asset-free test: the code +under test is taken from the sources, not copied. + +Running the game without a person at it: + +- `debug.network_test` (`HALO_NETWORK_TEST=host:` or `join`) hosts or + joins a game without the menus and logs each player's state every second; + several copies play on one machine on different loopback addresses + (`HALO_NET_ADDRESS`, `HALO_NET_BROADCAST`). `NETCODE.md` describes the + test settings. +- `debug.exit_after`, `debug.hidden_window`, `debug.null_renderer`, + `debug.menu_open` (start on one menu screen), `debug.telnet_console`. +- Use a virtual display (Xvfb, with `SDL_VIDEO_DRIVER=x11`) for windowed + runs, never the desktop of the person you work for, and stop only the + processes you started. + +## Rules of the project + +### The game's sources + +- Change `source/` as little as possible, and mark each change from the + original with a comment that starts `port:`. Write like the surrounding + code: its naming, its comment density, and the project's plain, + descriptive comment style. +- A port function the game's sources call is declared in + `halo_linux_source_fixups.h` or included with a note naming its file + (`#include "view_fov.h" /* port: port/linux/game/view_fov.c */`). +- Notes such as `BUG (original, preserved for exact matching)` mark the + original's bugs. Fixing one is allowed; say so in a `port:` comment. + +### The Xbox SDK + +`port/include/xdk` was written without the SDK's headers, from the January +build's debug information and public documentation. Never read the SDK's +headers or anything derived from them, never copy from them, and do not +read `xbox/include`. To add a name, follow `port/include/xdk/README.md`. + +### Networking + +- Raise `HALO_PORT_NETWORK_VERSION` (`port/linux/include/halo_port_limits.h`) + with any change to what the machines send each other, or to anything they + must agree on, and say why in the commit. Machines of different versions + do not play together. +- Only the host decides; clients predict their own players. Read + `port/linux/NETCODE.md` before changing the netcode. +- Anything another machine sends is untrusted: check sizes, indices and + rates, as the existing handlers do. `debug.network_corrupt` tests this. + +### Menus + +- The menus must match Halo Custom Edition's. Their XML (`port/assets/menus/ce/`) + is generated by `tools/ce_menus.py` from the PC version's tags; the + settings screens come from `tools/port_settings.py`. Do not edit the XML + by hand: change the generator and run it (the command is in + `port/assets/menus/README.md`, "Writing them again"), then check that + `git status` shows only the screens meant to change. +- Functions the menus call but the port has not wired are listed in + `port/assets/menus/UNWIRED.md`. + +### Art and credit + +- Replacement HUD, title and menu art is made of hand-drawn redraws. +- Prefer an existing redraw (the ui-svg-handmade set), then a new redraw. A + Custom Edition picture that cannot reasonably be redrawn (a 3D render, a + screenshot, a logo) may be used only if it is listed in + `port/assets/menus/NON_HANDDRAWN.md`. +- `AUTHORS.md` credits art only. +- Fonts: Overpass and OpenCE (Newtown, respaced) only; see + `port/assets/fonts/README.md`. Never ship the maps' commercial typefaces. + +### Custom Edition maps + +Custom Edition maps live in the data root's `custom_maps` folder with +`bitmaps.map`, `sounds.map` and `loc.map`; their level names are +`custom_maps\`. OpenSauce features are not supported and must not be +added: a map that needs them is refused, and one that only carries +OpenSauce's header runs as stock Custom Edition runs it. Map files are +untrusted input: read them through bounds-checked readers. See +`docs/custom_edition_caches.md`. + +### Settings + +A setting is an entry in `port/linux/src/port_config.c`, with a default, an +environment variable and a comment that `config.toml` gets. Document it in +the settings table of `port/linux/README.md` (and the Android README for an +Android-only one). A departure from the stock game's look or behaviour +should be a setting, off or stock by default, unless the project decides +otherwise. + +### Third-party code and licences + +The repository is CC0. Vendored code goes in `port/third_party/`, +copied unchanged, with a README naming the upstream, the version or commit, +the checksum and what uses it. GPL code (OpenSauce, Reclaimer) may be read +as documentation of file formats, never copied. + +### Commits and pull requests + +- Commit subjects are plain sentences saying what changed; the body says + why and what was checked. +- A pull request is merged with a merge commit (`Merge pull request #N from + /`), and follow-up changes go in separate commits whose + subject ends with `(#N)`. +- A commit taken from a fork keeps its author and is made with + `git cherry-pick -x`. + +## Documents + +| Document | About | +| --- | --- | +| [README.md](README.md) | Downloads, game data, building, build options, profiling builds | +| [port/linux/README.md](port/linux/README.md) | The Linux build, controls, menus, every setting, updates, frame rate, field of view, system link, internet play, voice chat, the server browser, security, map checks, how the port operates, the game source changes | +| [port/linux/NETCODE.md](port/linux/NETCODE.md) | The distributed netcode, network versions, joining a game in progress, transport, testing | +| [port/windows/README.md](port/windows/README.md) | The Windows build, its headers, inline functions, crash reports | +| [port/android/README.md](port/android/README.md) | The Android build, the guest and host, touch controls, Android settings, finding problems | +| [port/include/xdk/README.md](port/include/xdk/README.md) | How the SDK declarations were written and are checked | +| [docs/custom_edition_caches.md](docs/custom_edition_caches.md) | Loading and converting Custom Edition maps, and the evidence for each layout | +| [port/assets/menus/README.md](port/assets/menus/README.md) | The menu files' format and how to write them again | +| [port/assets/menus/NON_HANDDRAWN.md](port/assets/menus/NON_HANDDRAWN.md) | Menu pictures that are not redraws | +| [port/assets/menus/UNWIRED.md](port/assets/menus/UNWIRED.md) | Menu functions that do nothing yet | +| [port/assets/fonts/README.md](port/assets/fonts/README.md) | The fonts and their licences | +| [tools/harness/README.md](tools/harness/README.md) | Asset-free regression tests | +| `port/third_party/*/README.md` | Each vendored library | +| [AUTHORS.md](AUTHORS.md) | Art credits | diff --git a/port/assets/menus/README.md b/port/assets/menus/README.md index 32190ad1..91332b25 100644 --- a/port/assets/menus/README.md +++ b/port/assets/menus/README.md @@ -30,6 +30,38 @@ file, the line and the problem, and the game uses the Xbox's menus. The setting `display.menus = "xbox"` also uses them. `debug.menu_open` starts on one screen (by its name, `main_menu/settings_select/...`), for looking at it. +## Writing them again + +`tools/ce_menus.py` writes every file in `ce/` and `menus.json` again, +the settings screens too (it runs `tools/port_settings.py`'s): + +```sh +python3 tools/ce_menus.py \ + --tags /tags \ + --definitions /src/tag/hek/definition \ + --redraws \ + --placeholder \ + --pictures +``` + +- `--tags`: the PC version's tags (its `ui\shell` widgets, strings and + bitmaps). +- `--definitions`: Invader's tag definitions, which say how the tags are + laid out. +- `--redraws`: the hand-made SVG redraws, copied to `svg/`. +- `--placeholder`: the picture drawn where a picture has no redraw and the + Xbox's map has not got it. +- `--pictures` (optional): the PC version's pictures, of which only where + each sits in its frame is read. + +It needs `rsvg-convert`, Pillow, NumPy and SciPy. It writes every file, so +`git status` should show only the screens meant to change; then +`ninja linux` (or `ninja android`) embeds them in the game. + +A settings screen (Controls Setup, Gamepads, Mouse, Audio, Video or Network +Setup) is changed in `tools/port_settings.py`, not in its XML, and written +with the command above. + ## Elements A file holds one `` element (`root` names the main menu's widget, in From e005b4ef3fc9427133ab8b733b4ee2977f707ee7 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Sat, 10 Oct 2026 08:51:53 +0200 Subject: [PATCH 185/187] VR roadmap: up to date with OpenCE, network version 25 Co-Authored-By: Claude Opus 5.5 --- port/android/VR_ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/port/android/VR_ROADMAP.md b/port/android/VR_ROADMAP.md index 7ce5345f..a36ba564 100644 --- a/port/android/VR_ROADMAP.md +++ b/port/android/VR_ROADMAP.md @@ -23,7 +23,7 @@ named where code is taken over. | Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles | | Comfort | Snap or smooth turning, a comfort vignette in vehicles, VR options in the main and pause menus (Y), a page of every controller button | | Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard | -| Multiplayer | Up to date with OpenCE (network version 24, 2026-10-08): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list | +| Multiplayer | Up to date with OpenCE (network version 25, 2026-10-10): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list | | Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person | | Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) | | Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) | From 2548dd4a54df36dd10fa8fe16a8f369bd83159ee Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:10:48 +0200 Subject: [PATCH 186/187] Desktop updater: the repository's new name Co-Authored-By: Claude Opus 5.5 --- port/linux/src/updater.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/port/linux/src/updater.c b/port/linux/src/updater.c index 384d65ed..f901a24a 100644 --- a/port/linux/src/updater.c +++ b/port/linux/src/updater.c @@ -47,7 +47,7 @@ update.h's: posix_update.c on Linux, win32_update.c on Windows. #endif /* the VR port's own releases (the original's must not replace it) */ -#define UPDATE_REPOSITORY "Cesarus85/halo-ce-vr" +#define UPDATE_REPOSITORY "Cesarus85/ringfall-mjolnir-vr" #ifdef _WIN32 #define UPDATE_PLATFORM "windows" #define PATH_SEPARATOR "\\" From 6f43e4f915e54886afcf88eb13c34dc088671b74 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:17:28 +0200 Subject: [PATCH 187/187] VR: the campaign's pause menu keeps its list's height with SETTINGS Upstream puts SETTINGS into the single-player pause menu's list; a list with room grows longer and is centred, which put its last row (SAVE AND QUIT) on the line of the button hints below it (B Cancel, A Select). In VR its rows now go closer within the span they had, as a list without room's do. Co-Authored-By: Claude Opus 5.5 --- port/linux/game/menu_tags.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index 73e88cc1..93492123 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -1889,6 +1889,16 @@ static void pause_campaign_patch(struct cache_file_tag_instance *instances) pause_list_fit(list, span); list->bounds.y1 = bottom; } +#ifdef HALO_ANDROID + /* port: in VR the rows closer within the span they had even when + the list has room: the pause menu's button hints (B Cancel, A + Select) sit just below its last row, which a list grown longer + and centred would put on their line */ + else if (halo_vr_active()) + { + pause_list_fit(list, span); + } +#endif pause_settings_patch(); platform_log("menus: the pause menu has SETTINGS"); }