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.
tuney/ui/controller.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dc
4
+ from functools import cached_property
5
+ from typing import Any
6
+
7
+ from ..audio.synth_player import OscillatorController
8
+ from ..keyboard import KeyAction
9
+ from ..mapper.linear_mapper import LinearMapper
10
+ from ..scale import twelve_tet as tt
11
+ from .note_grid import NoteGrid, Text
12
+
13
+
14
+ @dc.dataclass
15
+ class Controller:
16
+ mapper: LinearMapper = LinearMapper()
17
+ oc: OscillatorController = OscillatorController()
18
+
19
+ @cached_property
20
+ def grid(self) -> NoteGrid:
21
+ items = self.mapper.char_to_number.items()
22
+ texts = {n: Text((tt.number_to_name(n), ' ' + c)) for c, n in items}
23
+ return NoteGrid(list(texts.values()))
24
+
25
+ def key_callback(self, k: KeyAction) -> None:
26
+ if (note_number := self.mapper(k.char)) is not None:
27
+ self.on_note(note_number, k.is_press)
28
+
29
+ def on_note(self, note_number: int, is_press: bool) -> None:
30
+ if self.oc.note(note_number, is_press) and False:
31
+ self.grid.texts[note_number].on = is_press
32
+ self.grid.redraw()
33
+
34
+ def run(self) -> None:
35
+ self.grid.run()
36
+
37
+ def stop(self) -> None:
38
+ self.grid.stop()
39
+
40
+ def __enter__(self) -> Controller:
41
+ self.run()
42
+ return self
43
+
44
+ def __exit__(self, *args: Any) -> None:
45
+ self.stop()
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dc
4
+ from functools import cached_property
5
+
6
+ from ..keyboard import KeyboardQueue
7
+ from .controller import Controller
8
+
9
+
10
+ @dc.dataclass
11
+ class KeyboardController(Controller):
12
+ @cached_property
13
+ def keyboard_queue(self) -> KeyboardQueue:
14
+ return KeyboardQueue(self.key_callback)
15
+
16
+ def run(self) -> None:
17
+ self.keyboard_queue.start()
18
+ super().run()
19
+
20
+ def stop(self) -> None:
21
+ super().stop()
22
+ self.keyboard_queue.stop()
23
+
24
+ def join(self) -> None:
25
+ self.keyboard_queue.join()
tuney/ui/note_grid.py ADDED
@@ -0,0 +1,74 @@
1
+ import dataclasses as dc
2
+ import math
3
+ from collections.abc import Sequence
4
+ from functools import cached_property
5
+ from typing import Any, NamedTuple
6
+
7
+ from textual.app import App, ComposeResult
8
+ from textual.reactive import reactive
9
+ from textual.widgets import Static
10
+
11
+ FLAT = ('C', 'D♭', 'D', 'E♭', 'E', 'F', 'G♭', 'G', 'A♭', 'A', 'B♭', 'B')
12
+
13
+
14
+ @dc.dataclass
15
+ class Text:
16
+ labels: Sequence[str]
17
+ on: bool = False
18
+
19
+
20
+ class ColumnsRows(NamedTuple):
21
+ columns: int
22
+ rows: int
23
+
24
+
25
+ class NoteGrid(App):
26
+ theme = 'textual-light'
27
+ CSS_PATH = 'note_grid.tcss'
28
+
29
+ version = reactive(0, recompose=True)
30
+
31
+ texts: Sequence[Text]
32
+
33
+ def __init__(self, texts: Sequence[Text], *args: Any, **kwargs: Any) -> None:
34
+ self.texts = texts
35
+ super().__init__(*args, **kwargs)
36
+
37
+ def redraw(self) -> None:
38
+ self.version += 1
39
+
40
+ def compose(self) -> ComposeResult:
41
+ self.resize_grid()
42
+ for t in self.texts:
43
+ yield Static('\n'.join(t.labels), classes='on' if t.on else 'off')
44
+
45
+ @cached_property
46
+ def shape(self) -> tuple[int, int]:
47
+ n = len(self.texts)
48
+ cols = int(math.ceil(n**0.5))
49
+ rows = n // cols
50
+ rows += n > (rows * cols)
51
+ return cols, rows
52
+
53
+ def resize_grid(self) -> None:
54
+ # From https://textual.textualize.io/styles/grid/grid_size/#python
55
+ self.screen.styles.grid_size_columns = self.shape[0]
56
+ self.screen.styles.grid_size_rows = self.shape[1]
57
+
58
+ def stop(self) -> Any:
59
+ return self.exit()
60
+
61
+
62
+ def _text(i: int) -> Text:
63
+ s = FLAT[i % len(FLAT)]
64
+ return Text((s, s), len(s) > 1)
65
+
66
+
67
+ TEXTS = [_text(i) for i in range(len(FLAT))]
68
+
69
+
70
+ if __name__ == '__main__':
71
+ import sys
72
+
73
+ count = int(sys.argv[1])
74
+ NoteGrid([_text(i) for i in range(count)]).run()
@@ -0,0 +1,18 @@
1
+ Screen {
2
+ layout: grid;
3
+ background: white;
4
+ }
5
+ Static {
6
+ height: 100%;
7
+ content-align-horizontal: center;
8
+ content-align-vertical: middle;
9
+ }
10
+ .on {
11
+ border: solid green;
12
+ color: orange;
13
+ text-style: bold;
14
+ }
15
+ .off {
16
+ border: solid lightblue;
17
+ color: lightgrey;
18
+ }
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dc
4
+ from functools import cached_property
5
+ from threading import Thread
6
+ from typing import TypeAlias
7
+
8
+ from ..keyboard import KeyAction
9
+ from ..time import event
10
+ from ..time.text_timings import TextTimings
11
+ from .controller import Controller
12
+
13
+ Event: TypeAlias = event.Event[KeyAction]
14
+ Runner: TypeAlias = event.Runner[KeyAction]
15
+
16
+
17
+ @dc.dataclass
18
+ class TextController(Controller):
19
+ text: str = ''
20
+ timings: TextTimings = dc.field(default_factory=TextTimings)
21
+
22
+ @cached_property
23
+ def runner(self) -> Runner:
24
+ events = []
25
+ for char, begin, end in self.timings(self.text):
26
+ events.append(Event(begin, KeyAction(char, True)))
27
+ events.append(Event(end, KeyAction(char, False)))
28
+
29
+ return event.Runner(events, self.key_callback)
30
+
31
+ def run(self) -> None:
32
+ Thread(target=self.runner.run).start()
33
+ super().run()
34
+
35
+ def stop(self) -> None:
36
+ super().stop()
37
+ self.runner.stop()
38
+
39
+
40
+ def main() -> None:
41
+ # msg = "Now is the time for all good men to come to the aid of the party"
42
+ msg = 'Now is the time'
43
+ with TextController(text=msg):
44
+ pass
45
+
46
+
47
+ if __name__ == '__main__':
48
+ main()
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: tuney
3
+ Version: 0.2.0
4
+ Summary: 🎶 Turn text into music (#noAI) 🎶
5
+ License-File: LICENSE
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.12
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Classifier: Programming Language :: Python :: 3.14
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: mido>=1.3.3
12
+ Requires-Dist: numpy>=2.4.1
13
+ Requires-Dist: pynput>=1.8.1
14
+ Requires-Dist: sounddevice>=0.5.3
15
+ Requires-Dist: soundfile>=0.13.1
16
+ Requires-Dist: textual>=7.3.0
17
+ Requires-Dist: ty>=0.0.13
18
+ Requires-Dist: typer>=0.20.1
19
+ Description-Content-Type: text/markdown
20
+
21
+ # tuney
22
+
23
+ Turn text into musical notes
@@ -0,0 +1,36 @@
1
+ tuney/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ tuney/__main__.py,sha256=Y_tcx3qTGLUTqRb80jPd0hS4GEbEuj4XVrzFkxDb05E,161
3
+ tuney/audio/__init__.py,sha256=zUU9xhX_Fm_LmYlBlUqb-U_avmnkbu8N8rhDDNB6KDE,221
4
+ tuney/audio/device_config.py,sha256=_8mEHsjNhfPeFsdfdfQ5iA7DsB6ATe94DipAQ8d9TbM,1022
5
+ tuney/audio/file_player.py,sha256=H2hRCmgnozduDneegW__sPbh7Rq3X5OJACgi0gh39XE,997
6
+ tuney/audio/midi.py,sha256=sZl16g39zkr-6NFDf2XCTDsDOUG2JAQYWIq87rI8oyQ,525
7
+ tuney/audio/oscillator.py,sha256=MOm-MZcXzJmBFlnE6FAi4g2AcdHyPn2C3SOrIyhMgrU,1044
8
+ tuney/audio/player.py,sha256=net9FubCpzre6-lTVE5zyfzWIIXqZkV99nPrZfLgzkM,1649
9
+ tuney/audio/runnable.py,sha256=W--PFuMpan6rYw-p44uwoab_HfohM8dj7ozY0DOm_zo,411
10
+ tuney/audio/sample_data.py,sha256=Xc0m25214uOQQl20NF6ctqlBcKw9ADjYZBYaQBUrWLM,932
11
+ tuney/audio/scipy.py,sha256=2Xif9ZeAqy8ohyqSjxp7MNdbQbPma9zvOMQGDtQ7vEk,22895
12
+ tuney/audio/synth_player.py,sha256=cq1VCIusPSvQ9NDvFdyzcYnRSYejFis-NviO0nM5ka8,4622
13
+ tuney/keyboard/__init__.py,sha256=LBU2BtZevRvxtLicVpA76XhTQjLGu9YG_43VpLe5hcs,331
14
+ tuney/keyboard/key_types.py,sha256=doV8MRfGv6mug8beMPS0hNnxKs_-OgZSLRYDkwdpjeQ,349
15
+ tuney/keyboard/listener.py,sha256=1csF1F8VWSLz9AE5H7rj6S5gNT8eFLF4yjXzSehtSEM,2568
16
+ tuney/keyboard/queue.py,sha256=LRD5s4kB1WiQRiRdOoi9PsUlM0nd-pl27B5jNE86h_4,1917
17
+ tuney/mapper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ tuney/mapper/linear_mapper.py,sha256=t9uWcyavqsb0IvA10VrRsjBl70oOw5hVcr4cmBK4F0Q,925
19
+ tuney/scale/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ tuney/scale/nearest_note.py,sha256=WGoqXEihkxwrwV09Rjky6P40mPIP0x21vZsWGa9oyhA,590
21
+ tuney/scale/scale.py,sha256=p3HsLnA4jTqnfAuTQAeQwi5xhYO50cRd-PYSDZ8k9t0,904
22
+ tuney/scale/twelve_tet.py,sha256=WeLtgeB7Xpl5iBKi99XkAct55cKhUdie3jq1P82nCvs,1642
23
+ tuney/time/__init__.py,sha256=NYBZRjIBgMsCfLGQc9kjVY1T-sNn1m4kotoLOXItREs,89
24
+ tuney/time/event.py,sha256=eu15qICNoZ_vL97mZ4amk60M6gfzm7MLgJ0zvzMlpaA,1323
25
+ tuney/time/text_timings.py,sha256=QWWja0MHraNiXGACSTpKbEUiPTDxPiXUFBCQSm4_FnM,4228
26
+ tuney/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ tuney/ui/controller.py,sha256=G-tX4_RJYMX8-MHvMGhe15kDP2m76N-m4gtoH_OwKyk,1313
28
+ tuney/ui/keyboard_controller.py,sha256=_kB2DAEQ4LhEVa8OZcQsD9VGJTFKi1U3BoIft15j6pE,579
29
+ tuney/ui/note_grid.py,sha256=SKempM_igjvDiO_-mY4G3D7tEMJe0Q7HNNpeSIg4nYI,1790
30
+ tuney/ui/note_grid.tcss,sha256=rCn35EgTakZan5Ij7wzwmN9kezhDD0PZARP1rThsuTo,289
31
+ tuney/ui/text_controller.py,sha256=xq_hZBEgBRwvnG9-6y1nenr1MnzyMvvP56nyhlmIyIs,1221
32
+ tuney-0.2.0.dist-info/METADATA,sha256=vjEcKUugLTfXQguJqU-k19TYwDrCRIUSSgTNXYMWQNQ,659
33
+ tuney-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ tuney-0.2.0.dist-info/entry_points.txt,sha256=-lJBV9vdrSdJWRaTmrG7iiUWcYYmQXrxL51FM168xSA,46
35
+ tuney-0.2.0.dist-info/licenses/LICENSE,sha256=NiP-Ub0TlwuTpT0x19xoO7MBhd_OeeX47wfh5mFgFeY,1070
36
+ tuney-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tuney = tuney.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tom Ritchford
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.