Skip to content

Commit

Permalink
vspx: support parsing vibrato
Browse files Browse the repository at this point in the history
  • Loading branch information
SoulMelody committed Dec 12, 2023
1 parent 16ca03b commit 5875c96
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 177 deletions.
34 changes: 18 additions & 16 deletions libresvip/plugins/acep/base_pitch_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def acep_vibrato_value_curve(
return (
math.sin(
math.pi
* (2 * (seconds - vibrato_start) * vibrato.frequency + vibrato.phase)
* (2 * (seconds - vibrato_start) * vibrato.frequency - vibrato.phase)
)
* vibrato.amplitude
* 0.5
Expand Down Expand Up @@ -97,27 +97,29 @@ def __post_init__(
vibrato_start + note.vibrato.attack_ratio * vibrato_duration
)
release_time = note_end - note.vibrato.release_ratio * vibrato_duration
if note.vibrato.release_ratio:
self.vibrato_coef_interval_dict[
portion.openclosed(release_time, note_end)
] = functools.partial(
linear_interpolation,
start=(release_time, note.vibrato.release_level),
end=(note_end, 0),
)
self.vibrato_coef_interval_dict[
portion.closed(vibrato_start, attack_time)
] = functools.partial(
linear_interpolation,
start=(vibrato_start, 0),
end=(attack_time, note.vibrato.attack_level),
)
self.vibrato_coef_interval_dict[
portion.open(attack_time, release_time)
portion.closed(attack_time, release_time)
] = functools.partial(
linear_interpolation,
start=(attack_time, note.vibrato.attack_level),
end=(release_time, note.vibrato.release_level),
)
self.vibrato_coef_interval_dict[
portion.closed(release_time, note_end)
] = functools.partial(
linear_interpolation,
start=(release_time, note.vibrato.release_level),
end=(note_end, 0),
)
if note.vibrato.attack_ratio:
self.vibrato_coef_interval_dict[
portion.closedopen(vibrato_start, attack_time)
] = functools.partial(
linear_interpolation,
start=(vibrato_start, 0),
end=(attack_time, note.vibrato.attack_level),
)
self.values_in_semitone = _convolve(note_list)

def semitone_value_at(self, seconds: float) -> float:
Expand Down
3 changes: 1 addition & 2 deletions libresvip/plugins/ustx/ustx_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,13 @@ def parse_tracks(
ai_singer_name=ustx_track.singer or "",
)
for ustx_track in tracks
if ustx_track.singer
]
for track in track_list:
track.edited_params.pitch.points.append(Point.start_point())
for voice_part in voice_parts:
track_index = voice_part.track_no
if track_index < len(track_list):
track = track_list[track_index]
track: SingingTrack = track_list[track_index]
else:
continue
if not track.title:
Expand Down
6 changes: 1 addition & 5 deletions libresvip/plugins/vspx/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class VocalSharpTrillBase(abc.ABC):
"type": "Element",
},
)
phase: Optional[int] = field(
phase: Optional[float] = field(
default=0,
metadata={
"type": "Element",
Expand Down Expand Up @@ -460,10 +460,6 @@ class Meta:
},
)

@property
def end_pos(self) -> int:
return self.pos + self.duration

@property
def key_number(self) -> int:
return note2midi(self.pitch)
Expand Down
8 changes: 5 additions & 3 deletions libresvip/plugins/vspx/vspx_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
VocalSharpTempo,
)
from .options import OutputOptions
from .vspx_interval_dict import vspx_key_interval_dict
from .vspx_interval_dict import BasePitchCurve


@dataclasses.dataclass
Expand Down Expand Up @@ -161,14 +161,16 @@ def generate_notes(self, notes: list[Note]) -> list[VocalSharpNote]:
def generate_pitch(
self, pitch: ParamCurve, note_track: VocalSharpNoteTrack
) -> list[PIT]:
key_interval_dict = vspx_key_interval_dict(note_track, self.synchronizer)
base_pitch_curve = BasePitchCurve(note_track, None, self.synchronizer)
pitch_points = []
prev_point: Optional[PIT] = None
for point in pitch.points:
cur_tick = point.x - self.first_bar_length
cur_secs = self.synchronizer.get_actual_secs_from_ticks(cur_tick)
if (
point.y > 0
and (base_key := key_interval_dict.get(cur_tick)) is not None
and (base_key := base_pitch_curve.semitone_value_at(cur_secs))
is not None
):
if prev_point is not None:
cur_value = point.y - base_key * 100
Expand Down
225 changes: 191 additions & 34 deletions libresvip/plugins/vspx/vspx_interval_dict.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import dataclasses
import functools
import math
from typing import Optional, Union

import more_itertools
import portion
Expand All @@ -10,7 +12,7 @@
Point,
)

from .model import VocalSharpNoteTrack
from .model import VocalSharpDefaultTrill, VocalSharpNoteTrack, VocalSharpTrill


def vspx_cosine_easing_in_out_interpolation(
Expand All @@ -21,40 +23,195 @@ def vspx_cosine_easing_in_out_interpolation(
return (y0 + y1) / 2 + (y0 - y1) * math.cos((x - x0) / (x1 - x0) * math.pi) / 2


def vspx_key_interval_dict(
note_track: VocalSharpNoteTrack,
synchronizer: TimeSynchronizer,
) -> PiecewiseIntervalDict:
interval_dict = PiecewiseIntervalDict()
for is_first, is_last, (prev_note, next_note) in more_itertools.mark_ends(
more_itertools.pairwise(note_track.note)
):
if is_first:
interval_dict[portion.closedopen(0, prev_note.pos)] = prev_note.key_number
middle_pos = (prev_note.end_pos + next_note.pos) // 2
interval_dict[
portion.closedopen(prev_note.pos, middle_pos)
] = prev_note.key_number
interval_dict[
portion.closedopen(
middle_pos,
next_note.end_pos,
def vspx_sine_vibrato_interpolation(
seconds: float,
vibrato_start: float,
trill: Union[VocalSharpTrill, VocalSharpDefaultTrill],
) -> float:
return (
math.sin(
math.pi * (2 * (seconds - vibrato_start) * trill.frequency + trill.phase)
)
* trill.amplitude
)


def vspx_cosine_vibrato_coef_attack_interpolation(
seconds: float, vibrato_start: float, por: float
) -> float:
return 1 - math.cos(math.pi * (0.5 * (seconds - vibrato_start) / por))


def vspx_cosine_vibrato_coef_release_interpolation(
seconds: float, vibrato_end: float, por: float
) -> float:
return 1 - math.cos(math.pi * (0.5 * (vibrato_end - seconds) / por))


@dataclasses.dataclass
class BasePitchCurve:
note_track: dataclasses.InitVar[VocalSharpNoteTrack]
default_trill: dataclasses.InitVar[Optional[VocalSharpDefaultTrill]]
synchronizer: TimeSynchronizer
key_interval_dict: PiecewiseIntervalDict = dataclasses.field(
default_factory=PiecewiseIntervalDict
)
vibrato_value_interval_dict: PiecewiseIntervalDict = dataclasses.field(
default_factory=PiecewiseIntervalDict
)
vibrato_coef_interval_dict: PiecewiseIntervalDict = dataclasses.field(
default_factory=PiecewiseIntervalDict
)

def __post_init__(
self,
note_track: VocalSharpNoteTrack,
default_trill: Optional[VocalSharpDefaultTrill] = None,
) -> None:
if not len(note_track.note):
pass
elif len(note_track.note) == 1:
note = note_track.note[0]
self.key_interval_dict[portion.closedopen(0, portion.inf)] = note.key_number
if (trill := note.trill or default_trill) is not None:
vibrato_start_secs = (
self.synchronizer.get_actual_secs_from_ticks(note.pos) + trill.pos
)
vibrato_end_secs = self.synchronizer.get_actual_secs_from_ticks(
note.pos + note.duration
)
vibrato_duration = vibrato_end_secs - vibrato_start_secs
if 0 < vibrato_duration < 2 * note_track.por:
self.set_vspx_vibrato_curve(
vibrato_start_secs, vibrato_end_secs, trill
)
elif vibrato_duration >= 2 * note_track.por:
self.set_vspx_vibrato_curve(
vibrato_start_secs, vibrato_end_secs, trill, note_track.por
)
else:
for is_first, is_last, (prev_note, next_note) in more_itertools.mark_ends(
more_itertools.pairwise(note_track.note)
):
prev_start_secs = self.synchronizer.get_actual_secs_from_ticks(
prev_note.pos
)
prev_end_secs = self.synchronizer.get_actual_secs_from_ticks(
prev_note.pos + prev_note.duration
)
next_start_secs = self.synchronizer.get_actual_secs_from_ticks(
next_note.pos
)
next_end_secs = self.synchronizer.get_actual_secs_from_ticks(
next_note.pos + next_note.duration
)
if is_first:
self.key_interval_dict[
portion.closedopen(0, prev_start_secs)
] = prev_note.key_number
middle_secs = (prev_end_secs + next_start_secs) / 2
self.key_interval_dict[
portion.closedopen(prev_start_secs, middle_secs)
] = prev_note.key_number
self.key_interval_dict[
portion.closedopen(
middle_secs,
next_end_secs,
)
] = next_note.key_number
if is_last:
self.key_interval_dict[
portion.closedopen(next_start_secs, portion.inf)
] = next_note.key_number
if (trill := next_note.trill or default_trill) is not None:
vibrato_start_secs = next_start_secs + trill.pos
vibrato_end_secs = next_end_secs
vibrato_duration = vibrato_end_secs - vibrato_start_secs
if 0 < vibrato_duration < 2 * note_track.por:
self.set_vspx_vibrato_curve(
vibrato_start_secs, vibrato_end_secs, trill
)
elif vibrato_duration >= 2 * note_track.por:
self.set_vspx_vibrato_curve(
vibrato_start_secs,
vibrato_end_secs,
trill,
note_track.por,
)
if note_track.por > 0:
por_start = middle_secs - note_track.por
por_end = middle_secs + note_track.por
self.key_interval_dict[
portion.closedopen(por_start, por_end)
] = functools.partial(
vspx_cosine_easing_in_out_interpolation,
start=Point(x=por_start, y=prev_note.key_number),
end=Point(x=por_end, y=next_note.key_number),
)
if (trill := prev_note.trill or default_trill) is not None:
vibrato_start_secs = prev_start_secs + trill.pos
vibrato_end_secs = (prev_end_secs + next_start_secs) / 2
vibrato_duration = vibrato_end_secs - vibrato_start_secs
if 0 < vibrato_duration < 2 * note_track.por:
self.set_vspx_vibrato_curve(
vibrato_start_secs, vibrato_end_secs, trill
)
elif vibrato_duration >= 2 * note_track.por:
self.set_vspx_vibrato_curve(
vibrato_start_secs, vibrato_end_secs, trill, note_track.por
)

def set_vspx_vibrato_curve(
self,
start: float,
end: float,
trill: Union[VocalSharpTrill, VocalSharpDefaultTrill],
por: Optional[float] = None,
) -> None:
self.vibrato_value_interval_dict[
portion.closed(start, end)
] = functools.partial(
vspx_sine_vibrato_interpolation, vibrato_start=start, trill=trill
)
if por is None:
middle = (start + end) / 2
half = (end - start) / 2
self.vibrato_coef_interval_dict[
portion.closedopen(start, middle)
] = functools.partial(
vspx_cosine_vibrato_coef_attack_interpolation,
vibrato_start=start,
por=half,
)
] = next_note.key_number
if is_last:
interval_dict[
portion.closedopen(next_note.pos, portion.inf)
] = next_note.key_number
if note_track.por > 0:
por_start = synchronizer.get_actual_ticks_from_secs_offset(
middle_pos, -note_track.por
self.vibrato_coef_interval_dict[
portion.closed(middle, end)
] = functools.partial(
vspx_cosine_vibrato_coef_release_interpolation,
vibrato_end=end,
por=half,
)
por_end = synchronizer.get_actual_ticks_from_secs_offset(
middle_pos, note_track.por
elif por:
self.vibrato_coef_interval_dict[
portion.closedopen(start, start + por)
] = functools.partial(
vspx_cosine_vibrato_coef_attack_interpolation,
vibrato_start=start,
por=por,
)
interval_dict[portion.closedopen(por_start, por_end)] = functools.partial(
vspx_cosine_easing_in_out_interpolation,
start=Point(x=por_start, y=prev_note.key_number),
end=Point(x=por_end, y=next_note.key_number),
self.vibrato_coef_interval_dict[
portion.openclosed(end - por, end)
] = functools.partial(
vspx_cosine_vibrato_coef_release_interpolation, vibrato_end=end, por=por
)
return interval_dict
self.vibrato_coef_interval_dict[portion.closed(start + por, end - por)] = 1
else:
self.vibrato_coef_interval_dict[portion.closed(start, end)] = 0

def semitone_value_at(self, seconds: float) -> Optional[float]:
if (pitch_value := self.key_interval_dict.get(seconds)) is not None:
if (
vibrato_value := self.vibrato_value_interval_dict.get(seconds)
) is not None:
vibrato_value *= self.vibrato_coef_interval_dict[seconds]
pitch_value += vibrato_value
return pitch_value
Loading

0 comments on commit 5875c96

Please sign in to comment.