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

1"""Dithering algorithms.""" 

2import random 

3 

4 

5def dither_pattern(count: int, dither_levels: int) -> list[int]: 

6 """Generate an unbiased dither pattern list. 

7 

8 The pattern is tiled and then adjusted so that its mean value is close to (levels - 1) / 2. 

9 

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 

15 

16 # If no dithering (0 or 1 level), just return a list of zeros 

17 if dither_max_amplitude <= 0: 

18 return [0] * count 

19 

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)) 

22 

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)] 

26 

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) 

31 

32 current_sum = sum(dither_values) 

33 diff = target_sum_int - current_sum 

34 

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 

45 

46 # shuffle with fixed seed (deterministic order) 

47 rng = random.Random(1) 

48 rng.shuffle(dither_values) 

49 

50 return dither_values 

51 

52 

53def dither_weights(count: int, dither_levels: int) -> list[float]: 

54 """Generate an unbiased dither weights. 

55 

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. 

58 

59 Not all count & dither_levels combinations produces zero mean. For zero mean, use:: 

60 

61 - When dither_levels is odd: count >= dither_levels - 2 

62 - When dither_levels is even: count = even and >= dither_levels - 2 

63 

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)]