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/__init__.py +0 -0
- tuney/__main__.py +10 -0
- tuney/audio/__init__.py +8 -0
- tuney/audio/device_config.py +40 -0
- tuney/audio/file_player.py +37 -0
- tuney/audio/midi.py +20 -0
- tuney/audio/oscillator.py +49 -0
- tuney/audio/player.py +58 -0
- tuney/audio/runnable.py +22 -0
- tuney/audio/sample_data.py +36 -0
- tuney/audio/scipy.py +701 -0
- tuney/audio/synth_player.py +151 -0
- tuney/keyboard/__init__.py +13 -0
- tuney/keyboard/key_types.py +17 -0
- tuney/keyboard/listener.py +93 -0
- tuney/keyboard/queue.py +84 -0
- tuney/mapper/__init__.py +0 -0
- tuney/mapper/linear_mapper.py +29 -0
- tuney/scale/__init__.py +0 -0
- tuney/scale/nearest_note.py +23 -0
- tuney/scale/scale.py +32 -0
- tuney/scale/twelve_tet.py +50 -0
- tuney/time/__init__.py +4 -0
- tuney/time/event.py +61 -0
- tuney/time/text_timings.py +114 -0
- tuney/ui/__init__.py +0 -0
- tuney/ui/controller.py +45 -0
- tuney/ui/keyboard_controller.py +25 -0
- tuney/ui/note_grid.py +74 -0
- tuney/ui/note_grid.tcss +18 -0
- tuney/ui/text_controller.py +48 -0
- tuney-0.2.0.dist-info/METADATA +23 -0
- tuney-0.2.0.dist-info/RECORD +36 -0
- tuney-0.2.0.dist-info/WHEEL +4 -0
- tuney-0.2.0.dist-info/entry_points.txt +2 -0
- tuney-0.2.0.dist-info/licenses/LICENSE +21 -0
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()
|
tuney/ui/note_grid.tcss
ADDED
|
@@ -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,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.
|