trackmod 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.
- trackmod/__init__.py +79 -0
- trackmod/binary/__init__.py +0 -0
- trackmod/binary/bits.py +35 -0
- trackmod/binary/cursor.py +101 -0
- trackmod/binary/layout.py +12 -0
- trackmod/binary/nibble.py +46 -0
- trackmod/binary/pcm/__init__.py +0 -0
- trackmod/binary/pcm/blocks.py +20 -0
- trackmod/binary/pcm/codec.py +53 -0
- trackmod/binary/pcm/encoding.py +13 -0
- trackmod/binary/pcm/quantize.py +19 -0
- trackmod/binary/pcm/sign.py +39 -0
- trackmod/binary/records/__init__.py +0 -0
- trackmod/binary/records/field.py +50 -0
- trackmod/binary/records/record.py +99 -0
- trackmod/binary/records/values.py +44 -0
- trackmod/binary/text.py +31 -0
- trackmod/binary/volume.py +114 -0
- trackmod/binary/warnings.py +40 -0
- trackmod/core/__init__.py +0 -0
- trackmod/core/effects/__init__.py +0 -0
- trackmod/core/effects/catalog.py +35 -0
- trackmod/core/effects/effect.py +17 -0
- trackmod/core/envelopes/__init__.py +0 -0
- trackmod/core/envelopes/curve.py +85 -0
- trackmod/core/envelopes/envelope.py +43 -0
- trackmod/core/envelopes/kind.py +10 -0
- trackmod/core/envelopes/point.py +16 -0
- trackmod/core/envelopes/repair.py +85 -0
- trackmod/core/envelopes/span.py +22 -0
- trackmod/core/instruments/__init__.py +0 -0
- trackmod/core/instruments/behavior.py +30 -0
- trackmod/core/instruments/fade.py +61 -0
- trackmod/core/instruments/instrument.py +122 -0
- trackmod/core/instruments/keymap.py +38 -0
- trackmod/core/instruments/repair.py +49 -0
- trackmod/core/instruments/transfer.py +95 -0
- trackmod/core/instruments/unit.py +42 -0
- trackmod/core/notes/__init__.py +0 -0
- trackmod/core/notes/checks.py +33 -0
- trackmod/core/notes/codec.py +24 -0
- trackmod/core/notes/command.py +20 -0
- trackmod/core/notes/pitch.py +93 -0
- trackmod/core/patterns/__init__.py +0 -0
- trackmod/core/patterns/builder.py +59 -0
- trackmod/core/patterns/cell.py +22 -0
- trackmod/core/patterns/codec.py +38 -0
- trackmod/core/patterns/column.py +19 -0
- trackmod/core/patterns/grid.py +148 -0
- trackmod/core/patterns/repair.py +36 -0
- trackmod/core/repairs/__init__.py +0 -0
- trackmod/core/repairs/report.py +47 -0
- trackmod/core/samples/__init__.py +0 -0
- trackmod/core/samples/depth.py +36 -0
- trackmod/core/samples/loop.py +58 -0
- trackmod/core/samples/repair.py +86 -0
- trackmod/core/samples/sample.py +140 -0
- trackmod/core/samples/vibrato.py +22 -0
- trackmod/core/songs/__init__.py +0 -0
- trackmod/core/songs/order.py +37 -0
- trackmod/core/songs/playback.py +17 -0
- trackmod/core/songs/repair.py +20 -0
- trackmod/core/songs/song.py +67 -0
- trackmod/core/timing/__init__.py +0 -0
- trackmod/core/timing/clock.py +50 -0
- trackmod/core/timing/lattice.py +122 -0
- trackmod/core/timing/timing.py +14 -0
- trackmod/core/timing/timings.py +54 -0
- trackmod/core/voices/__init__.py +0 -0
- trackmod/core/voices/convert.py +110 -0
- trackmod/core/voices/voices.py +68 -0
- trackmod/core/volumes/__init__.py +0 -0
- trackmod/core/volumes/checks.py +41 -0
- trackmod/core/volumes/codec.py +37 -0
- trackmod/core/volumes/command.py +60 -0
- trackmod/limits/__init__.py +0 -0
- trackmod/limits/bound.py +42 -0
- trackmod/limits/capability.py +45 -0
- trackmod/limits/capacity.py +41 -0
- trackmod/limits/checklist.py +30 -0
- trackmod/limits/compliance.py +22 -0
- trackmod/limits/error.py +31 -0
- trackmod/limits/guard.py +13 -0
- trackmod/limits/reach.py +35 -0
- trackmod/limits/table.py +99 -0
- trackmod/limits/tier.py +14 -0
- trackmod/limits/violation.py +40 -0
- trackmod/module/__init__.py +0 -0
- trackmod/module/instrument.py +96 -0
- trackmod/module/protocol.py +112 -0
- trackmod/module/provenance.py +39 -0
- trackmod/module/reaching.py +44 -0
- trackmod/module/size.py +33 -0
- trackmod/module/storage.py +106 -0
- trackmod/py.typed +0 -0
- trackmod/schema/__init__.py +0 -0
- trackmod/schema/array.py +52 -0
- trackmod/schema/config.py +6 -0
- trackmod/schema/scalars.py +39 -0
- trackmod/spec/__init__.py +0 -0
- trackmod/spec/application.py +13 -0
- trackmod/spec/clock.py +7 -0
- trackmod/spec/grid.py +14 -0
- trackmod/spec/levels.py +17 -0
- trackmod/spec/pitch.py +24 -0
- trackmod/spec/text.py +5 -0
- trackmod/spec/volumes.py +8 -0
- trackmod/spec/width.py +15 -0
- trackmod/trackers/amiga/__init__.py +0 -0
- trackmod/trackers/amiga/checks.py +65 -0
- trackmod/trackers/amiga/layout/__init__.py +0 -0
- trackmod/trackers/amiga/layout/file.py +10 -0
- trackmod/trackers/amiga/layout/sample.py +17 -0
- trackmod/trackers/amiga/note.py +122 -0
- trackmod/trackers/amiga/patterns/__init__.py +0 -0
- trackmod/trackers/amiga/patterns/packer.py +94 -0
- trackmod/trackers/amiga/patterns/parser.py +109 -0
- trackmod/trackers/amiga/patterns/sizing.py +11 -0
- trackmod/trackers/amiga/reading.py +140 -0
- trackmod/trackers/amiga/samples/__init__.py +0 -0
- trackmod/trackers/amiga/samples/parser.py +87 -0
- trackmod/trackers/amiga/samples/writer.py +122 -0
- trackmod/trackers/amiga/sizing.py +32 -0
- trackmod/trackers/amiga/spec/__init__.py +0 -0
- trackmod/trackers/amiga/spec/cells.py +19 -0
- trackmod/trackers/amiga/spec/defaults.py +7 -0
- trackmod/trackers/amiga/spec/periods.py +34 -0
- trackmod/trackers/amiga/spec/ranges.py +21 -0
- trackmod/trackers/amiga/spec/sizes.py +11 -0
- trackmod/trackers/amiga/spec/storage.py +9 -0
- trackmod/trackers/amiga/tuning.py +31 -0
- trackmod/trackers/amiga/writing.py +50 -0
- trackmod/trackers/it/__init__.py +0 -0
- trackmod/trackers/it/addressing.py +29 -0
- trackmod/trackers/it/checks.py +120 -0
- trackmod/trackers/it/detection.py +15 -0
- trackmod/trackers/it/effects/__init__.py +0 -0
- trackmod/trackers/it/effects/catalog.py +115 -0
- trackmod/trackers/it/effects/command.py +59 -0
- trackmod/trackers/it/extensions.py +118 -0
- trackmod/trackers/it/fade.py +19 -0
- trackmod/trackers/it/instrument_file.py +182 -0
- trackmod/trackers/it/instruments/__init__.py +0 -0
- trackmod/trackers/it/instruments/envelope.py +109 -0
- trackmod/trackers/it/instruments/keymap.py +53 -0
- trackmod/trackers/it/instruments/parser.py +100 -0
- trackmod/trackers/it/instruments/writer.py +82 -0
- trackmod/trackers/it/layout/__init__.py +0 -0
- trackmod/trackers/it/layout/envelope.py +51 -0
- trackmod/trackers/it/layout/file.py +38 -0
- trackmod/trackers/it/layout/instrument.py +54 -0
- trackmod/trackers/it/layout/pattern.py +14 -0
- trackmod/trackers/it/layout/sample.py +34 -0
- trackmod/trackers/it/limits.py +8 -0
- trackmod/trackers/it/message.py +50 -0
- trackmod/trackers/it/module.py +229 -0
- trackmod/trackers/it/note.py +43 -0
- trackmod/trackers/it/panning.py +12 -0
- trackmod/trackers/it/parser.py +309 -0
- trackmod/trackers/it/patterns/__init__.py +0 -0
- trackmod/trackers/it/patterns/cost.py +16 -0
- trackmod/trackers/it/patterns/encoded.py +9 -0
- trackmod/trackers/it/patterns/memory.py +19 -0
- trackmod/trackers/it/patterns/packer.py +178 -0
- trackmod/trackers/it/patterns/parser.py +211 -0
- trackmod/trackers/it/patterns/sizing.py +117 -0
- trackmod/trackers/it/patterns/width.py +36 -0
- trackmod/trackers/it/samples/__init__.py +0 -0
- trackmod/trackers/it/samples/compression.py +159 -0
- trackmod/trackers/it/samples/parser.py +288 -0
- trackmod/trackers/it/samples/writer.py +97 -0
- trackmod/trackers/it/settings.py +68 -0
- trackmod/trackers/it/sizing.py +55 -0
- trackmod/trackers/it/spec/__init__.py +0 -0
- trackmod/trackers/it/spec/capacities.py +122 -0
- trackmod/trackers/it/spec/cells.py +52 -0
- trackmod/trackers/it/spec/defaults.py +24 -0
- trackmod/trackers/it/spec/effects.py +19 -0
- trackmod/trackers/it/spec/extensions.py +13 -0
- trackmod/trackers/it/spec/flags.py +62 -0
- trackmod/trackers/it/spec/identity.py +14 -0
- trackmod/trackers/it/spec/orders.py +5 -0
- trackmod/trackers/it/spec/ranges.py +56 -0
- trackmod/trackers/it/spec/sizes.py +20 -0
- trackmod/trackers/it/spec/storage.py +27 -0
- trackmod/trackers/it/spec/volume.py +51 -0
- trackmod/trackers/it/timing.py +10 -0
- trackmod/trackers/it/version.py +100 -0
- trackmod/trackers/it/writer.py +133 -0
- trackmod/trackers/mod/__init__.py +0 -0
- trackmod/trackers/mod/detection.py +11 -0
- trackmod/trackers/mod/dialect.py +60 -0
- trackmod/trackers/mod/effects/__init__.py +0 -0
- trackmod/trackers/mod/effects/catalog.py +106 -0
- trackmod/trackers/mod/effects/command.py +63 -0
- trackmod/trackers/mod/layout/__init__.py +0 -0
- trackmod/trackers/mod/layout/file.py +17 -0
- trackmod/trackers/mod/limits.py +8 -0
- trackmod/trackers/mod/module.py +261 -0
- trackmod/trackers/mod/parser.py +102 -0
- trackmod/trackers/mod/settings.py +27 -0
- trackmod/trackers/mod/sizing.py +9 -0
- trackmod/trackers/mod/spec/__init__.py +0 -0
- trackmod/trackers/mod/spec/capacities.py +58 -0
- trackmod/trackers/mod/spec/dialects.py +34 -0
- trackmod/trackers/mod/spec/effects.py +19 -0
- trackmod/trackers/mod/spec/identity.py +12 -0
- trackmod/trackers/mod/spec/ranges.py +21 -0
- trackmod/trackers/mod/spec/sizes.py +13 -0
- trackmod/trackers/mod/spec/storage.py +17 -0
- trackmod/trackers/mod/tag.py +79 -0
- trackmod/trackers/mod/timing.py +6 -0
- trackmod/trackers/mod/writer.py +58 -0
- trackmod/trackers/registry.py +277 -0
- trackmod/trackers/s3m/__init__.py +0 -0
- trackmod/trackers/s3m/channels.py +38 -0
- trackmod/trackers/s3m/checks.py +91 -0
- trackmod/trackers/s3m/detection.py +10 -0
- trackmod/trackers/s3m/effects/__init__.py +0 -0
- trackmod/trackers/s3m/effects/catalog.py +105 -0
- trackmod/trackers/s3m/effects/command.py +54 -0
- trackmod/trackers/s3m/layout/__init__.py +0 -0
- trackmod/trackers/s3m/layout/file.py +36 -0
- trackmod/trackers/s3m/layout/instrument.py +28 -0
- trackmod/trackers/s3m/layout/pattern.py +10 -0
- trackmod/trackers/s3m/limits.py +8 -0
- trackmod/trackers/s3m/module.py +252 -0
- trackmod/trackers/s3m/note.py +101 -0
- trackmod/trackers/s3m/panning.py +22 -0
- trackmod/trackers/s3m/parapointers.py +41 -0
- trackmod/trackers/s3m/parser.py +233 -0
- trackmod/trackers/s3m/patterns/__init__.py +0 -0
- trackmod/trackers/s3m/patterns/packer.py +103 -0
- trackmod/trackers/s3m/patterns/parser.py +144 -0
- trackmod/trackers/s3m/patterns/sizing.py +37 -0
- trackmod/trackers/s3m/placement.py +89 -0
- trackmod/trackers/s3m/samples/__init__.py +0 -0
- trackmod/trackers/s3m/samples/parser.py +211 -0
- trackmod/trackers/s3m/samples/writer.py +102 -0
- trackmod/trackers/s3m/settings.py +67 -0
- trackmod/trackers/s3m/sizing.py +44 -0
- trackmod/trackers/s3m/spec/__init__.py +0 -0
- trackmod/trackers/s3m/spec/capacities.py +100 -0
- trackmod/trackers/s3m/spec/cells.py +45 -0
- trackmod/trackers/s3m/spec/defaults.py +17 -0
- trackmod/trackers/s3m/spec/effects.py +19 -0
- trackmod/trackers/s3m/spec/flags.py +57 -0
- trackmod/trackers/s3m/spec/identity.py +18 -0
- trackmod/trackers/s3m/spec/keys.py +16 -0
- trackmod/trackers/s3m/spec/orders.py +4 -0
- trackmod/trackers/s3m/spec/ranges.py +58 -0
- trackmod/trackers/s3m/spec/sizes.py +20 -0
- trackmod/trackers/s3m/spec/storage.py +35 -0
- trackmod/trackers/s3m/spec/volume.py +24 -0
- trackmod/trackers/s3m/timing.py +10 -0
- trackmod/trackers/s3m/version.py +98 -0
- trackmod/trackers/s3m/writer.py +176 -0
- trackmod/trackers/st/__init__.py +0 -0
- trackmod/trackers/st/detection.py +36 -0
- trackmod/trackers/st/effects/__init__.py +0 -0
- trackmod/trackers/st/effects/catalog.py +110 -0
- trackmod/trackers/st/effects/command.py +25 -0
- trackmod/trackers/st/layout/__init__.py +0 -0
- trackmod/trackers/st/layout/file.py +15 -0
- trackmod/trackers/st/limits.py +8 -0
- trackmod/trackers/st/module.py +244 -0
- trackmod/trackers/st/parser.py +95 -0
- trackmod/trackers/st/settings.py +20 -0
- trackmod/trackers/st/sizing.py +9 -0
- trackmod/trackers/st/spec/__init__.py +0 -0
- trackmod/trackers/st/spec/capacities.py +42 -0
- trackmod/trackers/st/spec/defaults.py +3 -0
- trackmod/trackers/st/spec/effects.py +10 -0
- trackmod/trackers/st/spec/identity.py +3 -0
- trackmod/trackers/st/spec/ranges.py +21 -0
- trackmod/trackers/st/spec/sizes.py +14 -0
- trackmod/trackers/st/spec/storage.py +17 -0
- trackmod/trackers/st/timing.py +10 -0
- trackmod/trackers/st/writer.py +51 -0
- trackmod/trackers/xm/__init__.py +0 -0
- trackmod/trackers/xm/addressing.py +22 -0
- trackmod/trackers/xm/checks.py +122 -0
- trackmod/trackers/xm/detection.py +20 -0
- trackmod/trackers/xm/effects/__init__.py +0 -0
- trackmod/trackers/xm/effects/catalog.py +113 -0
- trackmod/trackers/xm/effects/command.py +62 -0
- trackmod/trackers/xm/fade.py +19 -0
- trackmod/trackers/xm/instrument_file.py +188 -0
- trackmod/trackers/xm/instruments/__init__.py +0 -0
- trackmod/trackers/xm/instruments/envelope.py +118 -0
- trackmod/trackers/xm/instruments/group.py +45 -0
- trackmod/trackers/xm/instruments/grouping.py +100 -0
- trackmod/trackers/xm/instruments/keymap.py +26 -0
- trackmod/trackers/xm/instruments/parser.py +83 -0
- trackmod/trackers/xm/instruments/writer.py +144 -0
- trackmod/trackers/xm/layout/__init__.py +0 -0
- trackmod/trackers/xm/layout/envelope.py +75 -0
- trackmod/trackers/xm/layout/file.py +30 -0
- trackmod/trackers/xm/layout/instrument.py +79 -0
- trackmod/trackers/xm/layout/pattern.py +15 -0
- trackmod/trackers/xm/layout/sample.py +21 -0
- trackmod/trackers/xm/limits.py +8 -0
- trackmod/trackers/xm/module.py +246 -0
- trackmod/trackers/xm/note.py +83 -0
- trackmod/trackers/xm/parser.py +166 -0
- trackmod/trackers/xm/patterns/__init__.py +0 -0
- trackmod/trackers/xm/patterns/encoded.py +32 -0
- trackmod/trackers/xm/patterns/packer.py +80 -0
- trackmod/trackers/xm/patterns/parser.py +163 -0
- trackmod/trackers/xm/patterns/sizing.py +37 -0
- trackmod/trackers/xm/samples/__init__.py +0 -0
- trackmod/trackers/xm/samples/parser.py +139 -0
- trackmod/trackers/xm/samples/writer.py +73 -0
- trackmod/trackers/xm/settings.py +23 -0
- trackmod/trackers/xm/sizing.py +52 -0
- trackmod/trackers/xm/spec/__init__.py +0 -0
- trackmod/trackers/xm/spec/capacities.py +110 -0
- trackmod/trackers/xm/spec/cells.py +36 -0
- trackmod/trackers/xm/spec/defaults.py +13 -0
- trackmod/trackers/xm/spec/effects.py +19 -0
- trackmod/trackers/xm/spec/flags.py +37 -0
- trackmod/trackers/xm/spec/identity.py +13 -0
- trackmod/trackers/xm/spec/ranges.py +70 -0
- trackmod/trackers/xm/spec/sizes.py +32 -0
- trackmod/trackers/xm/spec/storage.py +27 -0
- trackmod/trackers/xm/spec/tuning.py +12 -0
- trackmod/trackers/xm/spec/volume.py +67 -0
- trackmod/trackers/xm/timing.py +10 -0
- trackmod/trackers/xm/tuning.py +73 -0
- trackmod/trackers/xm/writer.py +62 -0
- trackmod/utils/__init__.py +0 -0
- trackmod/utils/arithmetic.py +47 -0
- trackmod/wave/__init__.py +0 -0
- trackmod/wave/chunks.py +77 -0
- trackmod/wave/layout.py +88 -0
- trackmod/wave/parser.py +225 -0
- trackmod/wave/settings.py +25 -0
- trackmod/wave/spec.py +64 -0
- trackmod/wave/writer.py +213 -0
- trackmod-0.2.0.dist-info/METADATA +170 -0
- trackmod-0.2.0.dist-info/RECORD +343 -0
- trackmod-0.2.0.dist-info/WHEEL +4 -0
- trackmod-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from trackmod.core.volumes.codec import decode_volume
|
|
6
|
+
from trackmod.core.volumes.command import (
|
|
7
|
+
VolumeCommand,
|
|
8
|
+
VolumeEffect,
|
|
9
|
+
VolumeValue,
|
|
10
|
+
)
|
|
11
|
+
from trackmod.limits.bound import Bound
|
|
12
|
+
from trackmod.schema.config import FROZEN
|
|
13
|
+
from trackmod.spec.grid import EMPTY
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VolumeSpan(BaseModel):
|
|
17
|
+
"""One run of a volume-column byte: what it states, where the run opens, and the amounts it holds."""
|
|
18
|
+
|
|
19
|
+
model_config = FROZEN
|
|
20
|
+
|
|
21
|
+
effect: VolumeEffect
|
|
22
|
+
base: int
|
|
23
|
+
amounts: Bound
|
|
24
|
+
|
|
25
|
+
def holds(self, byte: int) -> bool:
|
|
26
|
+
"""Whether ``byte`` falls inside this run."""
|
|
27
|
+
return self.amounts.contains(byte - self.base)
|
|
28
|
+
|
|
29
|
+
def stated(self, byte: int) -> VolumeCommand:
|
|
30
|
+
"""The command ``byte`` states, at the amount it counts from this run's base."""
|
|
31
|
+
return VolumeCommand(effect=self.effect, amount=byte - self.base)
|
|
32
|
+
|
|
33
|
+
def stored(self, amount: int) -> int:
|
|
34
|
+
"""The byte this run stores ``amount`` as."""
|
|
35
|
+
return self.base + amount
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VolumeColumn(BaseModel):
|
|
39
|
+
"""How one format divides its volume-column byte between levels and the effects it carries beside them.
|
|
40
|
+
|
|
41
|
+
A tracker reads the column by the range a byte falls in, so stating those ranges as data is what lets
|
|
42
|
+
the parser, the packer and the capacity table read one specification. ``absent`` is the byte a format
|
|
43
|
+
writes where a cell states no volume at all, which the formats storing every column of a cell need.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
model_config = FROZEN
|
|
47
|
+
|
|
48
|
+
level_base: int
|
|
49
|
+
levels: Bound
|
|
50
|
+
spans: tuple[VolumeSpan, ...]
|
|
51
|
+
absent: int | None
|
|
52
|
+
|
|
53
|
+
def span(self, effect: VolumeEffect) -> VolumeSpan | None:
|
|
54
|
+
"""The run this column gives ``effect``, where it names one."""
|
|
55
|
+
return next((span for span in self.spans if span.effect is effect), None)
|
|
56
|
+
|
|
57
|
+
def states_volume(self, byte: int) -> bool:
|
|
58
|
+
"""Whether ``byte`` states a volume at all, apart from the byte a format writes for none."""
|
|
59
|
+
return byte != self.absent
|
|
60
|
+
|
|
61
|
+
def stated(self, byte: int) -> VolumeValue | None:
|
|
62
|
+
"""The level or command ``byte`` states, or ``None`` when it names nothing this column defines."""
|
|
63
|
+
if not self.states_volume(byte):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
level = byte - self.level_base
|
|
67
|
+
if self.levels.contains(level):
|
|
68
|
+
return level
|
|
69
|
+
|
|
70
|
+
return next((span.stated(byte) for span in self.spans if span.holds(byte)), None)
|
|
71
|
+
|
|
72
|
+
def stored(self, volume: VolumeValue) -> int | None:
|
|
73
|
+
"""The byte this column stores ``volume`` as, or ``None`` when it cannot state it."""
|
|
74
|
+
match volume:
|
|
75
|
+
case VolumeCommand():
|
|
76
|
+
span = self.span(volume.effect)
|
|
77
|
+
return span.stored(volume.amount) if span is not None and span.amounts.contains(volume.amount) else None
|
|
78
|
+
case int():
|
|
79
|
+
return self.level_base + volume if self.levels.contains(volume) else None
|
|
80
|
+
|
|
81
|
+
def encoded(self, volume: VolumeValue) -> int:
|
|
82
|
+
"""The byte this column stores ``volume`` as.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ValueError: when the column cannot state ``volume``.
|
|
86
|
+
"""
|
|
87
|
+
byte = self.stored(volume)
|
|
88
|
+
if byte is None:
|
|
89
|
+
raise ValueError(self.refusal(volume))
|
|
90
|
+
|
|
91
|
+
return byte
|
|
92
|
+
|
|
93
|
+
def stored_code(self, code: int) -> int:
|
|
94
|
+
"""The byte a grid volume code is written as, leaving an absent volume absent.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: when the code names a volume this column cannot state.
|
|
98
|
+
"""
|
|
99
|
+
if code == EMPTY:
|
|
100
|
+
return EMPTY
|
|
101
|
+
|
|
102
|
+
return self.encoded(decode_volume(code))
|
|
103
|
+
|
|
104
|
+
def refusal(self, volume: VolumeValue) -> str:
|
|
105
|
+
"""Why this column cannot state ``volume``."""
|
|
106
|
+
match volume:
|
|
107
|
+
case VolumeCommand():
|
|
108
|
+
span = self.span(volume.effect)
|
|
109
|
+
if span is None:
|
|
110
|
+
return f"the volume column has no run for {volume.effect.name}"
|
|
111
|
+
|
|
112
|
+
return f"amount {volume.amount} lies outside {span.amounts}, the amounts its run holds"
|
|
113
|
+
case int():
|
|
114
|
+
return f"level {volume} lies outside {self.levels}, the levels the volume column holds"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
from trackmod.core.patterns.column import Column
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UnnamedByteWarning(UserWarning):
|
|
10
|
+
"""A stored byte naming nothing its format defines, which the column it sits in reads as absent."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnnamedBytes:
|
|
14
|
+
"""The bytes one parse met that name nothing the format defines, gathered so one warning covers them.
|
|
15
|
+
|
|
16
|
+
A file reaching past what this library has a vocabulary for states it in cell after cell, so
|
|
17
|
+
gathering what was met and reporting once says as much as a warning per cell would -- the way a
|
|
18
|
+
:class:`~trackmod.limits.checklist.Checklist` gathers the violations of a whole module.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._met: dict[Column, set[int]] = defaultdict(set)
|
|
23
|
+
|
|
24
|
+
def met(self, byte: int, *, column: Column) -> None:
|
|
25
|
+
"""Record a byte the column names nothing for."""
|
|
26
|
+
self._met[column].add(byte)
|
|
27
|
+
|
|
28
|
+
def warn(self) -> None:
|
|
29
|
+
"""Report everything gathered as one warning, where anything was met."""
|
|
30
|
+
if not self._met:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
stated = "; ".join(
|
|
34
|
+
f"{column} {', '.join(str(byte) for byte in sorted(bytes_met))}" for column, bytes_met in self._met.items()
|
|
35
|
+
)
|
|
36
|
+
warnings.warn(
|
|
37
|
+
f"bytes this format leaves unnamed, read as absent: {stated}",
|
|
38
|
+
UnnamedByteWarning,
|
|
39
|
+
stacklevel=2,
|
|
40
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from trackmod.core.effects.effect import Effect
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EffectCatalog(Protocol):
|
|
7
|
+
"""The effects every format here expresses, so a caller can author them without picking a format.
|
|
8
|
+
|
|
9
|
+
Every method returns the :class:`Effect` its format uses for that intent and validates the argument
|
|
10
|
+
against the room the format's parameter byte leaves for it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def set_speed(self, ticks: int) -> Effect:
|
|
14
|
+
"""Set the ticks each row lasts."""
|
|
15
|
+
|
|
16
|
+
def set_tempo(self, beats_per_minute: int) -> Effect:
|
|
17
|
+
"""Set the tick rate, in the tracker's beats-per-minute units."""
|
|
18
|
+
|
|
19
|
+
def position_jump(self, order: int) -> Effect:
|
|
20
|
+
"""Continue playback at the given order-list position."""
|
|
21
|
+
|
|
22
|
+
def pattern_break(self, row: int) -> Effect:
|
|
23
|
+
"""End the pattern and continue at the given row of the next order."""
|
|
24
|
+
|
|
25
|
+
def note_delay(self, ticks: int) -> Effect:
|
|
26
|
+
"""Delay this cell's note by the given number of ticks into the row."""
|
|
27
|
+
|
|
28
|
+
def note_cut(self, ticks: int) -> Effect:
|
|
29
|
+
"""Silence this channel the given number of ticks into the row."""
|
|
30
|
+
|
|
31
|
+
def volume_slide(self, *, up: int, down: int) -> Effect:
|
|
32
|
+
"""Slide the channel volume by one nibble per tick, in exactly one direction."""
|
|
33
|
+
|
|
34
|
+
def set_panning(self, position: int) -> Effect:
|
|
35
|
+
"""Place the channel on the stereo field, on the shared 0..255 scale."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
from trackmod.schema.config import FROZEN
|
|
4
|
+
from trackmod.spec.width import BYTE_MAX
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Effect(BaseModel):
|
|
8
|
+
"""One effect-column entry: a format-specific command byte and its parameter byte.
|
|
9
|
+
|
|
10
|
+
The pair travels together because a tracker reads them together — a parameter without its command is
|
|
11
|
+
meaningless. Each format's catalog and command enumeration give the byte values their meaning.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
model_config = FROZEN
|
|
15
|
+
|
|
16
|
+
command: int = Field(ge=0, le=BYTE_MAX)
|
|
17
|
+
parameter: int = Field(ge=0, le=BYTE_MAX)
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Final
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from trackmod.core.envelopes.envelope import Envelope
|
|
7
|
+
from trackmod.core.envelopes.point import EnvelopePoint
|
|
8
|
+
from trackmod.core.envelopes.span import EnvelopeSpan
|
|
9
|
+
from trackmod.core.timing.clock import elapsed_ticks, tick_seconds
|
|
10
|
+
from trackmod.limits.bound import Bound
|
|
11
|
+
from trackmod.schema.config import FROZEN
|
|
12
|
+
|
|
13
|
+
TICK_APART: Final = 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Breakpoint(BaseModel):
|
|
17
|
+
"""One point of a curve stated in time: the value it reaches ``seconds`` after a voice starts."""
|
|
18
|
+
|
|
19
|
+
model_config = FROZEN
|
|
20
|
+
|
|
21
|
+
seconds: float = Field(ge=0.0)
|
|
22
|
+
value: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def placed_ticks(breakpoints: Sequence[Breakpoint], *, tempo: int, bound: Bound) -> tuple[int, ...]:
|
|
26
|
+
"""Where each breakpoint lands on the tick grid, ascending and inside ``bound``.
|
|
27
|
+
|
|
28
|
+
A format reads its breakpoints in order and separates them by at least a tick, so two that round onto
|
|
29
|
+
the same tick are moved apart, and a curve reaching past the last tick a format counts is drawn back
|
|
30
|
+
into the ticks that remain. Each breakpoint keeps room for the ones behind it, which is what keeps
|
|
31
|
+
the end of a curve stated where a format would otherwise be handed points it cannot order.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: when the breakpoints outnumber the ticks in ``bound``, leaving nowhere to separate them.
|
|
35
|
+
"""
|
|
36
|
+
if len(breakpoints) > bound.room:
|
|
37
|
+
raise ValueError(f"{len(breakpoints)} breakpoints ask for more than the {bound.room} ticks in {bound}")
|
|
38
|
+
|
|
39
|
+
placed: list[int] = []
|
|
40
|
+
for index, point in enumerate(breakpoints):
|
|
41
|
+
lowest = placed[-1] + TICK_APART if placed else bound.minimum
|
|
42
|
+
highest = bound.maximum - (len(breakpoints) - 1 - index)
|
|
43
|
+
placed.append(min(max(elapsed_ticks(point.seconds, tempo), lowest), highest))
|
|
44
|
+
|
|
45
|
+
return tuple(placed)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def timed_envelope(
|
|
49
|
+
breakpoints: Sequence[Breakpoint],
|
|
50
|
+
*,
|
|
51
|
+
tempo: int,
|
|
52
|
+
tick_bound: Bound,
|
|
53
|
+
value_bound: Bound,
|
|
54
|
+
loop: EnvelopeSpan | None = None,
|
|
55
|
+
sustain: EnvelopeSpan | None = None,
|
|
56
|
+
) -> Envelope:
|
|
57
|
+
"""An envelope holding a curve given in time, written onto the tick grid ``tempo`` runs.
|
|
58
|
+
|
|
59
|
+
A caller fitting a curve to a recording works in seconds and levels; a format stores ticks and node
|
|
60
|
+
values inside its own ranges. This states the curve once in the terms it was measured in and hands a
|
|
61
|
+
format what it holds: each breakpoint at the tick it falls on, each value on the grid ``value_bound``
|
|
62
|
+
names, both spans over the breakpoint indices as given.
|
|
63
|
+
|
|
64
|
+
Ticks are relative to ``tempo``, so the same curve written for a different clock is a different
|
|
65
|
+
envelope -- which is why the tempo a curve was fitted at is worth keeping beside it.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: when the breakpoints outnumber the ticks in ``tick_bound``, or when a span reaches
|
|
69
|
+
past the breakpoints given.
|
|
70
|
+
"""
|
|
71
|
+
ticks = placed_ticks(breakpoints, tempo=tempo, bound=tick_bound)
|
|
72
|
+
points = tuple(
|
|
73
|
+
EnvelopePoint(tick=tick, value=value_bound.clamp(point.value)) for tick, point in zip(ticks, breakpoints)
|
|
74
|
+
)
|
|
75
|
+
return Envelope(points=points, loop=loop, sustain=sustain)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def envelope_seconds(envelope: Envelope, *, tempo: int) -> tuple[Breakpoint, ...]:
|
|
79
|
+
"""The curve an envelope holds, read back in time at the tempo it was written for.
|
|
80
|
+
|
|
81
|
+
This is the way back from :func:`timed_envelope`, for a caller measuring what a stored curve does
|
|
82
|
+
against the trajectory it was fitted to.
|
|
83
|
+
"""
|
|
84
|
+
length = tick_seconds(tempo)
|
|
85
|
+
return tuple(Breakpoint(seconds=point.tick * length, value=point.value) for point in envelope.points)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
|
+
|
|
7
|
+
from trackmod.core.envelopes.point import EnvelopePoint
|
|
8
|
+
from trackmod.core.envelopes.span import EnvelopeSpan
|
|
9
|
+
from trackmod.schema.config import FROZEN
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Envelope(BaseModel):
|
|
13
|
+
"""A breakpoint curve applied to every voice an instrument starts.
|
|
14
|
+
|
|
15
|
+
An instrument that carries no envelope of a kind leaves that property alone, so absence rather than
|
|
16
|
+
an enable flag is what switches an envelope off.
|
|
17
|
+
|
|
18
|
+
Points are given in tick order. Two points sharing a tick stand the curve up vertically there, which
|
|
19
|
+
is how a tracker states an instant change of level.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
model_config = FROZEN
|
|
23
|
+
|
|
24
|
+
points: tuple[EnvelopePoint, ...] = Field(min_length=1)
|
|
25
|
+
loop: EnvelopeSpan | None = None
|
|
26
|
+
sustain: EnvelopeSpan | None = None
|
|
27
|
+
|
|
28
|
+
@model_validator(mode="after")
|
|
29
|
+
def _consistent(self) -> Envelope:
|
|
30
|
+
ticks = [point.tick for point in self.points]
|
|
31
|
+
if any(later < earlier for earlier, later in itertools.pairwise(ticks)):
|
|
32
|
+
raise ValueError(f"envelope ticks run backward: {ticks}")
|
|
33
|
+
|
|
34
|
+
for name, span in (("loop", self.loop), ("sustain", self.sustain)):
|
|
35
|
+
if span is not None and span.end >= self.length:
|
|
36
|
+
raise ValueError(f"envelope {name} span {span.begin}..{span.end} escapes {self.length} points")
|
|
37
|
+
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def length(self) -> int:
|
|
42
|
+
"""How many breakpoints the curve holds."""
|
|
43
|
+
return len(self.points)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from trackmod.schema.config import FROZEN
|
|
4
|
+
from trackmod.schema.scalars import Tick
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EnvelopePoint(BaseModel):
|
|
8
|
+
"""One breakpoint: the value the envelope reaches at ``tick``, interpolated from the point before.
|
|
9
|
+
|
|
10
|
+
A point sharing its tick with the point before it is reached at once, stating a step in the curve.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
model_config = FROZEN
|
|
14
|
+
|
|
15
|
+
tick: Tick
|
|
16
|
+
value: int
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from trackmod.core.envelopes.point import EnvelopePoint
|
|
4
|
+
from trackmod.core.envelopes.span import EnvelopeSpan
|
|
5
|
+
from trackmod.core.repairs.report import Repairs
|
|
6
|
+
from trackmod.limits.bound import Bound
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def repaired_span(
|
|
10
|
+
bounds: tuple[int, int] | None,
|
|
11
|
+
*,
|
|
12
|
+
points: int,
|
|
13
|
+
name: str,
|
|
14
|
+
subject: str,
|
|
15
|
+
repairs: Repairs,
|
|
16
|
+
) -> EnvelopeSpan | None:
|
|
17
|
+
"""The pair of point indices a file states, drawn inside the points the envelope holds.
|
|
18
|
+
|
|
19
|
+
A tracker states a loop or a sustain as a pair of point indices, and files carry pairs reaching past
|
|
20
|
+
the last point or ending before they begin. Both are drawn back to the points that exist: the end to
|
|
21
|
+
the last point, then the begin to the end, which is the reading players settled on. The pair is taken
|
|
22
|
+
as the two numbers the record holds, since a span ending before it begins is one this model states no
|
|
23
|
+
span for.
|
|
24
|
+
"""
|
|
25
|
+
if bounds is None:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
stated_begin, stated_end = bounds
|
|
29
|
+
end = min(stated_end, points - 1)
|
|
30
|
+
begin = min(stated_begin, end)
|
|
31
|
+
if (begin, end) != (stated_begin, stated_end):
|
|
32
|
+
repairs.made(
|
|
33
|
+
f"{name} span {stated_begin}..{stated_end} drawn to {begin}..{end}",
|
|
34
|
+
subject=subject,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return EnvelopeSpan(begin=begin, end=end)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def repaired_points(
|
|
41
|
+
points: Sequence[EnvelopePoint],
|
|
42
|
+
*,
|
|
43
|
+
subject: str,
|
|
44
|
+
repairs: Repairs,
|
|
45
|
+
) -> tuple[EnvelopePoint, ...]:
|
|
46
|
+
"""Stored breakpoints put in tick order, holding each one at the tick the one before it reached.
|
|
47
|
+
|
|
48
|
+
A curve is read in the order its points are stored, so a point stated at an earlier tick than the
|
|
49
|
+
one before it is played the moment that one is. Files carry such points where a tracker left the
|
|
50
|
+
node table longer than the count it uses.
|
|
51
|
+
"""
|
|
52
|
+
ordered: list[EnvelopePoint] = []
|
|
53
|
+
for point in points:
|
|
54
|
+
reached = ordered[-1].tick if ordered else point.tick
|
|
55
|
+
ordered.append(point if point.tick >= reached else EnvelopePoint(tick=reached, value=point.value))
|
|
56
|
+
|
|
57
|
+
moved = sum(1 for before, after in zip(points, ordered) if before.tick != after.tick)
|
|
58
|
+
if moved:
|
|
59
|
+
repairs.made(
|
|
60
|
+
f"{moved} envelope points stated out of order held at the tick before them",
|
|
61
|
+
subject=subject,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return tuple(ordered)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def leveled_points(
|
|
68
|
+
points: Sequence[EnvelopePoint],
|
|
69
|
+
*,
|
|
70
|
+
bound: Bound,
|
|
71
|
+
subject: str,
|
|
72
|
+
repairs: Repairs,
|
|
73
|
+
) -> tuple[EnvelopePoint, ...]:
|
|
74
|
+
"""Stored breakpoints drawn inside the levels a format's own field holds.
|
|
75
|
+
|
|
76
|
+
A tracker states each breakpoint's level in the width its record keeps and leaves the nodes past the
|
|
77
|
+
count it uses as it found them, so a file states levels in nodes no curve reaches. Each is drawn to
|
|
78
|
+
the nearest level the field holds, which keeps the curve the file meant.
|
|
79
|
+
"""
|
|
80
|
+
drawn = tuple(EnvelopePoint(tick=point.tick, value=bound.clamp(point.value)) for point in points)
|
|
81
|
+
moved = sum(1 for before, after in zip(points, drawn) if before.value != after.value)
|
|
82
|
+
if moved:
|
|
83
|
+
repairs.made(f"{moved} envelope levels drawn inside {bound}", subject=subject)
|
|
84
|
+
|
|
85
|
+
return drawn
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, model_validator
|
|
4
|
+
|
|
5
|
+
from trackmod.schema.config import FROZEN
|
|
6
|
+
from trackmod.schema.scalars import Index
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EnvelopeSpan(BaseModel):
|
|
10
|
+
"""An inclusive range of point indices, as trackers store envelope loop and sustain points."""
|
|
11
|
+
|
|
12
|
+
model_config = FROZEN
|
|
13
|
+
|
|
14
|
+
begin: Index
|
|
15
|
+
end: Index
|
|
16
|
+
|
|
17
|
+
@model_validator(mode="after")
|
|
18
|
+
def _ascending(self) -> EnvelopeSpan:
|
|
19
|
+
if self.end < self.begin:
|
|
20
|
+
raise ValueError(f"envelope span {self.begin}..{self.end} runs backward")
|
|
21
|
+
|
|
22
|
+
return self
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from enum import IntEnum, unique
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@unique
|
|
5
|
+
class NewNoteAction(IntEnum):
|
|
6
|
+
"""What happens to a channel's playing voice when the same channel starts a new note."""
|
|
7
|
+
|
|
8
|
+
CUT = 0
|
|
9
|
+
CONTINUE = 1
|
|
10
|
+
NOTE_OFF = 2
|
|
11
|
+
FADE = 3
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@unique
|
|
15
|
+
class DuplicateCheck(IntEnum):
|
|
16
|
+
"""What makes two voices of one instrument count as duplicates."""
|
|
17
|
+
|
|
18
|
+
OFF = 0
|
|
19
|
+
NOTE = 1
|
|
20
|
+
SAMPLE = 2
|
|
21
|
+
INSTRUMENT = 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@unique
|
|
25
|
+
class DuplicateAction(IntEnum):
|
|
26
|
+
"""What happens to the voice a duplicate check finds."""
|
|
27
|
+
|
|
28
|
+
CUT = 0
|
|
29
|
+
NOTE_OFF = 1
|
|
30
|
+
FADE = 2
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Final
|
|
3
|
+
|
|
4
|
+
from trackmod.core.timing.clock import tick_seconds
|
|
5
|
+
from trackmod.spec.levels import MIN_FADEOUT, NO_FADEOUT
|
|
6
|
+
|
|
7
|
+
SLOWEST_STEP: Final = 1
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fade_ticks(fadeout: int, *, counter: int) -> float:
|
|
11
|
+
"""How many ticks a fading voice takes to reach silence, counting down by ``fadeout`` a tick.
|
|
12
|
+
|
|
13
|
+
A voice being faded carries a counter that starts full at ``counter`` and drops by ``fadeout`` every
|
|
14
|
+
tick, and what remains of it scales the level the voice plays at. So the fadeout an instrument
|
|
15
|
+
states is a rate, and the time it buys is ``counter / fadeout`` ticks.
|
|
16
|
+
|
|
17
|
+
``NO_FADEOUT`` leaves the counter full, which is a voice that keeps its level for as long as it
|
|
18
|
+
sounds and reads here as a fade of unbounded length.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: when ``fadeout`` runs backward, or ``counter`` is empty.
|
|
22
|
+
"""
|
|
23
|
+
if fadeout < MIN_FADEOUT:
|
|
24
|
+
raise ValueError(f"fadeout {fadeout} is below {MIN_FADEOUT}")
|
|
25
|
+
|
|
26
|
+
if counter < SLOWEST_STEP:
|
|
27
|
+
raise ValueError(f"a fade counter holds at least {SLOWEST_STEP}, got {counter}")
|
|
28
|
+
|
|
29
|
+
return math.inf if fadeout == NO_FADEOUT else counter / fadeout
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fade_seconds(fadeout: int, *, counter: int, tempo: int) -> float:
|
|
33
|
+
"""How long a fading voice takes to reach silence at ``tempo``.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: when ``fadeout`` runs backward, ``counter`` is empty, or ``tempo`` is below the
|
|
37
|
+
slowest clock the shared model counts.
|
|
38
|
+
"""
|
|
39
|
+
return fade_ticks(fadeout, counter=counter) * tick_seconds(tempo)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def fadeout_value(seconds: float, *, counter: int, tempo: int) -> int:
|
|
43
|
+
"""The rate an instrument states to fade a voice out over ``seconds`` at ``tempo``.
|
|
44
|
+
|
|
45
|
+
This is the way back from :func:`fade_seconds`, for a caller that knows how long a released note
|
|
46
|
+
should take to die and needs the counter step a format stores.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: when ``seconds`` is at most nothing, or asks for a fade slower than the counter
|
|
50
|
+
reaches. The slowest fade a format states drops its counter by one a tick, which lasts
|
|
51
|
+
``counter`` ticks, and anything slower would round to the value meaning no fade at all.
|
|
52
|
+
"""
|
|
53
|
+
if seconds <= 0.0:
|
|
54
|
+
raise ValueError(f"a fade lasts a positive time, got {seconds} seconds")
|
|
55
|
+
|
|
56
|
+
value = round(counter * tick_seconds(tempo) / seconds)
|
|
57
|
+
if value < SLOWEST_STEP:
|
|
58
|
+
longest = fade_seconds(SLOWEST_STEP, counter=counter, tempo=tempo)
|
|
59
|
+
raise ValueError(f"a fade of {seconds} s at tempo {tempo} is longer than the {longest} s the counter reaches")
|
|
60
|
+
|
|
61
|
+
return value
|