mms-lib 0.2.1__tar.gz → 0.3.0__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.
Files changed (42) hide show
  1. {mms_lib-0.2.1 → mms_lib-0.3.0}/PKG-INFO +1 -1
  2. {mms_lib-0.2.1 → mms_lib-0.3.0}/pyproject.toml +1 -1
  3. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/__init__.py +1 -1
  4. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/core.py +21 -12
  5. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/dataclass.py +1 -1
  6. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/midi_reader.py +1 -1
  7. mms_lib-0.3.0/src/mms/nbs_reader.py +63 -0
  8. mms_lib-0.3.0/src/mms/pynbs/__init__.py +4 -0
  9. mms_lib-0.3.0/src/mms/pynbs/file.py +306 -0
  10. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/tools.py +2 -0
  11. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms_lib.egg-info/PKG-INFO +1 -1
  12. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms_lib.egg-info/SOURCES.txt +3 -0
  13. {mms_lib-0.2.1 → mms_lib-0.3.0}/LICENSE +0 -0
  14. {mms_lib-0.2.1 → mms_lib-0.3.0}/README.md +0 -0
  15. {mms_lib-0.2.1 → mms_lib-0.3.0}/setup.cfg +0 -0
  16. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/assets/text/features.json +0 -0
  17. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/assets/text/mapping.json +0 -0
  18. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/assets/text/profile.json +0 -0
  19. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/assets/text/settings.json +0 -0
  20. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/cli.py +0 -0
  21. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/evaltools.py +0 -0
  22. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/__init__.py +0 -0
  23. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/frozen.py +0 -0
  24. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/messages/__init__.py +0 -0
  25. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/messages/checks.py +0 -0
  26. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/messages/decode.py +0 -0
  27. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/messages/encode.py +0 -0
  28. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/messages/messages.py +0 -0
  29. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/messages/specs.py +0 -0
  30. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/messages/strings.py +0 -0
  31. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/midifiles/__init__.py +0 -0
  32. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/midifiles/meta.py +0 -0
  33. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/midifiles/midifiles.py +0 -0
  34. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/midifiles/tracks.py +0 -0
  35. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/midifiles/units.py +0 -0
  36. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/parser.py +0 -0
  37. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/tokenizer.py +0 -0
  38. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms/mido/version.py +0 -0
  39. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms_lib.egg-info/dependency_links.txt +0 -0
  40. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms_lib.egg-info/entry_points.txt +0 -0
  41. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms_lib.egg-info/requires.txt +0 -0
  42. {mms_lib-0.2.1 → mms_lib-0.3.0}/src/mms_lib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mms-lib
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Extract from MIDI-MCSTRUCTURE NEXT Project
5
5
  Requires-Python: >=3.14
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mms-lib"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "Extract from MIDI-MCSTRUCTURE NEXT Project"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -2,4 +2,4 @@ from .core import init, read_midi, convertor, profile
2
2
  from .dataclass import Settings, Note, Lyrics
3
3
 
4
4
  __all__ = ["init", "read_midi", "convertor", "profile", "Note", "Lyrics", "Settings"]
5
- __version__ = "0.2.1"
5
+ __version__ = "0.3.0"
@@ -1,8 +1,9 @@
1
1
  import os
2
2
  from . import evaltools
3
3
  from math import ceil, isclose
4
- from .tools import round_int, round_45, is_number, limit
4
+ from .tools import round_int, round_45, is_number, limit, is_midi
5
5
  from .dataclass import LyricsList, Note, Lyrics, Eval, AverageNumber, Profiles, Settings
6
+ from .nbs_reader import NBSReader
6
7
  from .midi_reader import MIDIReader
7
8
  from importlib.resources import files
8
9
 
@@ -41,7 +42,7 @@ def load_lrc(lines: list[str], time_per_tick: int = 50):
41
42
  elif tag_type == "offset":
42
43
  offset = int(value)
43
44
 
44
- def read_midi(midi_reader: MIDIReader, platform_profile: dict, lyrics_data: list[str] | None = None) -> dict[int, set[Note | Lyrics]]:
45
+ def read_midi(midi_reader: MIDIReader | NBSReader, platform_profile: dict, lyrics_data: list[str] | None = None) -> dict[int, set[Note | Lyrics]]:
45
46
  if profile.settings.max_time_error:
46
47
  tempo = profile.settings.time_per_tick
47
48
  min_error = float("INF")
@@ -105,13 +106,18 @@ def read_midi(midi_reader: MIDIReader, platform_profile: dict, lyrics_data: list
105
106
  # 歌词数据处理
106
107
  if lyrics_data is not None:
107
108
  lyrics_buffer.clear()
109
+
108
110
  for k, i in load_lrc(lyrics_data, best_tempo):
109
111
  if k in lyrics_buffer:
110
112
  lyrics_buffer[k] += " " + i
111
113
  else:
112
114
  lyrics_buffer[k] = i
113
115
 
114
- # 渲染歌词字幕
116
+ render_lyrics(lyrics_buffer, result)
117
+
118
+ return result
119
+
120
+ def render_lyrics(lyrics_buffer: dict[int, str], music_data: dict[int, set[Note | Lyrics]]) -> None:
115
121
  if lyrics_buffer:
116
122
  last_l = None
117
123
  for k, l in LyricsList(lyrics_buffer, profile.settings.smooth_progress, profile.settings.auto_join_lyrics):
@@ -120,25 +126,28 @@ def read_midi(midi_reader: MIDIReader, platform_profile: dict, lyrics_data: list
120
126
  continue
121
127
 
122
128
  # 将歌词数据合并到结果中
123
- if k not in result:
124
- result[k] = set()
129
+ if k not in music_data:
130
+ music_data[k] = set()
125
131
 
126
- result[k].add(l)
132
+ music_data[k].add(l)
127
133
  last_l = l
128
134
 
129
- return result
130
-
131
135
  def convertor(file_data: bytes, platform: str, lyrics_data: list[str] | None = None):
132
136
  platform_profile = profile.profile[platform]
133
137
 
134
- midi_data = read_midi(MIDIReader(file_data), platform_profile, lyrics_data)
138
+ if is_midi(file_data):
139
+ file_data = MIDIReader(file_data)
140
+ else:
141
+ file_data = NBSReader(file_data)
142
+
143
+ music_data = read_midi(file_data, platform_profile, lyrics_data)
135
144
 
136
145
  if profile.settings.skip:
137
- time_offset = min(midi_data.keys())
146
+ time_offset = min(music_data.keys())
138
147
  else:
139
148
  time_offset = 0
140
149
 
141
- return cmd_convertor(platform_profile, time_offset, midi_data)
150
+ return cmd_convertor(platform_profile, time_offset, music_data)
142
151
 
143
152
  def get_notes(midi_file: MIDIReader, platform_profile: dict, tempo: float | int):
144
153
  if not profile.settings.enable_extension:
@@ -240,7 +249,7 @@ def cmd_convertor(platform_profile: dict, start_time: int, note_data: dict[int,
240
249
  cmd = cmd.replace("{ADDRESS}", str(profile.settings.id))
241
250
  cmd = cmd.replace("{ID_SCOREBOARD}", profile.settings.id_scoreboard_name)
242
251
  cmd = cmd.replace("{TIMER_SCOREBOARD}", profile.settings.timer_scoreboard_name)
243
- yield time, cmd
252
+ yield time, cmd.lstrip("/")
244
253
 
245
254
  def cmd_generator(platform_profile: dict, start_time: int, note_data: dict[int, set[Note | Lyrics]]):
246
255
  raw_cmd: str = platform_profile["command"][profile.settings.command_type][0]
@@ -237,7 +237,7 @@ class NoteData:
237
237
  "type"
238
238
  )
239
239
 
240
- def __init__(self, time: float, pitch: float, program: tuple[int, int], panning: tuple[float], velocity: float):
240
+ def __init__(self, time: float, pitch: float, program: tuple[int, int], panning: tuple[float, float], velocity: float):
241
241
  self.type = "note"
242
242
  self.time = time
243
243
  self.pitch = pitch
@@ -211,7 +211,7 @@ class MIDIReader:
211
211
  elif message.type == "note_off" or (message.type == "note_on" and message.velocity == 0):
212
212
  data = stack.get(channel, message.note)
213
213
 
214
- # 将音符时间转为游戏tick时间并返回结果
214
+ # 将音符时间转为绝对时间并返回结果,时间单位为毫秒
215
215
  if data is not None: yield tempo_info.compute_time(time), data
216
216
 
217
217
  for data in stack: yield data.time + 0.00001, data
@@ -0,0 +1,63 @@
1
+ import io
2
+ import math
3
+ from . import pynbs
4
+ from .tools import round_45
5
+ from .dataclass import NoteData
6
+
7
+ NBS_TO_GM_MAPPING = {
8
+ 0: 0,
9
+ 1: 43,
10
+ 2: 116,
11
+ 3: 118,
12
+ 4: 115,
13
+ 5: 24,
14
+ 6: 73,
15
+ 7: 14,
16
+ 8: 8,
17
+ 9: 13,
18
+ 10: 9,
19
+ 11: 113,
20
+ 12: 111,
21
+ 13: 80,
22
+ 14: 105,
23
+ 15: 98,
24
+ }
25
+
26
+ class NBSReader:
27
+ def __init__(self, data: bytes):
28
+ self.__nbs_file = pynbs.Parser(io.BytesIO(data)).read_file()
29
+ self.__nodes_cache = []
30
+ self.__instruments_mapping = {}
31
+
32
+ def get_time_accuracy(self, _) -> float:
33
+ return 0.0
34
+
35
+ def override_mapping(self, mapping: dict[int, dict[int, int]]) -> None:
36
+ self.__instruments_mapping = mapping
37
+
38
+ def __iter__(self):
39
+ volume_info = {}
40
+ panning_info = {}
41
+ for layer in self.__nbs_file.layers:
42
+ volume_info[layer.id] = layer.volume
43
+ panning_info[layer.id] = layer.panning
44
+
45
+ for tick, notes in self.__nbs_file:
46
+ for note in notes:
47
+ if panning_info[note.layer] == 0:
48
+ panning = note.panning
49
+ else:
50
+ panning = (note.panning + panning_info[note.layer]) / 2
51
+
52
+ radian = math.radians((panning + 100) * 0.9)
53
+
54
+ note_data = NoteData(
55
+ tick * 50,
56
+ note.key + 21 + note.pitch / 100,
57
+ (NBS_TO_GM_MAPPING.get(note.instrument, 0), ) * 2,
58
+ (round_45(math.cos(radian), 2), round_45(math.sin(radian), 2)),
59
+ note.velocity * volume_info[note.layer]
60
+ )
61
+
62
+ yield tick * 50 + 0.00001, note_data
63
+
@@ -0,0 +1,4 @@
1
+ __version__ = "1.0.0-beta.0"
2
+
3
+
4
+ from .file import *
@@ -0,0 +1,306 @@
1
+ __all__ = [
2
+ "read",
3
+ "new_file",
4
+ "Parser",
5
+ "Writer",
6
+ "File",
7
+ "Header",
8
+ "Note",
9
+ "Layer",
10
+ "Instrument",
11
+ ]
12
+
13
+
14
+ from dataclasses import dataclass
15
+ from struct import Struct
16
+
17
+ CURRENT_NBS_VERSION = 5
18
+
19
+ BYTE = Struct("<B")
20
+ SHORT = Struct("<H")
21
+ SSHORT = Struct("<h")
22
+ INT = Struct("<I")
23
+
24
+
25
+ @dataclass
26
+ class Instrument:
27
+ id: int
28
+ name: str
29
+ file: str
30
+ pitch: int = 45
31
+ press_key: bool = True
32
+
33
+
34
+ @dataclass
35
+ class Note:
36
+ tick: int
37
+ layer: int
38
+ instrument: int
39
+ key: int
40
+ velocity: int = 100
41
+ panning: int = 0
42
+ pitch: int = 0
43
+
44
+
45
+ @dataclass
46
+ class Layer:
47
+ id: int
48
+ name: str = ""
49
+ lock: bool = False
50
+ volume: int = 100
51
+ panning: int = 0
52
+
53
+
54
+ def read(filename):
55
+ with open(filename, "rb") as fileobj:
56
+ return Parser(fileobj).read_file()
57
+
58
+
59
+ def new_file(**header):
60
+ return File(Header(**header), [], [Layer(0, "", False, 100, 0)], [])
61
+
62
+
63
+ @dataclass
64
+ class Header:
65
+ version: int = CURRENT_NBS_VERSION
66
+ default_instruments: int = 16
67
+ song_length: int = 0
68
+ song_layers: int = 0
69
+ song_name: str = ""
70
+ song_author: str = ""
71
+ original_author: str = ""
72
+ description: str = ""
73
+ tempo: float = 10.0
74
+ auto_save: bool = False
75
+ auto_save_duration: int = 10
76
+ time_signature: int = 4
77
+ minutes_spent: int = 0
78
+ left_clicks: int = 0
79
+ right_clicks: int = 0
80
+ blocks_added: int = 0
81
+ blocks_removed: int = 0
82
+ song_origin: str = ""
83
+ loop: bool = False
84
+ max_loop_count: int = 0
85
+ loop_start: int = 0
86
+
87
+
88
+ class File:
89
+ def __init__(self, header, notes, layers, instruments):
90
+ self.header = header
91
+ self.notes = notes
92
+ self.layers = layers
93
+ self.instruments = instruments
94
+
95
+ def update_header(self, version):
96
+ self.header.version = version
97
+ if self.notes:
98
+ self.header.song_length = self.notes[-1].tick
99
+ self.header.song_layers = len(self.layers)
100
+
101
+ def save(self, filename, version=CURRENT_NBS_VERSION):
102
+ self.update_header(version)
103
+ with open(filename, "wb") as fileobj:
104
+ Writer(fileobj).encode_file(self, version)
105
+
106
+ def __iter__(self):
107
+ if not self.notes:
108
+ return
109
+ chord = []
110
+ current_tick = self.notes[0].tick
111
+
112
+ for note in sorted(self.notes, key=lambda n: n.tick):
113
+ if note.tick == current_tick:
114
+ chord.append(note)
115
+ else:
116
+ chord.sort(key=lambda n: n.layer)
117
+ yield current_tick, chord
118
+ current_tick, chord = note.tick, [note]
119
+ yield current_tick, chord
120
+
121
+
122
+ class Parser:
123
+ def __init__(self, fileobj):
124
+ self.fileobj = fileobj
125
+
126
+ def read_file(self):
127
+ header = self.parse_header()
128
+ version = header.version
129
+ return File(
130
+ header,
131
+ list(self.parse_notes(version)),
132
+ list(self.parse_layers(header.song_layers, version)),
133
+ list(self.parse_instruments(version)),
134
+ )
135
+
136
+ def read_numeric(self, fmt):
137
+ return fmt.unpack(self.fileobj.read(fmt.size))[0]
138
+
139
+ def read_string(self):
140
+ length = self.read_numeric(INT)
141
+ return self.fileobj.read(length).decode(encoding="cp1252")
142
+
143
+ def jump(self):
144
+ value = -1
145
+ while True:
146
+ jump = self.read_numeric(SHORT)
147
+ if not jump:
148
+ break
149
+ value += jump
150
+ yield value
151
+
152
+ def parse_header(self):
153
+ song_length = self.read_numeric(SHORT)
154
+ if song_length == 0:
155
+ # A song length of 0 indicates the Open Note Block Studio format
156
+ version = self.read_numeric(BYTE)
157
+ else:
158
+ version = 0
159
+
160
+ return Header(
161
+ version=version,
162
+ default_instruments=self.read_numeric(BYTE) if version > 0 else 10,
163
+ song_length=self.read_numeric(SHORT) if version >= 3 else song_length,
164
+ song_layers=self.read_numeric(SHORT),
165
+ song_name=self.read_string(),
166
+ song_author=self.read_string(),
167
+ original_author=self.read_string(),
168
+ description=self.read_string(),
169
+ tempo=self.read_numeric(SHORT) / 100.0,
170
+ auto_save=self.read_numeric(BYTE) == 1,
171
+ auto_save_duration=self.read_numeric(BYTE),
172
+ time_signature=self.read_numeric(BYTE),
173
+ minutes_spent=self.read_numeric(INT),
174
+ left_clicks=self.read_numeric(INT),
175
+ right_clicks=self.read_numeric(INT),
176
+ blocks_added=self.read_numeric(INT),
177
+ blocks_removed=self.read_numeric(INT),
178
+ song_origin=self.read_string(),
179
+ loop=self.read_numeric(BYTE) == 1 if version >= 4 else False,
180
+ max_loop_count=self.read_numeric(BYTE) if version >= 4 else 0,
181
+ loop_start=self.read_numeric(SHORT) if version >= 4 else 0,
182
+ )
183
+
184
+ def parse_notes(self, version):
185
+ for current_tick in self.jump():
186
+ for current_layer in self.jump():
187
+ instrument = self.read_numeric(BYTE)
188
+ key = self.read_numeric(BYTE)
189
+ velocity = self.read_numeric(BYTE) if version >= 4 else 100
190
+ panning = self.read_numeric(BYTE) - 100 if version >= 4 else 0
191
+ pitch = self.read_numeric(SSHORT) if version >= 4 else 0
192
+ yield Note(
193
+ current_tick,
194
+ current_layer,
195
+ instrument,
196
+ key,
197
+ velocity,
198
+ panning,
199
+ pitch,
200
+ )
201
+
202
+ def parse_layers(self, layers_count, version):
203
+ for i in range(layers_count):
204
+ name = self.read_string()
205
+ lock = self.read_numeric(BYTE) == 1 if version >= 4 else False
206
+ volume = self.read_numeric(BYTE)
207
+ panning = self.read_numeric(BYTE) - 100 if version >= 2 else 0
208
+ yield Layer(i, name, lock, volume, panning)
209
+
210
+ def parse_instruments(self, version):
211
+ for i in range(self.read_numeric(BYTE)):
212
+ name = self.read_string()
213
+ sound_file = self.read_string()
214
+ pitch = self.read_numeric(BYTE)
215
+ press_key = self.read_numeric(BYTE) == 1
216
+ yield Instrument(i, name, sound_file, pitch, press_key)
217
+
218
+
219
+ class Writer:
220
+ def __init__(self, fileobj):
221
+ self.fileobj = fileobj
222
+
223
+ def encode_file(self, nbs_file, version):
224
+ self.write_header(nbs_file, version)
225
+ self.write_notes(nbs_file, version)
226
+ self.write_layers(nbs_file, version)
227
+ self.write_instruments(nbs_file, version)
228
+
229
+ def encode_numeric(self, fmt, value):
230
+ self.fileobj.write(fmt.pack(value))
231
+
232
+ def encode_string(self, value):
233
+ self.encode_numeric(INT, len(value))
234
+ self.fileobj.write(value.encode(encoding="cp1252"))
235
+
236
+ def write_header(self, nbs_file, version):
237
+ header = nbs_file.header
238
+
239
+ if version > 0:
240
+ self.encode_numeric(SHORT, 0)
241
+ self.encode_numeric(BYTE, version)
242
+ self.encode_numeric(BYTE, header.default_instruments)
243
+ else:
244
+ self.encode_numeric(SHORT, header.song_length)
245
+ if version >= 3:
246
+ self.encode_numeric(SHORT, header.song_length)
247
+ self.encode_numeric(SHORT, header.song_layers)
248
+ self.encode_string(header.song_name)
249
+ self.encode_string(header.song_author)
250
+ self.encode_string(header.original_author)
251
+ self.encode_string(header.description)
252
+
253
+ self.encode_numeric(SHORT, int(header.tempo * 100))
254
+ self.encode_numeric(BYTE, int(header.auto_save))
255
+ self.encode_numeric(BYTE, header.auto_save_duration)
256
+ self.encode_numeric(BYTE, header.time_signature)
257
+
258
+ self.encode_numeric(INT, header.minutes_spent)
259
+ self.encode_numeric(INT, header.left_clicks)
260
+ self.encode_numeric(INT, header.right_clicks)
261
+ self.encode_numeric(INT, header.blocks_added)
262
+ self.encode_numeric(INT, header.blocks_removed)
263
+ self.encode_string(header.song_origin)
264
+
265
+ if version >= 4:
266
+ self.encode_numeric(BYTE, int(header.loop))
267
+ self.encode_numeric(BYTE, header.max_loop_count)
268
+ self.encode_numeric(SHORT, header.loop_start)
269
+
270
+ def write_notes(self, nbs_file, version):
271
+ current_tick = -1
272
+
273
+ for tick, chord in nbs_file:
274
+ self.encode_numeric(SHORT, tick - current_tick)
275
+ current_tick = tick
276
+ current_layer = -1
277
+
278
+ for note in chord:
279
+ self.encode_numeric(SHORT, note.layer - current_layer)
280
+ current_layer = note.layer
281
+ self.encode_numeric(BYTE, note.instrument)
282
+ self.encode_numeric(BYTE, note.key)
283
+ if version >= 4:
284
+ self.encode_numeric(BYTE, note.velocity)
285
+ self.encode_numeric(BYTE, note.panning + 100)
286
+ self.encode_numeric(SSHORT, note.pitch)
287
+
288
+ self.encode_numeric(SHORT, 0)
289
+ self.encode_numeric(SHORT, 0)
290
+
291
+ def write_layers(self, nbs_file, version):
292
+ for layer in nbs_file.layers:
293
+ self.encode_string(layer.name)
294
+ if version >= 4:
295
+ self.encode_numeric(BYTE, int(layer.lock))
296
+ self.encode_numeric(BYTE, layer.volume)
297
+ if version >= 2:
298
+ self.encode_numeric(BYTE, layer.panning + 100)
299
+
300
+ def write_instruments(self, nbs_file, version):
301
+ self.encode_numeric(BYTE, len(nbs_file.instruments))
302
+ for instrument in nbs_file.instruments:
303
+ self.encode_string(instrument.name)
304
+ self.encode_string(instrument.file)
305
+ self.encode_numeric(BYTE, instrument.pitch)
306
+ self.encode_numeric(BYTE, int(instrument.press_key))
@@ -25,3 +25,5 @@ def is_number(text: str) -> bool:
25
25
  length = len(str)
26
26
  return all(text[i] in "0123456789" or (text[i] == "." and 1 < i + 1 < length) for i in range(length)) and text.count(".") <= 1
27
27
 
28
+ def is_midi(data: bytes) -> bool:
29
+ return data[:4] == b"MThd"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mms-lib
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Extract from MIDI-MCSTRUCTURE NEXT Project
5
5
  Requires-Python: >=3.14
6
6
  Description-Content-Type: text/markdown
@@ -7,6 +7,7 @@ src/mms/core.py
7
7
  src/mms/dataclass.py
8
8
  src/mms/evaltools.py
9
9
  src/mms/midi_reader.py
10
+ src/mms/nbs_reader.py
10
11
  src/mms/tools.py
11
12
  src/mms/assets/text/features.json
12
13
  src/mms/assets/text/mapping.json
@@ -29,6 +30,8 @@ src/mms/mido/midifiles/meta.py
29
30
  src/mms/mido/midifiles/midifiles.py
30
31
  src/mms/mido/midifiles/tracks.py
31
32
  src/mms/mido/midifiles/units.py
33
+ src/mms/pynbs/__init__.py
34
+ src/mms/pynbs/file.py
32
35
  src/mms_lib.egg-info/PKG-INFO
33
36
  src/mms_lib.egg-info/SOURCES.txt
34
37
  src/mms_lib.egg-info/dependency_links.txt
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes