Coverage for src / zooc / dsp / dither.py: 8%
28 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 21:45 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 21:45 +0000
1"""Dithering algorithms."""
2import random
5def dither_pattern(count: int, dither_levels: int) -> list[int]:
6 """Generate an unbiased dither pattern list.
8 The pattern is tiled and then adjusted so that its mean value is close to (levels - 1) / 2.
10 :param count: The total number of dither values to generate.
11 :param dither_levels: The number of discrete dither levels (e.g., 3 for [0, 1, 2]).
12 :return: A list of dither levels.
13 """
14 dither_max_amplitude = dither_levels - 1
16 # If no dithering (0 or 1 level), just return a list of zeros
17 if dither_max_amplitude <= 0:
18 return [0] * count
20 # Generate base pattern, e.g. dither_max_amplitude = 2 -> [0, 1, 2, 1]
21 dither_pattern_base = list(range(dither_max_amplitude + 1)) + list(range(dither_max_amplitude - 1, 0, -1))
23 # Tile the pattern
24 base_len = len(dither_pattern_base)
25 dither_values = [dither_pattern_base[i % base_len] for i in range(count)]
27 # Adjust pattern to be unbiased. Ensure the mean is as close as possible to target_mean
28 target_mean = dither_max_amplitude / 2.0
29 target_sum = count * target_mean
30 target_sum_int = round(target_sum)
32 current_sum = sum(dither_values)
33 diff = target_sum_int - current_sum
35 # Distribute the difference to match the target sum
36 for i in range(count):
37 if diff == 0:
38 break
39 if diff > 0 and dither_values[i] < dither_max_amplitude:
40 dither_values[i] += 1
41 diff -= 1
42 elif diff < 0 < dither_values[i]:
43 dither_values[i] -= 1
44 diff += 1
46 # shuffle with fixed seed (deterministic order)
47 rng = random.Random(1)
48 rng.shuffle(dither_values)
50 return dither_values
53def dither_weights(count: int, dither_levels: int) -> list[float]:
54 """Generate an unbiased dither weights.
56 The weights are tiled and then adjusted so that its mean value is close to zero.
57 The noise injected to measurement signal should have an amplitude of weight*LSB.
59 Not all count & dither_levels combinations produces zero mean. For zero mean, use::
61 - When dither_levels is odd: count >= dither_levels - 2
62 - When dither_levels is even: count = even and >= dither_levels - 2
64 :param count: The total number of dither values to generate (measurements).
65 :param dither_levels: The number of discrete dither levels.
66 :return: A list of dither weights.
67 """
68 offset = (dither_levels - 1) / dither_levels / 2.0
69 return [level / dither_levels - offset for level in dither_pattern(count=count, dither_levels=dither_levels)]