tuney 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dc
4
+ import time
5
+ from contextlib import suppress
6
+ from functools import cached_property
7
+ from threading import Thread
8
+
9
+ import numpy as np
10
+
11
+ from ..scale.scale import NoteNumber, Scale
12
+ from ..scale.twelve_tet import TWELVE_TET
13
+ from . import Data, Number
14
+ from . import oscillator as osc
15
+ from .device_config import DeviceConfig
16
+ from .player import Player
17
+
18
+ INTENSITY = 0.1
19
+ FADE = 0 # 0x40000
20
+ OSC = osc.sawtooth
21
+
22
+
23
+ # TODO: relieve the tension between human units (frequency, time) and sample units.
24
+ @dc.dataclass(frozen=True)
25
+ class Sound:
26
+ period: Number = 0x100
27
+ intensity: Number = INTENSITY
28
+ fade_in_samples: Number = 0x1000
29
+ fade_out_samples: Number = 0x1000
30
+
31
+
32
+ @dc.dataclass
33
+ class OscillatorPlayer(Player):
34
+ sound: Sound = dc.field(default_factory=Sound)
35
+ oscillator: osc.Oscillator = OSC
36
+
37
+ _stopping: bool = False
38
+
39
+ #: Records the the frame we started to fade out.
40
+ _fade_frame: Number | None = None
41
+
42
+ def stop(self) -> None:
43
+ if self.sound.fade_out_samples > 0:
44
+ self._stopping = True
45
+ else:
46
+ super().stop()
47
+
48
+ def _fill(self, out: Data) -> bool:
49
+ start = self.frame_count % self.sound.period
50
+ end = start + len(out)
51
+ ratio = self.oscillator.period / self.sound.period
52
+ wave = np.linspace(start * ratio, end * ratio, len(out))
53
+ wave = self.oscillator.function(wave, out=wave)
54
+
55
+ intensity = self.sound.intensity
56
+ with suppress(ValueError):
57
+ # Scale up from [-1, 1] for int types only
58
+ intensity *= np.iinfo(out.dtype).max
59
+ wave *= intensity
60
+
61
+ fade_in = self.sound.fade_in_samples
62
+ if self.frame_count < fade_in and not self._stopping:
63
+ _fade(wave, self.frame_count / fade_in, len(out) / fade_in)
64
+
65
+ elif self._stopping:
66
+ if self._fade_frame is None:
67
+ # Account for the case when we fade out before we've faded in
68
+ offset = max(0.0, fade_in - self.frame_count)
69
+ self._fade_frame = self.frame_count - offset
70
+
71
+ fade_out = self.sound.fade_out_samples
72
+ elapsed = self.frame_count - self._fade_frame
73
+ if (start := 1 - elapsed / fade_out) <= 0:
74
+ super().stop()
75
+ return False
76
+
77
+ _fade(wave, start, -len(out) / fade_out)
78
+
79
+ wave = wave.reshape((len(wave), 1))
80
+ out[:] = np.asarray(wave, dtype=out.dtype)
81
+ return True
82
+
83
+
84
+ @dc.dataclass(frozen=True)
85
+ class OscillatorController:
86
+ config: DeviceConfig = dc.field(default_factory=DeviceConfig)
87
+ oscillator: osc.Oscillator = OSC
88
+ players: dict[int, OscillatorPlayer] = dc.field(default_factory=dict)
89
+ scale: Scale = TWELVE_TET
90
+ start_note_name: str = 'C3'
91
+
92
+ def note(self, note_number: NoteNumber, is_press: bool) -> bool:
93
+ return self.start(note_number) if is_press else self.stop(note_number)
94
+
95
+ def start(self, note_number: NoteNumber) -> bool:
96
+ if note_number in self.players:
97
+ return False
98
+ frequency = self.scale.tuning(note_number + self.start_note_number)
99
+ period = (self.config.samplerate or 48_000) / frequency
100
+ op = OscillatorPlayer(
101
+ config=self.config, oscillator=self.oscillator, sound=Sound(period)
102
+ )
103
+ Thread(target=op.run).start()
104
+ self.players[note_number] = op
105
+ return True
106
+
107
+ def stop(self, note_number: NoteNumber) -> bool:
108
+ if (op := self.players.pop(note_number, None)) is not None:
109
+ op.stop()
110
+ return bool(op)
111
+
112
+ def stop_all(self) -> None:
113
+ for player in self.players.values():
114
+ player.stop()
115
+ self.players.clear()
116
+
117
+ @cached_property
118
+ def start_note_number(self) -> int:
119
+ return self.scale.name_to_number(self.start_note_name)
120
+
121
+
122
+ def _clamp(x: Number) -> Number:
123
+ return max(0.0, min(1.0, x))
124
+
125
+
126
+ def _fade(wave: Data, start: Number, length: Number) -> None:
127
+ wave *= np.linspace(_clamp(start), _clamp(start + length), len(wave))
128
+
129
+
130
+ def run_many_notes():
131
+ oc = OscillatorController()
132
+ DT = 0.2
133
+
134
+ stack = []
135
+ o1 = 'C4', 'E4', 'D5', 'Eb3', 'G3', 'C3', 'E3', 'D4', 'Eb2', 'G2'
136
+ o2 = 'C2', 'E2', 'D3', 'Eb1', 'G1', 'C1', 'E1', 'D2', 'Eb0', 'G0'
137
+
138
+ for name in (o1 + o2)[0]:
139
+ stack.append(note := TWELVE_TET.name_to_number(name))
140
+ if not oc.start(note):
141
+ print('oops', name)
142
+ time.sleep(DT)
143
+
144
+ while stack:
145
+ if not oc.stop(note := stack.pop()):
146
+ print('oops off', note)
147
+ time.sleep(DT / 2)
148
+
149
+
150
+ if __name__ == '__main__':
151
+ run_many_notes()
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ['KeyAction', 'KeyboardListener', 'KeyboardQueue']
4
+
5
+ from .key_types import KeyAction
6
+ from .listener import KeyboardListener
7
+ from .queue import KeyboardQueue
8
+
9
+ if __name__ == '__main__':
10
+ if True:
11
+ KeyboardQueue(print).start()
12
+ else:
13
+ KeyboardListener(print).start()
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dc
4
+ from collections.abc import Callable
5
+ from typing import TypeAlias
6
+
7
+ from pynput import keyboard
8
+
9
+
10
+ @dc.dataclass(frozen=True)
11
+ class KeyAction:
12
+ char: str = ''
13
+ is_press: bool = False
14
+
15
+
16
+ Key: TypeAlias = keyboard.Key | keyboard.KeyCode
17
+ Callback: TypeAlias = Callable[[KeyAction], None]
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dc
4
+ from functools import cached_property, wraps
5
+ from typing import Any
6
+
7
+ from pynput import keyboard
8
+
9
+ from .key_types import Callback, Key, KeyAction
10
+
11
+
12
+ @dc.dataclass
13
+ class Modifiers:
14
+ alt: int = 0
15
+ cmd: int = 0
16
+ ctrl: int = 0
17
+ shift: int = 0
18
+
19
+ def apply(self, key: keyboard.Key, is_press: bool) -> None:
20
+ name = key.name.partition('_')[0]
21
+ if (value := vars(self).get(name)) is not None:
22
+ value = max(0, min(2, value + (1 if is_press else -1)))
23
+ setattr(self, name, value)
24
+
25
+ @property
26
+ def is_printable(self) -> bool:
27
+ return not (self.alt or self.cmd or self.ctrl)
28
+
29
+
30
+ @dc.dataclass
31
+ class KeyboardListener:
32
+ callback: Callback
33
+ stop_key: keyboard.Key | None = None
34
+
35
+ _running: bool = False
36
+
37
+ @cached_property
38
+ def listener(self) -> keyboard.Listener:
39
+ return _make_listener(self)
40
+
41
+ @cached_property
42
+ def modifiers(self) -> Modifiers:
43
+ return Modifiers()
44
+
45
+ def on_press(self, key: Key | None) -> bool | None:
46
+ if not self._running or key == self.stop_key:
47
+ return False
48
+ self._on(key, True)
49
+
50
+ def on_release(self, key: Key | None) -> None:
51
+ self._on(key, False)
52
+
53
+ def start(self) -> None:
54
+ self._running = True
55
+ self.listener.__enter__()
56
+
57
+ def join(self) -> None:
58
+ self.listener.join()
59
+
60
+ def stop(self) -> None:
61
+ self._running = False
62
+ self.listener.stop()
63
+
64
+ def _on(self, key: Key | None, is_press: bool) -> None:
65
+ if isinstance(key, keyboard.Key):
66
+ self.modifiers.apply(key, is_press)
67
+ if self.modifiers.is_printable and (char := getattr(key, 'char', '')):
68
+ self.callback(KeyAction(char, is_press))
69
+
70
+
71
+ def _make_listener(kl: KeyboardListener) -> keyboard.Listener:
72
+ listener = keyboard.Listener(
73
+ on_press=kl.on_press,
74
+ on_release=kl.on_release,
75
+ )
76
+ log = getattr(listener, '_log', None)
77
+ if not (log and hasattr(listener, 'IS_TRUSTED')):
78
+ return listener
79
+
80
+ # Work around a bogus warning in pynput and Darwin
81
+ BOGUS_WARNING = (
82
+ 'This process is not trusted! Input event monitoring will not be possible'
83
+ ' until it is added to accessibility clients.'
84
+ )
85
+ warning_ = log.warning
86
+
87
+ @wraps(warning_)
88
+ def warning(a: str, *args: Any, **kwargs: Any) -> None:
89
+ if not a.strip() or a.replace(BOGUS_WARNING, '').strip() or args or kwargs:
90
+ warning_(a, *args, **kwargs)
91
+
92
+ log.warning = warning
93
+ return listener
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dc
4
+ import sys
5
+ import threading
6
+ import time
7
+ import traceback
8
+ from functools import cached_property
9
+ from queue import Empty, Queue
10
+
11
+ from .key_types import Callback, KeyAction
12
+ from .listener import KeyboardListener
13
+
14
+
15
+ @dc.dataclass
16
+ class KeyboardQueue:
17
+ callback: Callback
18
+ timeout: float = 0.01
19
+ running: bool = False
20
+
21
+ def start(self) -> None:
22
+ self.running = True
23
+ threading.Thread(target=self._target).start()
24
+ self._listener.start()
25
+
26
+ def stop(self) -> None:
27
+ self.running = False
28
+ self._listener.stop()
29
+
30
+ def join(self) -> None:
31
+ # TODO: shouldn't I stop first?
32
+ try:
33
+ self._listener.join()
34
+ finally:
35
+ self.stop()
36
+
37
+ @cached_property
38
+ def _listener(self) -> KeyboardListener:
39
+ return KeyboardListener(self._queue.put)
40
+
41
+ def _target(self) -> None:
42
+ try:
43
+ while self.running:
44
+ try:
45
+ key_action = self._queue.get(timeout=self.timeout)
46
+ except Empty:
47
+ continue
48
+ if not key_action:
49
+ break
50
+ self.callback(key_action)
51
+ self.callback(KeyAction())
52
+ except Exception:
53
+ print('THREAD TERMINATED', file=sys.stderr)
54
+ traceback.print_exc()
55
+
56
+ @cached_property
57
+ def _queue(self) -> Queue[KeyAction]:
58
+ return Queue()
59
+
60
+
61
+ def time_keyboard() -> None:
62
+ def key_callback(k):
63
+ if k.is_press:
64
+ nonlocal now
65
+ old, now = now, time.time()
66
+ print(now - old)
67
+
68
+ now = time.time()
69
+ kq = KeyboardQueue(key_callback)
70
+ kq.start()
71
+
72
+
73
+ def report() -> None:
74
+ def key_callback(k):
75
+ if k.is_press:
76
+ print(k)
77
+
78
+ kq = KeyboardQueue(key_callback)
79
+ kq.start()
80
+
81
+
82
+ if __name__ == '__main__':
83
+ report()
84
+ # time_keyboard()
File without changes
@@ -0,0 +1,29 @@
1
+ import dataclasses as dc
2
+ from functools import cached_property
3
+ from string import ascii_letters, ascii_lowercase
4
+
5
+
6
+ @dc.dataclass(frozen=True)
7
+ class LinearMapper:
8
+ alphabet: str | None = None
9
+ length: int = 0
10
+ case_sensitive: bool = True
11
+ invert: bool = False
12
+ offset: int = 0
13
+
14
+ def __call__(self, k: str) -> int | None:
15
+ return self.char_to_number.get(k if self.case_sensitive else k.lower())
16
+
17
+ @cached_property
18
+ def char_to_number(self) -> dict[str, int]:
19
+ if not (alphabet := self.alphabet):
20
+ alphabet = ascii_letters if self.case_sensitive else ascii_lowercase
21
+
22
+ def char_to_number(index: int, c: str) -> int:
23
+ if self.invert:
24
+ index = len(alphabet) - index - 1
25
+ if self.length:
26
+ index %= self.length
27
+ return index + self.offset
28
+
29
+ return {a: char_to_number(i, a) for i, a in enumerate(alphabet)}
File without changes
@@ -0,0 +1,23 @@
1
+ from .scale import Tuning
2
+
3
+ EPSILON = 1e-6
4
+
5
+
6
+ def nearest_note(
7
+ tuning: Tuning, frequency: float, epsilon: float = EPSILON
8
+ ) -> int | tuple[int, int]:
9
+ below = above = 0
10
+ while tuning(below) > frequency:
11
+ below = (2 * below) or -0x40
12
+ while tuning(above) < frequency:
13
+ above = (2 * above) or 0x40
14
+ while (above - below) > 1:
15
+ mid = (below + above) // 2
16
+ f = tuning(mid)
17
+ if abs(f - frequency) < epsilon:
18
+ return mid
19
+ elif f < frequency:
20
+ below = mid
21
+ else:
22
+ above = mid
23
+ return below, above
tuney/scale/scale.py ADDED
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import NamedTuple
5
+
6
+ type Frequency = float # Must be non-negative
7
+ type NoteNumber = int # May be negative
8
+ type NumberToFrequency = Callable[[NoteNumber], Frequency]
9
+ type Tuning = NumberToFrequency
10
+
11
+ type NameToNumber = Callable[[str], NoteNumber]
12
+ type NumberToName = Callable[[NoteNumber, ...], str]
13
+
14
+
15
+ class Scale(NamedTuple):
16
+ tuning: Tuning
17
+ name_to_number: NameToNumber
18
+ number_to_name: NumberToName
19
+
20
+
21
+ class XNote(NamedTuple): # The future of Note
22
+ scale: Scale
23
+ number: NoteNumber
24
+ name: str
25
+
26
+ @staticmethod
27
+ def make(scale: Scale, n: NoteNumber | str) -> XNote:
28
+ if isinstance(n, str):
29
+ name, number = n, scale.name_to_number(n)
30
+ else:
31
+ name, number = scale.number_to_name(n), n # ty: ignore[missing-argument]
32
+ return XNote(scale, number, name)
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from .scale import NoteNumber, Scale
6
+
7
+ # Standard: 60 = C3, C-1 == 0 Yamaha: 60 = C4, C0 == 0
8
+ A440 = 69
9
+
10
+ MIDI_ZERO_OCTAVE = -1
11
+ ACCIDENTAL_DICT = {'#': '♯', 'b': '♭', '♭': '♭', '♯': '♯'}
12
+ ACCIDENTALS = '#b♭♯'
13
+ FLAT, SHARP = '♭', '♯'
14
+ CANONICALS = FLAT + SHARP
15
+ NAME_TO_NUMBER = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11}
16
+ NAMES = ''.join(NAME_TO_NUMBER)
17
+ NUMBER_TO_NAME = {
18
+ FLAT: ('C', 'D♭', 'D', 'E♭', 'E', 'F', 'G♭', 'G', 'A♭', 'A', 'B♭', 'B'),
19
+ SHARP: ('C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'),
20
+ }
21
+ NOTE_RE = re.compile(rf'([{NAMES}])([{ACCIDENTALS}]*)(-?\d*)')
22
+
23
+ assert ACCIDENTALS == ''.join(sorted(ACCIDENTAL_DICT))
24
+ assert CANONICALS == ACCIDENTALS[2:]
25
+
26
+
27
+ def tuning(note_number: NoteNumber) -> float:
28
+ return 440.0 * 2 ** ((note_number - A440) / 12)
29
+
30
+
31
+ def name_to_number(note_name: str) -> int:
32
+ if not (m := NOTE_RE.match(note_name)):
33
+ raise ValueError(f'Cannot understand note {note_name}')
34
+
35
+ name, accidentals, octave = m.groups()
36
+ semitones = sum(2 * (ACCIDENTAL_DICT[a] == SHARP) - 1 for a in accidentals)
37
+ octaves = int(octave) - MIDI_ZERO_OCTAVE
38
+ return 12 * octaves + NAME_TO_NUMBER[name] + semitones
39
+
40
+
41
+ def number_to_name(number: NoteNumber, use_sharp: bool = True) -> str:
42
+ accidental = SHARP if use_sharp else FLAT
43
+ assert accidental in NUMBER_TO_NAME, accidental
44
+ octave, number1 = divmod(number, 12)
45
+ octave += MIDI_ZERO_OCTAVE
46
+ name = NUMBER_TO_NAME[accidental][number1]
47
+ return f'{name}{octave}'
48
+
49
+
50
+ TWELVE_TET = Scale(tuning, name_to_number, number_to_name)
tuney/time/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from typing import TypeAlias
2
+
3
+ Milliseconds: TypeAlias = float
4
+ Seconds: TypeAlias = float
tuney/time/event.py ADDED
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dc
4
+ import heapq
5
+ import time
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ from . import Seconds
10
+
11
+ MAX_WAIT: Seconds = 0.01
12
+
13
+
14
+ @dc.dataclass
15
+ class Event[Data]:
16
+ timestamp: Seconds
17
+ data: Data
18
+
19
+ def __lt__(self, other: Event) -> bool:
20
+ return self.timestamp < other.timestamp
21
+
22
+
23
+ @dc.dataclass
24
+ class Runner[Data]:
25
+ events: list[Event[Data]]
26
+ callback: Callable[..., Any]
27
+
28
+ _running: bool = False
29
+
30
+ def __post_init__(self) -> None:
31
+ heapq.heapify(self.events)
32
+
33
+ def run(self) -> None:
34
+ self._running = True
35
+ while self._running:
36
+ while self.events and self.events[0].timestamp <= time.time():
37
+ self.callback(d := heapq.heappop(self.events).data)
38
+ print('run', d)
39
+
40
+ if self.events:
41
+ time.sleep(min(MAX_WAIT, self.events[0].timestamp - time.time()))
42
+ else:
43
+ self.stop()
44
+
45
+ def stop(self) -> None:
46
+ self._running = False
47
+
48
+
49
+ def demo() -> None:
50
+ def event() -> Event[float]:
51
+ import random
52
+
53
+ t = timestamp + random.uniform(0, 1)
54
+ return Event(t, t - timestamp)
55
+
56
+ timestamp = time.time()
57
+ Runner([event() for _ in range(10)], print).run()
58
+
59
+
60
+ if __name__ == '__main__':
61
+ demo()
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dc
4
+ import random
5
+ from collections.abc import Collection, Iterable, Iterator
6
+ from functools import cached_property
7
+ from typing import NamedTuple
8
+
9
+ from . import Milliseconds
10
+
11
+
12
+ class CharBeginEnd(NamedTuple):
13
+ char: str
14
+ begin: Milliseconds
15
+ end: Milliseconds
16
+
17
+
18
+ @dc.dataclass(frozen=True)
19
+ class TextTimings:
20
+ space: Milliseconds = 100
21
+ period: Milliseconds = 300
22
+ comma: Milliseconds = 200
23
+ colon: Milliseconds = 400
24
+ semicolon: Milliseconds = 400
25
+ blank_line: Milliseconds = 1000
26
+
27
+ overlap: Milliseconds = 20
28
+ random_seed: int | None = None
29
+ alpha_only: bool = True
30
+ strip_accents: bool = True
31
+
32
+ other: dict[str, Milliseconds] = dc.field(default_factory=dict)
33
+ timings: Collection[Milliseconds] | None = None
34
+
35
+ @cached_property
36
+ def timings_(self) -> Collection[Milliseconds]:
37
+ return self.timings or _TIMINGS
38
+
39
+ @cached_property
40
+ def average_time(self) -> float:
41
+ return sum(self.timings_) / len(self.timings_)
42
+
43
+ @cached_property
44
+ def random(self):
45
+ return random.Random(self.random_seed)
46
+
47
+ @cached_property
48
+ def char_to_time(self) -> dict[str, Milliseconds]:
49
+ return {v: getattr(self, k) for k, v in _CHARS.items()} | self.other
50
+
51
+ def __call__(self, text: str) -> Iterator[CharBeginEnd]:
52
+ time = 0
53
+ chars = _strip_accents(text) if self.strip_accents else text
54
+ for char in _filter_chars(chars):
55
+ dt = self.char_to_time.get(char)
56
+ if char.isalpha() or not (dt is None and self.alpha_only):
57
+ dt = (dt or 0.0) + self.random.choice(self.timings_)
58
+ yield CharBeginEnd(char, time, time + dt)
59
+ time += max(0, dt - self.overlap)
60
+
61
+
62
+ def _filter_chars(it: Iterable[str]) -> Iterator[str]:
63
+ # Filter out the first `\n', so each \n means an actual new line,
64
+ # and any spaces after the first one.
65
+ previous = ''
66
+ for c in it:
67
+ if not c.isspace():
68
+ yield c
69
+ elif c not in ' \n':
70
+ continue
71
+ elif c == '\n' == previous:
72
+ yield c
73
+ elif c == ' ' and previous not in ' \n':
74
+ yield c
75
+ previous = c
76
+
77
+
78
+ def _strip_accents(s: str) -> Iterator[str]:
79
+ # https://stackoverflow.com/questions/517923/
80
+ from unicodedata import category, normalize
81
+
82
+ yield from (c for c in normalize('NFD', s) if category(c) != 'Mn')
83
+
84
+
85
+ _CHARS = {
86
+ 'space': ' ',
87
+ 'period': '.',
88
+ 'comma': ',',
89
+ 'colon': ':',
90
+ 'semicolon': ';',
91
+ 'blank_line': '\n',
92
+ }
93
+ _TIMINGS = (
94
+ (56.04, 57.92, 60.35, 61.94, 62.54, 63.29, 63.32, 64.27, 66.26, 66.92)
95
+ + (67.98, 68.02, 68.44, 69.33, 69.61, 70.61, 72.72, 72.75, 72.76, 73.42)
96
+ + (75.07, 77.92, 78.52, 80.12, 83.55, 84.34, 85.13, 85.36, 85.47, 85.77)
97
+ + (85.97, 86.32, 86.7, 88.45, 89.24, 89.26, 89.44, 89.54, 90.82, 91.18)
98
+ + (91.22, 92.69, 92.94, 93.32, 94.24, 94.59, 94.75, 95.83, 96.64, 97.79)
99
+ + (98.76, 99.11, 100.01, 100.38, 101.67, 103.73, 104.81, 104.87, 105.41, 106.56)
100
+ + (107.5, 107.68, 107.97, 110.95, 111.07, 113.24, 115.18, 116.24, 116.24, 117.64)
101
+ + (120.67, 122.09, 124.4, 125.88, 127.58, 128.57, 129.16, 130.85, 131.73, 133.19)
102
+ + (133.81, 134.46, 136.04, 138.25, 140.16, 140.21, 140.22, 140.32, 141.9, 142.83)
103
+ + (143.1, 148.71, 149.47, 149.88, 150.53, 153.21, 153.44, 153.66, 153.95, 156.17)
104
+ + (157.75, 157.76, 158.17, 159.87, 160.48, 160.87, 160.97, 163.56, 164.12, 167.26)
105
+ + (167.36, 168.21, 168.55, 169.04, 169.85, 169.86, 170.61, 171.43, 171.46, 172.9)
106
+ + (173.78, 174.22, 174.76, 175.52, 176.18, 176.38, 176.49, 176.83, 178.75, 179.03)
107
+ + (179.41, 180.81, 181.69, 182.37, 184.2, 184.48, 185.46, 185.9, 185.96, 187.05)
108
+ + (188.67, 189.03, 190.0, 190.71, 192.68, 192.74, 192.88, 193.29, 194.49, 197.11)
109
+ + (197.81, 199.08, 200.55, 200.66, 200.7, 201.65, 202.79, 203.32, 205.1, 205.7)
110
+ + (206.18, 209.24, 210.69, 213.53, 214.09, 214.13, 222.19, 222.56, 222.58, 223.53)
111
+ + (224.91, 227.26, 230.08, 234.93, 236.72, 246.01, 259.32, 260.27, 261.92, 266.67)
112
+ + (269.42, 279.88, 287.71, 295.99, 299.94, 317.22, 329.73, 330.68, 357.17, 367.83)
113
+ + (419.5, 422.63, 475.46, 521.2, 526.85, 594.64, 738.79)
114
+ )
tuney/ui/__init__.py ADDED
File without changes