arkos2basic 0.1.0b5__tar.gz → 0.1.1b1__tar.gz
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.
- {arkos2basic-0.1.0b5 → arkos2basic-0.1.1b1}/PKG-INFO +1 -1
- {arkos2basic-0.1.0b5 → arkos2basic-0.1.1b1}/pyproject.toml +7 -1
- {arkos2basic-0.1.0b5 → arkos2basic-0.1.1b1}/src/arkos2basic/arkostracker/baseimport.py +73 -13
- {arkos2basic-0.1.0b5 → arkos2basic-0.1.1b1}/src/arkos2basic/arkostracker/song.py +13 -0
- {arkos2basic-0.1.0b5 → arkos2basic-0.1.1b1}/src/arkos2basic/arkostracker/txtimport.py +0 -10
- arkos2basic-0.1.1b1/src/arkos2basic/cli.py +301 -0
- arkos2basic-0.1.1b1/src/arkos2basic/cvbasic.py +1307 -0
- arkos2basic-0.1.0b5/src/arkos2basic/cli.py +0 -139
- arkos2basic-0.1.0b5/src/arkos2basic/cvbasic.py +0 -280
- {arkos2basic-0.1.0b5 → arkos2basic-0.1.1b1}/README.md +0 -0
- {arkos2basic-0.1.0b5 → arkos2basic-0.1.1b1}/src/arkos2basic/__init__.py +0 -0
- {arkos2basic-0.1.0b5 → arkos2basic-0.1.1b1}/src/arkos2basic/arkostracker/__init__.py +0 -0
- {arkos2basic-0.1.0b5 → arkos2basic-0.1.1b1}/src/arkos2basic/arkostracker/cell.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arkos2basic
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1b1
|
|
4
4
|
Summary: Converts Arkos Tracker 3 text exports to CVBasic MUSIC blocks. Handles note transposition, variable note duration, percussion detection and intro/loop splitting. Targets ColecoVision, MSX and Sega SG-1000.
|
|
5
5
|
Author: Francesco Maida
|
|
6
6
|
Author-email: francesco.maida@gmail.com
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "arkos2basic"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1b1"
|
|
4
4
|
description = "Converts Arkos Tracker 3 text exports to CVBasic MUSIC blocks. Handles note transposition, variable note duration, percussion detection and intro/loop splitting. Targets ColecoVision, MSX and Sega SG-1000."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Francesco Maida",email = "francesco.maida@gmail.com"}
|
|
@@ -17,6 +17,12 @@ packages = [{include = "arkos2basic", from = "src"}]
|
|
|
17
17
|
[tool.poetry.scripts]
|
|
18
18
|
arkos2basic = "arkos2basic.cli:app"
|
|
19
19
|
|
|
20
|
+
[tool.poetry.group.dev.dependencies]
|
|
21
|
+
pytest = "^8.3"
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
25
|
+
|
|
20
26
|
[build-system]
|
|
21
27
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
22
28
|
build-backend = "poetry.core.masonry.api"
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
"""Base class for Arkos Tracker file importers."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from arkos2basic.arkostracker.song import Song
|
|
7
|
-
from arkos2basic.cvbasic import convert
|
|
8
|
+
from arkos2basic.cvbasic import convert, convert_playlist
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
logger = logging.getLogger(__name__)
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
class BaseImport:
|
|
14
|
+
class BaseImport(ABC):
|
|
14
15
|
"""Base importer for Arkos Tracker source files.
|
|
15
16
|
|
|
16
17
|
Subclasses implement parse() to read a specific file format and return
|
|
@@ -38,6 +39,7 @@ class BaseImport:
|
|
|
38
39
|
self._transpose: int = 0
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
@abstractmethod
|
|
41
43
|
def parse(self) -> Song:
|
|
42
44
|
"""Read self.input_file and return the parsed Song.
|
|
43
45
|
|
|
@@ -69,6 +71,12 @@ class BaseImport:
|
|
|
69
71
|
stop: bool,
|
|
70
72
|
drum_length: int,
|
|
71
73
|
exclude_channels: set[int] | None = None,
|
|
74
|
+
map_instruments: bool = True,
|
|
75
|
+
force_suffixes: dict[int, str] | None = None,
|
|
76
|
+
playlist: bool = False,
|
|
77
|
+
min_block: int = 32,
|
|
78
|
+
fuse: set[int] | None = None,
|
|
79
|
+
pack: bool = False,
|
|
72
80
|
) -> None:
|
|
73
81
|
"""Convert the song to CVBasic and write the output file(s).
|
|
74
82
|
|
|
@@ -77,6 +85,10 @@ class BaseImport:
|
|
|
77
85
|
Otherwise a single file is written. Status messages are emitted
|
|
78
86
|
via the module logger at INFO level.
|
|
79
87
|
|
|
88
|
+
Playlist mode replaces the split: the song is cut into reusable
|
|
89
|
+
blocks and a single file carries the blocks, the playlist and the
|
|
90
|
+
BASIC glue that chains them, looping by rewinding the playlist.
|
|
91
|
+
|
|
80
92
|
Args:
|
|
81
93
|
output_file: path for the main output .bas file.
|
|
82
94
|
label: label assigned to the music block.
|
|
@@ -86,9 +98,27 @@ class BaseImport:
|
|
|
86
98
|
stop: if True, always end with MUSIC STOP (no split).
|
|
87
99
|
drum_length: consecutive steps per percussion hit.
|
|
88
100
|
exclude_channels: 1-based source channel indices to skip.
|
|
101
|
+
map_instruments: if True, Arkos instruments 1-3 are mapped
|
|
102
|
+
to the CVBasic suffixes W/X/Y.
|
|
103
|
+
force_suffixes: map of 1-based source channel index ->
|
|
104
|
+
CVBasic instrument (W/X/Y/Z) forced on that channel,
|
|
105
|
+
overriding the instrument mapping.
|
|
106
|
+
playlist: if True, deduplicate repeated stretches into
|
|
107
|
+
reusable blocks driven by a playlist.
|
|
108
|
+
min_block: shortest repeated stretch worth reusing, in steps.
|
|
109
|
+
fuse: playlist indices after which the junction is removed.
|
|
110
|
+
pack: if True, emit raw three-byte rows instead of MUSIC
|
|
111
|
+
lines; ignored (with a message) when the song has
|
|
112
|
+
percussion, which needs the fourth column.
|
|
89
113
|
"""
|
|
90
114
|
song = self._song
|
|
91
|
-
|
|
115
|
+
|
|
116
|
+
if pack and song.drum_instruments:
|
|
117
|
+
logger.info(
|
|
118
|
+
"Packing disabled: it drops the percussion column, and this "
|
|
119
|
+
"song uses percussion."
|
|
120
|
+
)
|
|
121
|
+
pack = False
|
|
92
122
|
|
|
93
123
|
conv_args: dict = dict(
|
|
94
124
|
data_byte=data_byte,
|
|
@@ -97,13 +127,30 @@ class BaseImport:
|
|
|
97
127
|
drum_length=drum_length,
|
|
98
128
|
exclude_channels=exclude_channels,
|
|
99
129
|
transpose=self._transpose,
|
|
130
|
+
map_instruments=map_instruments,
|
|
131
|
+
force_suffixes=force_suffixes,
|
|
132
|
+
pack=pack,
|
|
100
133
|
)
|
|
101
134
|
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
song
|
|
105
|
-
|
|
135
|
+
if playlist:
|
|
136
|
+
result = convert_playlist(
|
|
137
|
+
song, label=label, stop=stop,
|
|
138
|
+
min_block=min_block, fuse=fuse,
|
|
139
|
+
include_name=output_file.name, **conv_args,
|
|
106
140
|
)
|
|
141
|
+
if result is not None:
|
|
142
|
+
source, speeds = result
|
|
143
|
+
output_file.write_text(source, encoding="utf-8")
|
|
144
|
+
logger.info("Output written to: %s", output_file)
|
|
145
|
+
self._log_song_info(speeds)
|
|
146
|
+
return
|
|
147
|
+
# convert_playlist explained why; fall through to the
|
|
148
|
+
# classic single-block output.
|
|
149
|
+
|
|
150
|
+
do_split = song.loop_start_position > 0 and not stop
|
|
151
|
+
|
|
152
|
+
if do_split:
|
|
153
|
+
song_end = song.playback_end
|
|
107
154
|
source_intro, speeds_intro = convert(
|
|
108
155
|
song, label=label, stop=True,
|
|
109
156
|
pos_start=0, pos_end=song.loop_start_position,
|
|
@@ -136,20 +183,33 @@ class BaseImport:
|
|
|
136
183
|
)
|
|
137
184
|
output_file.write_text(source, encoding="utf-8")
|
|
138
185
|
|
|
139
|
-
|
|
186
|
+
if pack:
|
|
187
|
+
# Every step is a DATA BYTE row, minus the header and
|
|
188
|
+
# the terminator row.
|
|
189
|
+
steps = source.count("\tDATA BYTE $") - 2
|
|
190
|
+
else:
|
|
191
|
+
# Every step is a MUSIC row, minus the final STOP/REPEAT.
|
|
192
|
+
steps = source.count("\tMUSIC ") - 1
|
|
140
193
|
logger.info("Converted: %d patterns.", len(song.positions))
|
|
141
|
-
logger.info(
|
|
142
|
-
"DATA BYTE: %d -> MUSIC rows: %d", data_byte, music_lines
|
|
143
|
-
)
|
|
194
|
+
logger.info("DATA BYTE: %d -> MUSIC rows: %d", data_byte, steps)
|
|
144
195
|
logger.info("Output written to: %s", output_file)
|
|
145
196
|
|
|
197
|
+
self._log_song_info(speeds)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _log_song_info(self, speeds: set[int]) -> None:
|
|
201
|
+
"""Log the tracker speeds and the percussion instruments found.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
speeds: the tracker speeds encountered during conversion.
|
|
205
|
+
"""
|
|
146
206
|
logger.info(
|
|
147
207
|
"Speeds found (tracker frames/row): %s", sorted(speeds)
|
|
148
208
|
)
|
|
149
|
-
if
|
|
209
|
+
if self._song.drum_instruments:
|
|
150
210
|
logger.info(
|
|
151
211
|
"Percussion instruments detected: %s",
|
|
152
|
-
dict(
|
|
212
|
+
dict(self._song.drum_instruments),
|
|
153
213
|
)
|
|
154
214
|
else:
|
|
155
215
|
logger.info("No percussion instruments detected.")
|
|
@@ -33,3 +33,16 @@ class Song:
|
|
|
33
33
|
drum_instruments: dict[int, str] = field(default_factory=dict)
|
|
34
34
|
loop_start_position: int = 0
|
|
35
35
|
end_position: int = -1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def playback_end(self) -> int:
|
|
40
|
+
"""Exclusive index of the last position to play.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
end_position + 1 if set, otherwise the full positions length.
|
|
44
|
+
"""
|
|
45
|
+
if self.end_position >= 0:
|
|
46
|
+
return self.end_position + 1
|
|
47
|
+
|
|
48
|
+
return len(self.positions)
|
|
@@ -170,16 +170,6 @@ class TXTImport(BaseImport):
|
|
|
170
170
|
return song
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
def transpose(self, notes: int) -> None:
|
|
174
|
-
"""Set the semitone shift for all melodic notes on export.
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
notes: total semitones to add (positive = up, negative = down;
|
|
178
|
-
12 = one octave up).
|
|
179
|
-
"""
|
|
180
|
-
super().transpose(notes)
|
|
181
|
-
|
|
182
|
-
|
|
183
173
|
def _noise_to_drum_type(self, avg_noise: float) -> str:
|
|
184
174
|
"""Map the average noise period of an instrument to a CVBasic drum type.
|
|
185
175
|
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Arkos Tracker text format (V1.0) to CVBasic music converter — CLI entry point.
|
|
2
|
+
|
|
3
|
+
Receives command-line arguments, orchestrates the importer and the CVBasic
|
|
4
|
+
exporter, and writes status messages to stdout at the end of execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from arkos2basic.arkostracker.txtimport import TXTImport
|
|
15
|
+
from arkos2basic.cvbasic import MAX_DATA_BYTE
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Convert an Arkos Tracker (.txt) file to CVBasic music.")
|
|
19
|
+
|
|
20
|
+
# Names accepted by --channel1/2/3, plus the CVBasic letters themselves.
|
|
21
|
+
CHANNEL_INSTRUMENTS: dict[str, str] = {
|
|
22
|
+
"piano": "W",
|
|
23
|
+
"clarinet": "X",
|
|
24
|
+
"flute": "Y",
|
|
25
|
+
"bass": "Z",
|
|
26
|
+
"w": "W",
|
|
27
|
+
"x": "X",
|
|
28
|
+
"y": "Y",
|
|
29
|
+
"z": "Z",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_channel_instrument(value: str, option: str) -> str | None:
|
|
34
|
+
"""Turn a --channelN value into the CVBasic instrument suffix.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
value: the raw option value, empty when the option was omitted.
|
|
38
|
+
option: the option name, used in the error message.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The suffix (W/X/Y/Z), or None if the option was not given.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
typer.Exit: if the value names no known instrument.
|
|
45
|
+
"""
|
|
46
|
+
name = value.strip().lower()
|
|
47
|
+
if not name:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
suffix = CHANNEL_INSTRUMENTS.get(name)
|
|
51
|
+
if suffix is None:
|
|
52
|
+
typer.echo(
|
|
53
|
+
f"Error: {option} only accepts piano, clarinet, flute or bass "
|
|
54
|
+
f"(got: '{value}')",
|
|
55
|
+
err=True,
|
|
56
|
+
)
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
|
|
59
|
+
return suffix
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command()
|
|
63
|
+
def main(
|
|
64
|
+
input_file: Annotated[
|
|
65
|
+
Path,
|
|
66
|
+
typer.Argument(
|
|
67
|
+
help="Input Arkos Tracker file",
|
|
68
|
+
exists=True,
|
|
69
|
+
dir_okay=False,
|
|
70
|
+
readable=True,
|
|
71
|
+
),
|
|
72
|
+
],
|
|
73
|
+
output_file: Annotated[
|
|
74
|
+
Path,
|
|
75
|
+
typer.Argument(help="Output .bas file"),
|
|
76
|
+
],
|
|
77
|
+
octaves: Annotated[
|
|
78
|
+
int,
|
|
79
|
+
typer.Option(
|
|
80
|
+
help="Octave delta relative to base +1 "
|
|
81
|
+
"(e.g. --octaves 1 → +2 octaves, --octaves -1 → 0)"
|
|
82
|
+
),
|
|
83
|
+
] = 0,
|
|
84
|
+
data_byte: Annotated[
|
|
85
|
+
int,
|
|
86
|
+
typer.Option(
|
|
87
|
+
help="Frames per MUSIC step: lower = more faithful but more rows "
|
|
88
|
+
"(1 = exact timing)"
|
|
89
|
+
),
|
|
90
|
+
] = 2,
|
|
91
|
+
length_scale: Annotated[
|
|
92
|
+
float,
|
|
93
|
+
typer.Option(
|
|
94
|
+
"--length",
|
|
95
|
+
help="Delta from base 3.0 for note sounding duration "
|
|
96
|
+
"(e.g. --length 1 → 4.0, --length -1 → 2.0)",
|
|
97
|
+
),
|
|
98
|
+
] = 0.0,
|
|
99
|
+
invert: Annotated[
|
|
100
|
+
bool,
|
|
101
|
+
typer.Option(
|
|
102
|
+
help="Invert the scale: high forceInstrumentSpeed = shorter note"
|
|
103
|
+
),
|
|
104
|
+
] = False,
|
|
105
|
+
label: Annotated[
|
|
106
|
+
str,
|
|
107
|
+
typer.Option(help="Label for the music block in the output source"),
|
|
108
|
+
] = "musica",
|
|
109
|
+
stop: Annotated[
|
|
110
|
+
bool,
|
|
111
|
+
typer.Option("--stop", help="End with MUSIC STOP instead of MUSIC REPEAT"),
|
|
112
|
+
] = False,
|
|
113
|
+
drum_length: Annotated[
|
|
114
|
+
int,
|
|
115
|
+
typer.Option(
|
|
116
|
+
help="Delta from base 2 steps per percussion hit "
|
|
117
|
+
"(e.g. --drum-length 1 → 3, --drum-length -1 → 1)",
|
|
118
|
+
),
|
|
119
|
+
] = 0,
|
|
120
|
+
exclude_channels: Annotated[
|
|
121
|
+
str,
|
|
122
|
+
typer.Option(
|
|
123
|
+
"--exclude-channels",
|
|
124
|
+
help="Source channels to exclude, comma-separated "
|
|
125
|
+
"(1-3, e.g. '2' or '2,3'). Remaining channels pack left.",
|
|
126
|
+
),
|
|
127
|
+
] = "",
|
|
128
|
+
transpose: Annotated[
|
|
129
|
+
int,
|
|
130
|
+
typer.Option(
|
|
131
|
+
help="Chromatic semitones to add to all notes "
|
|
132
|
+
"(positive = up, negative = down; 12 = +1 octave).",
|
|
133
|
+
),
|
|
134
|
+
] = 0,
|
|
135
|
+
no_instruments: Annotated[
|
|
136
|
+
bool,
|
|
137
|
+
typer.Option(
|
|
138
|
+
"--no-instruments",
|
|
139
|
+
help="Do not map Arkos instruments 1-3 to CVBasic W/X/Y; "
|
|
140
|
+
"all melodic notes use the default piano.",
|
|
141
|
+
),
|
|
142
|
+
] = False,
|
|
143
|
+
channel1: Annotated[
|
|
144
|
+
str,
|
|
145
|
+
typer.Option(
|
|
146
|
+
"--channel1",
|
|
147
|
+
help="Force one CVBasic instrument on source channel 1: "
|
|
148
|
+
"piano, clarinet, flute or bass. Overrides the Arkos "
|
|
149
|
+
"instrument mapping; the octave the player adds to "
|
|
150
|
+
"clarinet and bass is compensated automatically.",
|
|
151
|
+
),
|
|
152
|
+
] = "",
|
|
153
|
+
channel2: Annotated[
|
|
154
|
+
str,
|
|
155
|
+
typer.Option("--channel2", help="Same as --channel1, for source channel 2."),
|
|
156
|
+
] = "",
|
|
157
|
+
channel3: Annotated[
|
|
158
|
+
str,
|
|
159
|
+
typer.Option("--channel3", help="Same as --channel1, for source channel 3."),
|
|
160
|
+
] = "",
|
|
161
|
+
playlist: Annotated[
|
|
162
|
+
bool,
|
|
163
|
+
typer.Option(
|
|
164
|
+
"--playlist",
|
|
165
|
+
help="Reuse repeated stretches: emit one block per unique "
|
|
166
|
+
"stretch plus a playlist and the BASIC glue that chains "
|
|
167
|
+
"them. Replaces the intro/loop split.",
|
|
168
|
+
),
|
|
169
|
+
] = False,
|
|
170
|
+
min_block: Annotated[
|
|
171
|
+
int,
|
|
172
|
+
typer.Option(
|
|
173
|
+
help="Shortest repeated stretch worth reusing, in MUSIC steps "
|
|
174
|
+
"(--playlist only). Higher = fewer junctions.",
|
|
175
|
+
),
|
|
176
|
+
] = 32,
|
|
177
|
+
fuse: Annotated[
|
|
178
|
+
str,
|
|
179
|
+
typer.Option(
|
|
180
|
+
"--fuse",
|
|
181
|
+
help="Playlist entries after which the junction is removed, "
|
|
182
|
+
"comma-separated (e.g. '2,5'). Use when a junction is "
|
|
183
|
+
"audible; the conversion log numbers the entries.",
|
|
184
|
+
),
|
|
185
|
+
] = "",
|
|
186
|
+
pack: Annotated[
|
|
187
|
+
bool,
|
|
188
|
+
typer.Option(
|
|
189
|
+
"--pack3",
|
|
190
|
+
help="Emit raw 3-byte rows instead of MUSIC lines, saving 25% "
|
|
191
|
+
"of the music ROM. Songs without percussion only.",
|
|
192
|
+
),
|
|
193
|
+
] = False,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Command-line entry point."""
|
|
196
|
+
if data_byte < 1:
|
|
197
|
+
typer.echo("Error: --data-byte must be at least 1", err=True)
|
|
198
|
+
raise typer.Exit(1)
|
|
199
|
+
|
|
200
|
+
if data_byte > MAX_DATA_BYTE:
|
|
201
|
+
typer.echo(
|
|
202
|
+
f"Error: --data-byte must be at most {MAX_DATA_BYTE} "
|
|
203
|
+
f"(the player keeps it in 6 bits)",
|
|
204
|
+
err=True,
|
|
205
|
+
)
|
|
206
|
+
raise typer.Exit(1)
|
|
207
|
+
|
|
208
|
+
if min_block < 1:
|
|
209
|
+
typer.echo("Error: --min-block must be at least 1", err=True)
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
|
|
212
|
+
fuse_set: set[int] = set()
|
|
213
|
+
for part in fuse.split(","):
|
|
214
|
+
part = part.strip()
|
|
215
|
+
if not part:
|
|
216
|
+
continue
|
|
217
|
+
try:
|
|
218
|
+
fuse_set.add(int(part))
|
|
219
|
+
except ValueError:
|
|
220
|
+
typer.echo(
|
|
221
|
+
f"Error: --fuse only accepts playlist entry numbers "
|
|
222
|
+
f"(got: '{part}')",
|
|
223
|
+
err=True,
|
|
224
|
+
)
|
|
225
|
+
raise typer.Exit(1)
|
|
226
|
+
|
|
227
|
+
excluded: set[int] = set()
|
|
228
|
+
for part in exclude_channels.split(","):
|
|
229
|
+
part = part.strip()
|
|
230
|
+
if not part:
|
|
231
|
+
continue
|
|
232
|
+
try:
|
|
233
|
+
val = int(part)
|
|
234
|
+
except ValueError:
|
|
235
|
+
typer.echo(
|
|
236
|
+
f"Error: --exclude-channels only accepts values 1, 2, 3 "
|
|
237
|
+
f"(got: '{part}')",
|
|
238
|
+
err=True,
|
|
239
|
+
)
|
|
240
|
+
raise typer.Exit(1)
|
|
241
|
+
if val not in (1, 2, 3):
|
|
242
|
+
typer.echo(
|
|
243
|
+
f"Error: --exclude-channels only accepts values 1, 2, 3 "
|
|
244
|
+
f"(got: {val})",
|
|
245
|
+
err=True,
|
|
246
|
+
)
|
|
247
|
+
raise typer.Exit(1)
|
|
248
|
+
excluded.add(val)
|
|
249
|
+
|
|
250
|
+
forced: dict[int, str] = {}
|
|
251
|
+
for index, value in ((1, channel1), (2, channel2), (3, channel3)):
|
|
252
|
+
suffix = parse_channel_instrument(value, f"--channel{index}")
|
|
253
|
+
if suffix is not None:
|
|
254
|
+
forced[index] = suffix
|
|
255
|
+
|
|
256
|
+
for index in sorted(set(forced) & excluded):
|
|
257
|
+
typer.echo(
|
|
258
|
+
f"Warning: --channel{index} has no effect, "
|
|
259
|
+
f"channel {index} is excluded."
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
importer = TXTImport(input_file=input_file)
|
|
263
|
+
|
|
264
|
+
# --octaves N contributes (1 + N) * 12 semitones (base 1 octave + delta).
|
|
265
|
+
# --transpose M adds M fine semitones on top.
|
|
266
|
+
effective_transpose = (1 + octaves) * 12 + transpose
|
|
267
|
+
importer.transpose(effective_transpose)
|
|
268
|
+
|
|
269
|
+
log_handler = logging.StreamHandler(sys.stdout)
|
|
270
|
+
log_handler.setFormatter(logging.Formatter("%(message)s"))
|
|
271
|
+
pkg_logger = logging.getLogger("arkos2basic")
|
|
272
|
+
pkg_logger.addHandler(log_handler)
|
|
273
|
+
pkg_logger.setLevel(logging.INFO)
|
|
274
|
+
|
|
275
|
+
exclude_set: set[int] | None = None
|
|
276
|
+
if excluded:
|
|
277
|
+
exclude_set = excluded
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
importer.export(
|
|
281
|
+
output_file=output_file,
|
|
282
|
+
label=label,
|
|
283
|
+
data_byte=data_byte,
|
|
284
|
+
length_scale=3.0 + length_scale,
|
|
285
|
+
invert_speed=invert,
|
|
286
|
+
stop=stop,
|
|
287
|
+
drum_length=2 + drum_length,
|
|
288
|
+
exclude_channels=exclude_set,
|
|
289
|
+
map_instruments=not no_instruments,
|
|
290
|
+
force_suffixes=forced or None,
|
|
291
|
+
playlist=playlist,
|
|
292
|
+
min_block=min_block,
|
|
293
|
+
fuse=fuse_set,
|
|
294
|
+
pack=pack,
|
|
295
|
+
)
|
|
296
|
+
finally:
|
|
297
|
+
pkg_logger.removeHandler(log_handler)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
app()
|