Skip to content

Commit

Permalink
vfp: init plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
SoulMelody committed Jan 7, 2025
1 parent 4472e6f commit 549d49c
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 27 deletions.
4 changes: 3 additions & 1 deletion libresvip/plugins/ufdata/options.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from pydantic import BaseModel

from libresvip.model.option_mixins import EnablePitchImportationMixin

class InputOptions(BaseModel):

class InputOptions(EnablePitchImportationMixin, BaseModel):
pass


Expand Down
2 changes: 1 addition & 1 deletion libresvip/plugins/ufdata/ufdata_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def parse_tracks(self, tracks: list[UFTracks], tick_prefix: int) -> list[Singing
title=track.name,
note_list=self.parse_notes(track.notes, tick_prefix),
)
if track.pitch is not None:
if self.options.import_pitch and track.pitch is not None:
singing_track.edited_params.pitch = self.parse_pitch(
track.pitch, singing_track.note_list, tick_prefix
)
Expand Down
2 changes: 1 addition & 1 deletion libresvip/plugins/ustx/ustx_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def generate_project(self, os_project: Project) -> USTXProject:
self.generate_time_signature(ts) for ts in os_project.time_signature_list
]
if not ustx_time_signatures:
ustx_time_signatures.append(UTimeSignature(0, 4, 4))
ustx_time_signatures.append(UTimeSignature())
first_bar_length = int(
1920 * ustx_time_signatures[0].beat_per_bar / ustx_time_signatures[0].beat_unit
)
Expand Down
File renamed without changes.
92 changes: 68 additions & 24 deletions experimental/vfp/model.py → libresvip/plugins/vfp/model.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
from typing import Any, Optional
import pathlib
from typing import Annotated, Any, Literal, Optional, Union

from pydantic import BaseModel, Field
from pydantic import Field, ValidationInfo, model_validator
from typing_extensions import Self

from libresvip.model.base import BaseModel


class VOXFactoryNote(BaseModel):
time: float
midi: int
name: str
syllable: str
ticks: int
ticks: float
duration: float
duration_ticks: int = Field(alias="durationTicks")
duration_ticks: float = Field(alias="durationTicks")
velocity: int
note_type: None = Field(alias="noteType")
vibrato_depth: float = Field(alias="vibratoDepth")
pre_bend: float = Field(alias="preBend")
post_bend: float = Field(alias="postBend")
harmonic_ratio: float = Field(alias="harmonicRatio")
vibrato_depth: Optional[float] = Field(None, alias="vibratoDepth")
pre_bend: Optional[float] = Field(None, alias="preBend")
post_bend: Optional[float] = Field(None, alias="postBend")
harmonic_ratio: Optional[float] = Field(None, alias="harmonicRatio")
pitch_bends: list[float] = Field(alias="pitchBends")


Expand All @@ -26,24 +30,50 @@ class VOXFactoryMetadata(BaseModel):
transpose: int
harmonic_ratio: float = Field(alias="harmonicRatio")
pitch_detection: Optional[str] = Field(None, alias="pitchDetection")
instrument: Optional[str] = None


class VOXFactoryClip(BaseModel):
type: str
length_type: str = Field(alias="lengthType")
class VOXFactoryClipBase(BaseModel):
name: str
start_quarter: int = Field(alias="startQuarter")
offset_quarter: int = Field(alias="offsetQuarter")
length: int
start_quarter: float = Field(alias="startQuarter")
offset_quarter: float = Field(alias="offsetQuarter")
length: float
use_source: bool = Field(alias="useSource")
audio_data_key: Optional[str] = Field(None, alias="audioDataKey")
audio_data_order: list[str] = Field(alias="audioDataOrder")
audio_data_quarter: float = Field(alias="audioDataQuarter")
note_bank: dict[str, VOXFactoryNote] = Field(alias="noteBank")
note_order: list[str] = Field(alias="noteOrder")
next_note_index: int = Field(alias="nextNoteIndex")
use_source: bool = Field(alias="useSource")
pinned_audio_data_order: list[str] = Field(alias="pinnedAudioDataOrder")
audio_data_order: list[str] = Field(alias="audioDataOrder")
audio_data_quarter: int = Field(alias="audioDataQuarter")
metadata: VOXFactoryMetadata
audio_data_key: str = Field(alias="audioDataKey")
metadata: Optional[VOXFactoryMetadata] = None


class VOXFactoryVocalClip(VOXFactoryClipBase):
type: Literal["vocal"] = "vocal"
length_type: Literal["quarter"] = Field("quarter", alias="lengthType")


class VOXFactoryAudioClip(VOXFactoryClipBase):
type: Literal["audio"] = "audio"
length_type: Literal["time"] = Field("time", alias="lengthType")
source_audio_data_key: str = Field(alias="sourceAudioDataKey")

@model_validator(mode="after")
def extract_audio(self, info: ValidationInfo) -> Self:
if (
info.context is not None
and info.context["extract_audio"]
and not hasattr(info.context["path"], "protocol")
):
archive_audio_path = f"resources/{self.source_audio_data_key}"
if not (
audio_path := (info.context["path"].parent / self.name).with_suffix(
pathlib.Path(archive_audio_path).suffix
)
).exists():
audio_path.write_bytes(info.context["archive_file"].read(archive_audio_path))
return self


class VOXFactoryAudioViewProperty(BaseModel):
Expand All @@ -69,24 +99,38 @@ class VOXFactoryDevice(BaseModel):
on: bool


class VOXFactoryTrack(BaseModel):
type: str
class VOXFactoryTrackBase(BaseModel):
name: str
instrument: str
instrument: Optional[str] = None
h: int
color: str
volume: float
pan: float
solo: bool
mute: bool
arm: bool
clip_bank: dict[str, VOXFactoryClip] = Field(alias="clipBank")
clip_order: list[str] = Field(alias="clipOrder")
device_bank: dict[str, VOXFactoryDevice] = Field(alias="deviceBank")
device_order: list[str] = Field(alias="deviceOrder")
audio_view_property: VOXFactoryAudioViewProperty = Field(alias="audioViewProperty")


class VOXFactoryVocalTrack(VOXFactoryTrackBase):
type: Literal["vocal"] = "vocal"
clip_bank: dict[str, VOXFactoryVocalClip] = Field(alias="clipBank")


class VOXFactoryAudioTrack(VOXFactoryTrackBase):
type: Literal["audio"] = "audio"
clip_bank: dict[str, VOXFactoryAudioClip] = Field(alias="clipBank")


VOXFactoryTrack = Annotated[
Union[VOXFactoryVocalTrack, VOXFactoryAudioTrack],
Field(discriminator="type"),
]


class VOXFactorySelectedClipBankItem(BaseModel):
track_key: str = Field(alias="trackKey")
clip_key: str = Field(alias="clipKey")
Expand All @@ -102,7 +146,7 @@ class VOXFactoryAudioData(BaseModel):
sample_rate: int = Field(alias="sampleRate")
number_of_channels: int = Field(alias="numberOfChannels")
sample_length: int = Field(alias="sampleLength")
metadata: VOXFactoryMetadata
metadata: Optional[VOXFactoryMetadata] = None


class VOXFactoryProject(BaseModel):
Expand Down
21 changes: 21 additions & 0 deletions libresvip/plugins/vfp/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pydantic import BaseModel

from libresvip.model.option_mixins import (
EnableInstrumentalTrackImportationMixin,
EnablePitchImportationMixin,
ExtractEmbededAudioMixin,
StaticTempoMixin,
)


class InputOptions(
EnableInstrumentalTrackImportationMixin,
EnablePitchImportationMixin,
ExtractEmbededAudioMixin,
BaseModel,
):
pass


class OutputOptions(StaticTempoMixin, BaseModel):
pass
12 changes: 12 additions & 0 deletions libresvip/plugins/vfp/vfp.yapsy-plugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Core]
name = VOX Factory
module = vox_factory_converter

[Documentation]
Author = SoulMelody
Version = 1.0.0
Website = https://space.bilibili.com/175862486
Format = VOX Factory Project File
Description = Conversion plugin for VOX Factory Project File
Suffix = vfp
IconBase64 = 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
42 changes: 42 additions & 0 deletions libresvip/plugins/vfp/vox_factory_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import io
import pathlib
import zipfile

from libresvip.core.compat import json
from libresvip.extension import base as plugin_base
from libresvip.model.base import Project

from .model import VOXFactoryProject
from .options import InputOptions, OutputOptions
from .vox_factory_generator import VOXFactoryGenerator
from .vox_factory_parser import VOXFactoryParser


class VOXFactoryConverter(plugin_base.SVSConverterBase):
def load(self, path: pathlib.Path, options: InputOptions) -> Project:
with zipfile.ZipFile(io.BytesIO(path.read_bytes()), "r") as archive_file:
proj = VOXFactoryProject.model_validate_json(
archive_file.read("project.json"),
context={
"extract_audio": options.extract_audio,
"path": path,
"archive_file": archive_file,
},
)
return VOXFactoryParser(options, path).parse_project(proj)

def dump(self, path: pathlib.Path, project: Project, options: OutputOptions) -> None:
buffer = io.BytesIO()
generator = VOXFactoryGenerator(options)
vox_factory_project = generator.generate_project(project)
with zipfile.ZipFile(buffer, "w") as archive_file:
archive_file.writestr(
"project.json",
json.dumps(
vox_factory_project.model_dump(mode="json", exclude_none=True, by_alias=True),
ensure_ascii=False,
),
)
for audio_name, audio_path in generator.audio_paths.items():
archive_file.writestr(f"resources/{audio_name}", audio_path.read_bytes())
path.write_bytes(buffer.getvalue())
73 changes: 73 additions & 0 deletions libresvip/plugins/vfp/vox_factory_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import dataclasses
import pathlib

from libresvip.core.constants import DEFAULT_BPM
from libresvip.model.base import Note, Project, SingingTrack, SongTempo, TimeSignature, Track

from .model import (
VOXFactoryNote,
VOXFactoryProject,
VOXFactoryTrack,
VOXFactoryVocalClip,
VOXFactoryVocalTrack,
)
from .options import OutputOptions


@dataclasses.dataclass
class VOXFactoryGenerator:
options: OutputOptions
audio_paths: dict[str, pathlib.Path] = dataclasses.field(default_factory=dict)

def generate_project(self, project: Project) -> VOXFactoryProject:
vox_project = VOXFactoryProject(
tempo=self.generate_tempo(project.song_tempo_list),
time_signature=self.generate_time_signature(project.time_signature_list),
track_bank=self.generate_tracks(project.track_list),
)
vox_project.track_order = sorted(vox_project.track_bank.keys())
return vox_project

def generate_tempo(self, tempos: list[SongTempo]) -> float:
return tempos[0].bpm if tempos else DEFAULT_BPM

def generate_time_signature(self, time_signatures: list[TimeSignature]) -> list[int]:
if time_signatures:
return [time_signatures[0].numerator, time_signatures[0].denominator]
else:
return [4, 4]

def generate_tracks(self, tracks: list[Track]) -> dict[str, VOXFactoryTrack]:
track_bank = {}
for i, track in enumerate(tracks):
if isinstance(track, SingingTrack):
note_list = self.generate_notes(track.note_list)
clip_bank = {f"clip_{i}": clip for i, clip in enumerate(note_list)}
clip_order = sorted(clip_bank.keys())
track_bank[str(i)] = VOXFactoryVocalTrack(
clip_bank=clip_bank,
clip_order=clip_order,
)
return track_bank

def generate_notes(self, notes: list[Note]) -> dict[str, VOXFactoryVocalClip]:
note_bank = {}
note_order = []
for i, note in enumerate(notes):
note_bank[f"note_{i}"] = self.generate_note(note)
note_order.append(f"note_{i}")
return {
"clip": VOXFactoryVocalClip(
note_bank=note_bank,
note_order=note_order,
),
}

def generate_note(self, note: Note) -> VOXFactoryNote:
return VOXFactoryNote(
ticks=note.start_pos,
duration_ticks=note.length,
midi=note.key_number,
name=note.lyric,
syllable=note.pronunciation,
)
Loading

0 comments on commit 549d49c

Please sign in to comment.