Skip to content

Commit

Permalink
update relative pitch processing
Browse files Browse the repository at this point in the history
  • Loading branch information
SoulMelody committed Aug 7, 2024
1 parent 213e72a commit 6907dcb
Show file tree
Hide file tree
Showing 21 changed files with 228 additions and 112 deletions.
43 changes: 26 additions & 17 deletions libresvip/model/pitch_simulator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dataclasses
import functools
from typing import Optional

import portion

Expand Down Expand Up @@ -37,23 +38,30 @@ def __post_init__(self, note_list: list[Note]) -> None:
next_note.start_pos, next_note.end_pos
)
next_portamento = min(next_dur * max_portamento_percent, max_portamento_time)
interval = next_head - current_head - current_dur
if interval <= 2 * max_portamento_time:
current_portamento_start = next_head - interval / 2 - current_portamento
current_portamento_end = next_head - interval / 2 + next_portamento
interval = (
0.0 if next_note.lyric == "-" else (next_head - current_head - current_dur) / 2
) - self.portamento.offset
if interval <= max_portamento_time:
current_portamento_start = next_head - interval - current_portamento
current_portamento_end = next_head - interval + next_portamento
else:
current_portamento_start = next_head - interval / 2 - max_portamento_time
current_portamento_end = next_head - interval / 2 + max_portamento_time
current_portamento_start = next_head - interval - max_portamento_time
current_portamento_end = next_head - interval + max_portamento_time
self.interval_dict[
portion.closedopen(prev_portamento_end, current_portamento_start)
] = current_note.key_number
self.interval_dict[
portion.closedopen(current_portamento_start, current_portamento_end)
] = functools.partial( # type: ignore[call-arg]
self.portamento.inter_func,
start=(current_portamento_start, current_note.key_number),
end=(current_portamento_end, next_note.key_number),
)
if current_note.key_number == next_note.key_number:
self.interval_dict[
portion.closedopen(current_portamento_start, current_portamento_end)
] = current_note.key_number
else:
self.interval_dict[
portion.closedopen(current_portamento_start, current_portamento_end)
] = functools.partial( # type: ignore[call-arg]
self.portamento.inter_func,
start=(current_portamento_start, current_note.key_number),
end=(current_portamento_end, next_note.key_number),
)
current_note = next_note
current_head = next_head
current_dur = next_dur
Expand All @@ -63,8 +71,9 @@ def __post_init__(self, note_list: list[Note]) -> None:
current_note.key_number
)

def pitch_at_ticks(self, ticks: int) -> float:
return self.pitch_at_secs(self.synchronizer.get_actual_ticks_from_ticks(ticks))
def pitch_at_ticks(self, ticks: int) -> Optional[float]:
return self.pitch_at_secs(self.synchronizer.get_actual_secs_from_ticks(ticks))

def pitch_at_secs(self, secs: float) -> float:
return self.interval_dict.get(secs, 0.0) * 100
def pitch_at_secs(self, secs: float) -> Optional[float]:
if value := self.interval_dict.get(secs):
return value * 100
10 changes: 10 additions & 0 deletions libresvip/model/portamento.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from libresvip.utils.music_math import (
cosine_easing_in_out_interpolation,
cubic_interpolation,
linear_interpolation,
sigmoid_interpolation,
)

Expand All @@ -19,6 +20,11 @@ class PortamentoPitch:
max_inter_time_in_secs: float
max_inter_time_percent: float
inter_func: Callable[[float, tuple[float, float], tuple[float, float]], float]
offset: float = 0.0

@classmethod
def no_portamento(cls) -> PortamentoPitch:
return cls(0.0, 0.0, linear_interpolation)

@classmethod
def cosine_portamento(cls) -> PortamentoPitch:
Expand All @@ -31,3 +37,7 @@ def cubic_portamento(cls) -> PortamentoPitch:
@classmethod
def sigmoid_portamento(cls) -> PortamentoPitch:
return cls(0.075, 0.48, partial(sigmoid_interpolation, k=5.5)) # type: ignore[call-arg]

@classmethod
def vocaloid_portamento(cls) -> PortamentoPitch:
return cls(0.075, 0.48, partial(sigmoid_interpolation, k=3.6), -0.02) # type: ignore[call-arg]
85 changes: 23 additions & 62 deletions libresvip/model/relative_pitch_curve.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,16 @@
from __future__ import annotations

import dataclasses
from typing import TYPE_CHECKING, Optional

import more_itertools
import portion

from libresvip.core.constants import TICKS_IN_BEAT
from libresvip.core.exceptions import NotesOverlappedError
from libresvip.core.time_interval import PiecewiseIntervalDict
from libresvip.model.base import Note, ParamCurve, Points
from libresvip.model.base import ParamCurve, Points
from libresvip.model.point import Point
from libresvip.utils.translation import gettext_lazy as _


def get_interval_dict(notes: list[Note], to_absolute: bool) -> PiecewiseIntervalDict:
interval_dict = PiecewiseIntervalDict()
for is_first, is_last, note in more_itertools.mark_ends(notes):
if is_first and is_last:
interval = portion.closed(note.start_pos, note.end_pos)
elif is_first:
interval = portion.closedopen(note.start_pos, note.end_pos)
elif is_last:
interval = portion.openclosed(note.start_pos, note.end_pos)
else:
interval = portion.open(note.start_pos, note.end_pos)
if not interval.intersection(interval_dict.domain()).empty:
msg = _("Notes Overlapped")
raise NotesOverlappedError(msg)
interval_dict[interval] = note.key_number
for prev_note, next_note in more_itertools.windowed(notes, 2):
if next_note is None:
continue
if prev_note.end_pos < next_note.start_pos:
if next_note.lyric == "-":
interval_dict[portion.singleton(prev_note.end_pos)] = (
prev_note.key_number + next_note.key_number
) / 2
interval_dict[portion.openclosed(prev_note.end_pos, next_note.start_pos)] = (
next_note.key_number
)
else:
middle_pos = (prev_note.end_pos + next_note.start_pos) / 2
interval_dict[portion.closedopen(prev_note.end_pos, middle_pos)] = (
prev_note.key_number
)
interval_dict[portion.singleton(middle_pos)] = (
prev_note.key_number + next_note.key_number
) / 2
interval_dict[portion.openclosed(middle_pos, next_note.start_pos)] = (
next_note.key_number
)
else:
interval_dict[portion.singleton(next_note.start_pos)] = next_note.key_number
return interval_dict
if TYPE_CHECKING:
from libresvip.model.pitch_simulator import PitchSimulator


@dataclasses.dataclass
Expand All @@ -62,48 +20,51 @@ class RelativePitchCurve:
upper_bound: float = portion.inf
pitch_interval: int = 5

def to_absolute(self, points: list[Point], note_list: list[Note]) -> ParamCurve:
def to_absolute(self, points: list[Point], pitch_simulator: PitchSimulator) -> ParamCurve:
return ParamCurve(
points=Points(root=self._convert_relativity(points, note_list, to_absolute=True))
points=Points(root=self._convert_relativity(points, pitch_simulator, to_absolute=True))
)

def _convert_relativity(
self,
points: list[Point],
note_list: list[Note],
pitch_simulator: PitchSimulator,
to_absolute: bool = False,
) -> list[Point]:
converted_data: list[Point] = []
if not len(note_list):
return converted_data
interval_dict = get_interval_dict(note_list, to_absolute)
prev_x = None
prev_y = None
prev_y: Optional[float] = None
for point in points:
pos = point.x + (0 if to_absolute else -self.first_bar_length)
cur_x = point.x + (self.first_bar_length if to_absolute else -self.first_bar_length)
if (base_key := interval_dict.get(pos)) is None:
if (base_key := pitch_simulator.pitch_at_ticks(pos)) is None:
y = None
elif not to_absolute and point.y == -100:
y = 0
if point.x in [-192000, 1073741823]:
continue
y = 0.0
else:
y = point.y + (base_key if to_absolute else -base_key) * 100
y = point.y + (base_key if to_absolute else -base_key)
if (
y is not None
and prev_y is not None
and converted_data
and cur_x - prev_x > self.pitch_interval
):
for tick in range(prev_x + self.pitch_interval, cur_x, self.pitch_interval):
tick_pos = tick + (-self.first_bar_length if to_absolute else 0)
if (tick_key := interval_dict.get(tick_pos)) is not None:
tick_pos = tick + (
-self.first_bar_length if to_absolute else self.first_bar_length
)
if (tick_key := pitch_simulator.pitch_at_ticks(tick_pos)) is not None:
if to_absolute:
converted_data.append(Point(x=tick, y=round(prev_y + tick_key * 100)))
converted_data.append(Point(x=tick, y=round(prev_y + tick_key)))
else:
converted_data.append(
Point(
x=tick,
y=prev_y + (y - prev_y) * (tick - prev_x) / (cur_x - prev_x),
y=round(
prev_y + (y - prev_y) * (tick - prev_x) / (cur_x - prev_x)
),
)
)
if y is not None:
Expand Down Expand Up @@ -132,6 +93,6 @@ def _convert_relativity(
def from_absolute(
self,
points: list[Point],
notes: list[Note],
pitch_simulator: PitchSimulator,
) -> list[Point]:
return self._convert_relativity(points, notes, to_absolute=False)
return self._convert_relativity(points, pitch_simulator, to_absolute=False)
5 changes: 4 additions & 1 deletion libresvip/plugins/mid/midi_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from libresvip.core.constants import TICKS_IN_BEAT
from libresvip.core.lyric_phoneme.chinese import get_pinyin_series
from libresvip.core.time_sync import TimeSynchronizer
from libresvip.model.base import (
Project,
SingingTrack,
Expand All @@ -22,6 +23,7 @@
@dataclasses.dataclass
class MidiGenerator:
options: OutputOptions
synchronizer: TimeSynchronizer = dataclasses.field(init=False)
first_bar_length: int = dataclasses.field(init=False)

@property
Expand All @@ -34,6 +36,7 @@ def generate_project(self, project: Project) -> mido.MidiFile:
self.first_bar_length = round(
project.time_signature_list[0].bar_length(self.options.ticks_per_beat)
)
self.synchronizer = TimeSynchronizer(project.song_tempo_list)
mido_obj = mido.MidiFile(charset=self.options.lyric_encoding)
mido_obj.ticks_per_beat = self.options.ticks_per_beat
master_track = mido.MidiTrack()
Expand Down Expand Up @@ -129,7 +132,7 @@ def generate_track(self, track: SingingTrack) -> mido.MidiTrack:
)
)
if pitch_data := generate_for_midi(
self.first_bar_length, track.edited_params.pitch, track.note_list
self.first_bar_length, track.edited_params.pitch, track.note_list, self.synchronizer
):
for pbs_event in pitch_data.pbs:
msg_time = round(pbs_event.tick / self.tick_rate)
Expand Down
12 changes: 11 additions & 1 deletion libresvip/plugins/mid/midi_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import more_itertools

from libresvip.core.constants import DEFAULT_PHONEME, TICKS_IN_BEAT
from libresvip.core.time_sync import TimeSynchronizer
from libresvip.core.warning_types import show_warning
from libresvip.model.base import (
Note,
Expand All @@ -20,6 +21,8 @@
TimeSignature,
Track,
)
from libresvip.model.pitch_simulator import PitchSimulator
from libresvip.model.portamento import PortamentoPitch
from libresvip.model.relative_pitch_curve import RelativePitchCurve
from libresvip.utils.music_math import ratio_to_db
from libresvip.utils.text import LATIN_ALPHABET
Expand Down Expand Up @@ -49,6 +52,7 @@ class MidiParser:
options: InputOptions
ticks_per_beat: int = dataclasses.field(init=False)
first_bar_length: int = dataclasses.field(init=False)
synchronizer: TimeSynchronizer = dataclasses.field(init=False)
selected_channels: list[int] = dataclasses.field(default_factory=list)

def __post_init__(self) -> None:
Expand Down Expand Up @@ -77,6 +81,7 @@ def parse_project(self, mido_obj: mido.MidiFile) -> Project:
master_track = mido_obj.tracks[0]
time_signature_list = self.parse_time_signatures(master_track)
song_tempo_list = self.parse_tempo(mido_obj.tracks)
self.synchronizer = TimeSynchronizer(song_tempo_list)
return Project(
song_tempo_list=song_tempo_list,
time_signature_list=time_signature_list,
Expand Down Expand Up @@ -253,9 +258,14 @@ def parse_track(self, track_idx: int, track: mido.MidiTrack) -> list[SingingTrac
show_warning(msg)
edited_params = Params(volume=expression)
if self.options.import_pitch:
pitch_simulator = PitchSimulator(
synchronizer=self.synchronizer,
portamento=PortamentoPitch.no_portamento(),
note_list=notes,
)
rel_pitch_points.sort(key=operator.attrgetter("x"))
edited_params.pitch = RelativePitchCurve(self.first_bar_length).to_absolute(
rel_pitch_points, notes
rel_pitch_points, pitch_simulator
)
if len(notes):
tracks.append(
Expand Down
12 changes: 10 additions & 2 deletions libresvip/plugins/mid/midi_pitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional

from libresvip.core.time_sync import TimeSynchronizer
from libresvip.model.base import Note, ParamCurve
from libresvip.model.pitch_simulator import PitchSimulator
from libresvip.model.portamento import PortamentoPitch
from libresvip.model.relative_pitch_curve import RelativePitchCurve
from libresvip.utils.music_math import clamp

Expand Down Expand Up @@ -30,9 +33,14 @@ class MIDIPitchData:


def generate_for_midi(
first_bar_length: int, pitch: ParamCurve, notes: list[Note]
first_bar_length: int, pitch: ParamCurve, notes: list[Note], synchronizer: TimeSynchronizer
) -> Optional[MIDIPitchData]:
data = RelativePitchCurve(first_bar_length).from_absolute(pitch.points.root, notes)
pitch_simulator = PitchSimulator(
synchronizer=synchronizer,
portamento=PortamentoPitch.no_portamento(),
note_list=notes,
)
data = RelativePitchCurve(first_bar_length).from_absolute(pitch.points.root, pitch_simulator)
if not len(data):
return None
pitch_sectioned: list[list[Point]] = [[]]
Expand Down
11 changes: 9 additions & 2 deletions libresvip/plugins/s5p/synthv_editor_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
TimeSignature,
Track,
)
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.music_math import ratio_to_db

Expand Down Expand Up @@ -43,9 +45,9 @@ class SynthVEditorGenerator:
def generate_project(self, project: Project) -> S5pProject:
self.first_bar_length = round(project.time_signature_list[0].bar_length())
s5p_project = S5pProject(
tracks=self.generate_singing_tracks(project.track_list),
tempo=self.generate_tempos(project.song_tempo_list),
meter=self.generate_time_signatures(project.time_signature_list),
tracks=self.generate_singing_tracks(project.track_list),
)
if (
instrumental_track_and_mixer := next(
Expand Down Expand Up @@ -145,8 +147,13 @@ def generate_instrumental_track_and_mixer(

def generate_parameters(self, edited_params: Params, note_list: list[Note]) -> S5pParameters:
interval = round(TICK_RATE * 3.75)
pitch_simulator = PitchSimulator(
synchronizer=self.synchronizer,
portamento=PortamentoPitch.sigmoid_portamento(),
note_list=note_list,
)
rel_pitch_points = RelativePitchCurve(self.first_bar_length).from_absolute(
edited_params.pitch.points.root, note_list
edited_params.pitch.points.root, pitch_simulator
)
return S5pParameters(
pitch_delta=self.generate_pitch_delta(rel_pitch_points, interval),
Expand Down
Loading

0 comments on commit 6907dcb

Please sign in to comment.