Skip to content

Commit

Permalink
vxf: implement vxf generator
Browse files Browse the repository at this point in the history
  • Loading branch information
SoulMelody committed Jan 12, 2025
1 parent 3f206a1 commit dae858c
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 44 deletions.
4 changes: 1 addition & 3 deletions libresvip/plugins/mid/midi_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ class MidiGenerator:

@property
def tick_rate(self) -> float:
if self.options is not None:
return TICKS_IN_BEAT / self.options.ticks_per_beat
return 1
return TICKS_IN_BEAT / self.options.ticks_per_beat

def generate_project(self, project: Project) -> mido.MidiFile:
self.first_bar_length = round(
Expand Down
4 changes: 1 addition & 3 deletions libresvip/plugins/mid/midi_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ def __post_init__(self) -> None:

@property
def tick_rate(self) -> float:
if self.ticks_per_beat is not None:
return TICKS_IN_BEAT / self.ticks_per_beat
return 1
return TICKS_IN_BEAT / self.ticks_per_beat

def parse_project(self, mido_obj: mido.MidiFile) -> Project:
self.ticks_per_beat = mido_obj.ticks_per_beat
Expand Down
4 changes: 2 additions & 2 deletions libresvip/plugins/mid/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from pydantic import BaseModel, Field

from libresvip.core.constants import DEFAULT_BPM
from libresvip.core.constants import DEFAULT_BPM, TICKS_IN_BEAT
from libresvip.model.option_mixins import (
EnablePitchImportationMixin,
EnableVolumeImportationMixin,
Expand Down Expand Up @@ -74,7 +74,7 @@ class OutputOptions(BaseModel):
title=_("Transpose"),
)
ticks_per_beat: int = Field(
default=480,
default=TICKS_IN_BEAT,
title=_("Ticks per beat"),
description=_(
"Also known as parts per quarter, ticks per quarter note, the number of pulses per quarter note. This setting should not be changed unless you know what it is."
Expand Down
3 changes: 2 additions & 1 deletion libresvip/plugins/vsq/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pydantic import BaseModel, Field

from libresvip.core.constants import TICKS_IN_BEAT
from libresvip.model.option_mixins import EnablePitchImportationMixin
from libresvip.utils.translation import gettext_lazy as _

Expand All @@ -27,7 +28,7 @@ class InputOptions(EnablePitchImportationMixin, BaseModel):

class OutputOptions(BaseModel):
ticks_per_beat: int = Field(
default=480,
default=TICKS_IN_BEAT,
title=_("Ticks per beat"),
description=_(
"Also known as parts per quarter, ticks per quarter note, the number of pulses per quarter note. This setting should not be changed unless you know what it is."
Expand Down
4 changes: 1 addition & 3 deletions libresvip/plugins/vsq/vsq_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ class VsqGenerator:

@property
def tick_rate(self) -> float:
if self.options is not None:
return TICKS_IN_BEAT / self.options.ticks_per_beat
return 1
return TICKS_IN_BEAT / self.options.ticks_per_beat

def generate_project(self, project: Project) -> mido.MidiFile:
project = limit_bars(project, 4096)
Expand Down
4 changes: 1 addition & 3 deletions libresvip/plugins/vsq/vsq_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ class VsqParser:

@property
def tick_rate(self) -> float:
if self.ticks_per_beat is not None:
return TICKS_IN_BEAT / self.ticks_per_beat
return 1
return TICKS_IN_BEAT / self.ticks_per_beat

def parse_project(self, vsq_project: mido.MidiFile) -> Project:
self.ticks_per_beat = vsq_project.ticks_per_beat
Expand Down
6 changes: 3 additions & 3 deletions libresvip/plugins/vxf/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"tempo"
/ ExprAdapter(
Int32ub,
encoder=lambda obj, context: int(1705032600 * obj),
encoder=lambda obj, context: int(1705032600 / obj),
decoder=lambda obj, context: 1705032600 / obj,
),
"padding" / Const(b"\x00" * 8),
Expand Down Expand Up @@ -129,7 +129,7 @@
"note" / StartPitch,
Check(lambda ctx: 0 <= ctx.note <= 127),
"velocity" / Int16ub,
"data" / Int16ub,
"on_data" / Int16ub,
"type" / Computed("note_on"),
)

Expand All @@ -138,7 +138,7 @@
"note" / EndPitch,
Check(lambda ctx: 0 <= ctx.note <= 127),
"velocity" / Int16ub,
"data" / Int16ub,
"off_data" / Int16ub,
"type" / Computed("note_off"),
)

Expand Down
18 changes: 15 additions & 3 deletions libresvip/plugins/vxf/options.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field

from libresvip.core.constants import DEFAULT_BPM, TICKS_IN_BEAT
from libresvip.model.option_mixins import EnablePitchImportationMixin
from libresvip.utils.translation import gettext_lazy as _


class InputOptions(EnablePitchImportationMixin, BaseModel):
pass
default_bpm: float = Field(
default=DEFAULT_BPM,
title=_("Default BPM"),
description=_("Used when no BPM information is found in the MIDI file."),
)


class OutputOptions(BaseModel):
pass
ticks_per_beat: int = Field(
default=TICKS_IN_BEAT,
title=_("Ticks per beat"),
description=_(
"Also known as parts per quarter, ticks per quarter note, the number of pulses per quarter note. This setting should not be changed unless you know what it is."
),
)
176 changes: 172 additions & 4 deletions libresvip/plugins/vxf/vx_beta_generator.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,182 @@
import dataclasses
import operator
from typing import Any, Optional

from libresvip.model.base import Project
import more_itertools

from libresvip.core.constants import TICKS_IN_BEAT
from libresvip.core.time_sync import TimeSynchronizer
from libresvip.model.base import Project, SingingTrack, SongTempo, TimeSignature, Track

from .model import VxFile
from .options import OutputOptions

Container = dict[str, Any]


@dataclasses.dataclass
class VxBetaGenerator:
options: OutputOptions
synchronizer: TimeSynchronizer = dataclasses.field(init=False)
first_bar_length: int = dataclasses.field(init=False)

@property
def tick_rate(self) -> float:
return TICKS_IN_BEAT / self.options.ticks_per_beat

def generate_project(self, project: Project) -> Container:
self.first_bar_length = round(
project.time_signature_list[0].bar_length(self.options.ticks_per_beat)
)
self.synchronizer = TimeSynchronizer(project.song_tempo_list)
master_track_events = []
master_track_events.extend(self.generate_tempos(project.song_tempo_list))
master_track_events.extend(self.generate_time_signatures(project.time_signature_list))
if tracks := self.generate_tracks(project.track_list):
tracks[0]["events"].extend(master_track_events)
self._convert_cumulative_to_delta(tracks)
return {
"ticks_per_beat": self.options.ticks_per_beat,
"tracks": tracks,
}

@staticmethod
def _convert_cumulative_to_delta(tracks: list[Container]) -> None:
for track in tracks:
tick = 0
track["events"] = sorted(track["events"], key=operator.itemgetter("time"))
for event in track["events"]:
tick, event["time"] = event["time"], event["time"] - tick

def generate_tempos(self, song_tempo_list: list[SongTempo]) -> list[Container]:
return [
{
"tempo": tempo.bpm,
"time": round(tempo.position / self.tick_rate),
}
for tempo in song_tempo_list
if tempo.position >= 0
]

def generate_time_signatures(
self,
time_signature_list: list[TimeSignature],
) -> list[Container]:
events = []
prev_ticks = 0
prev_time_signature = None
for time_signature in time_signature_list:
if time_signature.bar_index >= 0:
if prev_time_signature is not None:
prev_ticks += round(
(time_signature.bar_index - prev_time_signature.bar_index)
* prev_time_signature.bar_length(self.options.ticks_per_beat)
)
events.append(
{
"numerator": time_signature.numerator,
"denominator": time_signature.denominator,
"time": prev_ticks,
}
)
prev_time_signature = time_signature
return events

def generate_tracks(self, tracks: list[Track]) -> list[Container]:
return [
mido_track
for track in tracks
if isinstance(track, SingingTrack)
and (mido_track := self.generate_track(track)) is not None
]

def generate_project(self, project: Project) -> VxFile:
return VxFile()
def generate_track(self, track: SingingTrack) -> Optional[Container]:
events: list[Container] = []
encoded_title = track.title.encode().decode("latin-1")
title_parts = []
for is_first, is_last, i in more_itertools.mark_ends(range(0, len(encoded_title), 12)):
if len(encoded_title) <= 12:
title_parts.append(
{"name": encoded_title[i : i + 12].encode("latin-1").decode(), "seq_stat": 0x10}
)
elif is_first:
title_parts.append(
{"name": encoded_title[i : i + 12].encode("latin-1").decode(), "seq_stat": 0x50}
)
elif is_last:
title_parts.append(
{"name": encoded_title[i : i + 12].encode("latin-1").decode(), "seq_stat": 0xD0}
)
else:
title_parts.append(
{"name": encoded_title[i : i + 12].encode("latin-1").decode(), "seq_stat": 0x90}
)
for i, note in enumerate(track.note_list):
lyric = "l-aa"
if note.lyric.isascii():
lyric = note.lyric
elif note.pronunciation and note.pronunciation.isascii():
lyric = note.pronunciation
if lyric:
for is_first, is_last, offset in more_itertools.mark_ends(
range(-2, len(lyric), 12)
):
if len(lyric) <= 10:
events.append(
{
"type": "lyrics",
"seq_num": i,
"seq_stat": 0x10,
"text": lyric[:10],
"time": 0,
}
)
elif is_first:
events.append(
{
"type": "lyrics",
"seq_num": i,
"seq_stat": 0x50,
"text": lyric[:10],
"time": 0,
}
)
elif is_last:
events.append(
{
"type": "lyrics",
"seq_num": None,
"seq_stat": 0xD0,
"text": lyric[offset:],
"time": 0,
}
)
else:
events.append(
{
"type": "lyrics",
"seq_num": None,
"seq_stat": 0x90,
"text": lyric[offset : offset + 12],
"time": 0,
}
)
events.extend(
[
{
"type": "note_on",
"note": note.key_number,
"time": round(note.start_pos / self.tick_rate),
"velocity": 0x7F,
"on_data": i,
},
{
"type": "note_off",
"note": note.key_number,
"time": round(note.end_pos / self.tick_rate),
"velocity": 0x00,
"off_data": 0x00,
},
]
)
if events:
return {"title_parts": title_parts, "events": events}
50 changes: 31 additions & 19 deletions libresvip/plugins/vxf/vx_beta_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
from libresvip.core.time_sync import TimeSynchronizer
from libresvip.core.warning_types import show_warning
from libresvip.model.base import Note, Project, SingingTrack, SongTempo, TimeSignature
from libresvip.model.pitch_simulator import PitchSimulator
from libresvip.model.point import Point
from libresvip.model.portamento import PortamentoPitch
from libresvip.model.relative_pitch_curve import RelativePitchCurve
from libresvip.utils.translation import gettext_lazy as _

from .model import VxFile, VxPitchData, VxTrack
Expand Down Expand Up @@ -142,36 +139,51 @@ def parse_track(self, track: VxTrack) -> SingingTrack:
buffer := "".join(event.text for event in track.events if event.type == "metadata")
):
pitch_data = VxPitchData.model_validate_json(buffer)
rel_pitch_points = []
pitch_points = [Point.start_point()]
prev_pos = None
for point in pitch_data.time_based_pitch_sequence.pitch_sequence:
if prev_pos and point.position - prev_pos > 1 and len(rel_pitch_points):
rel_pitch_points.append(
if prev_pos and point.position - prev_pos > 1 and len(pitch_points):
pitch_points.append(
Point(
x=rel_pitch_points[-1].x,
y=0,
x=pitch_points[-1].x,
y=-100,
)
)
rel_pitch_points.append(
if pitch_points[-1].y == -100:
pitch_points.append(
Point(
x=int(
self.synchronizer.get_actual_ticks_from_secs(
point.position
* pitch_data.time_based_pitch_sequence.time_frame_period_seconds
)
)
+ self.first_bar_length,
y=-100,
)
)
pitch_points.append(
Point(
x=int(
self.synchronizer.get_actual_ticks_from_secs(
point.position
* pitch_data.time_based_pitch_sequence.time_frame_period_seconds
)
),
y=int(point.pitch),
)
+ self.first_bar_length,
y=int(point.pitch) + 6900,
)
)
prev_pos = point.position
pitch_simulator = PitchSimulator(
synchronizer=self.synchronizer,
portamento=PortamentoPitch.no_portamento(),
note_list=[note.model_copy(update={"key_number": 69}) for note in notes],
)
singing_track.edited_params.pitch = RelativePitchCurve(
self.first_bar_length
).to_absolute(rel_pitch_points, pitch_simulator)
if len(pitch_points) > 1:
pitch_points.append(
Point(
x=pitch_points[-1].x,
y=-100,
)
)
pitch_points.append(Point.end_point())
singing_track.edited_params.pitch.points.root = pitch_points
return singing_track

def parse_tracks(self, vx_tracks: list[VxTrack]) -> list[SingingTrack]:
Expand Down

0 comments on commit dae858c

Please sign in to comment.