1
1
import dataclasses
2
+ import functools
2
3
import math
3
4
import pathlib
4
5
import secrets
5
6
6
- from libresvip .core .constants import DEFAULT_BPM , TICKS_IN_BEAT
7
+ import more_itertools
8
+ import portion
9
+
10
+ from libresvip .core .constants import DEFAULT_BPM , DEFAULT_PHONEME , TICKS_IN_BEAT
11
+ from libresvip .core .time_interval import PiecewiseIntervalDict
7
12
from libresvip .core .time_sync import TimeSynchronizer
8
- from libresvip .model .base import Note , Project , SingingTrack , SongTempo , TimeSignature , Track
13
+ from libresvip .model .base import (
14
+ InstrumentalTrack ,
15
+ Note ,
16
+ ParamCurve ,
17
+ Params ,
18
+ Project ,
19
+ SongTempo ,
20
+ TimeSignature ,
21
+ Track ,
22
+ )
23
+ from libresvip .utils .audio import audio_track_info
24
+ from libresvip .utils .music_math import linear_interpolation
9
25
10
26
from .model import (
27
+ VOXFactoryAudioClip ,
28
+ VOXFactoryAudioData ,
29
+ VOXFactoryAudioTrack ,
11
30
VOXFactoryNote ,
12
31
VOXFactoryProject ,
13
32
VOXFactoryTrack ,
@@ -23,17 +42,20 @@ class VOXFactoryGenerator:
23
42
prefix : str = dataclasses .field (init = False )
24
43
audio_paths : dict [str , pathlib .Path ] = dataclasses .field (default_factory = dict )
25
44
synchronizer : TimeSynchronizer = dataclasses .field (init = False )
45
+ first_bar_length : int = dataclasses .field (init = False )
26
46
27
47
def generate_project (self , project : Project ) -> VOXFactoryProject :
28
48
self .prefix = secrets .token_hex (5 )
49
+ self .first_bar_length = int (project .time_signature_list [0 ].bar_length ())
29
50
self .synchronizer = TimeSynchronizer (project .song_tempo_list )
30
- vox_project = VOXFactoryProject (
51
+ track_bank , audio_data_bank = self .generate_tracks (project .track_list )
52
+ return VOXFactoryProject (
31
53
tempo = self .generate_tempo (project .song_tempo_list ),
32
54
time_signature = self .generate_time_signature (project .time_signature_list ),
33
- track_bank = self .generate_tracks (project .track_list ),
55
+ track_bank = track_bank ,
56
+ track_order = sorted (track_bank .keys ()),
57
+ audio_data_bank = audio_data_bank ,
34
58
)
35
- vox_project .track_order = sorted (vox_project .track_bank .keys ())
36
- return vox_project
37
59
38
60
def generate_tempo (self , tempos : list [SongTempo ]) -> float :
39
61
return tempos [0 ].bpm if tempos else DEFAULT_BPM
@@ -44,25 +66,60 @@ def generate_time_signature(self, time_signatures: list[TimeSignature]) -> list[
44
66
else :
45
67
return [4 , 4 ]
46
68
47
- def generate_tracks (self , tracks : list [Track ]) -> dict [str , VOXFactoryTrack ]:
69
+ def generate_tracks (
70
+ self , tracks : list [Track ]
71
+ ) -> tuple [dict [str , VOXFactoryTrack ], dict [str , VOXFactoryAudioData ]]:
48
72
track_bank = {}
73
+ audio_data_bank = {}
49
74
for i , track in enumerate (tracks ):
50
- if isinstance (track , SingingTrack ):
51
- clip_bank = self .generate_notes (track .note_list )
75
+ if isinstance (track , InstrumentalTrack ):
76
+ audio_path = pathlib .Path (track .audio_file_path )
77
+ if (track_info := audio_track_info (track .audio_file_path )) is not None :
78
+ source_audio_data_key = f"{ self .prefix } -au{ i } { audio_path .suffix } "
79
+ self .audio_paths [source_audio_data_key ] = audio_path
80
+ audio_data_bank [source_audio_data_key ] = VOXFactoryAudioData (
81
+ sample_rate = track_info .sampling_rate ,
82
+ sample_length = int (track_info .duration * track_info .sampling_rate / 1000 ),
83
+ number_of_channels = track_info .channel_s ,
84
+ )
85
+ clip_bank = {
86
+ f"{ self .prefix } -cl0" : VOXFactoryAudioClip (
87
+ name = audio_path .stem ,
88
+ offset_quarter = 0 ,
89
+ start_quarter = track .offset / TICKS_IN_BEAT ,
90
+ length = track_info .duration / 1000 ,
91
+ source_audio_data_key = source_audio_data_key ,
92
+ )
93
+ }
94
+ clip_order = [f"{ self .prefix } -cl0" ]
95
+ track_bank [f"{ self .prefix } -tr{ i } " ] = VOXFactoryAudioTrack (
96
+ clip_bank = clip_bank ,
97
+ clip_order = clip_order ,
98
+ name = track .title ,
99
+ mute = track .mute ,
100
+ solo = track .solo ,
101
+ pan = track .pan ,
102
+ )
103
+ else :
104
+ clip_bank = self .generate_notes (track .note_list , track .edited_params )
52
105
clip_order = sorted (clip_bank .keys ())
53
106
track_bank [f"{ self .prefix } -tr{ i } " ] = VOXFactoryVocalTrack (
54
107
clip_bank = clip_bank ,
55
108
clip_order = clip_order ,
109
+ name = track .title ,
110
+ mute = track .mute ,
111
+ solo = track .solo ,
112
+ pan = track .pan ,
56
113
)
57
- return track_bank
114
+ return track_bank , audio_data_bank
58
115
59
- def generate_notes (self , notes : list [Note ]) -> dict [str , VOXFactoryVocalClip ]:
116
+ def generate_notes (self , notes : list [Note ], params : Params ) -> dict [str , VOXFactoryVocalClip ]:
60
117
note_bank = {}
61
118
note_order = []
62
119
max_ticks = notes [- 1 ].end_pos if notes else 0
63
120
max_quarter = max_ticks / TICKS_IN_BEAT
64
121
for i , note in enumerate (notes ):
65
- note_bank [f"{ self .prefix } -no{ i } " ] = self .generate_note (note )
122
+ note_bank [f"{ self .prefix } -no{ i } " ] = self .generate_note (note , params )
66
123
note_order .append (f"{ self .prefix } -no{ i } " )
67
124
clip_count = math .ceil (max_quarter / 32 )
68
125
clip_bank = {}
@@ -76,13 +133,48 @@ def generate_notes(self, notes: list[Note]) -> dict[str, VOXFactoryVocalClip]:
76
133
)
77
134
return clip_bank
78
135
79
- def generate_note (self , note : Note ) -> VOXFactoryNote :
136
+ def generate_note (self , note : Note , params : Params ) -> VOXFactoryNote :
80
137
note_start_time = self .synchronizer .get_actual_secs_from_ticks (note .start_pos )
81
138
return VOXFactoryNote (
82
139
time = note_start_time ,
83
140
ticks = note .start_pos ,
84
141
duration_ticks = note .length ,
85
142
midi = note .key_number ,
86
143
name = note .lyric ,
87
- syllable = note .pronunciation ,
144
+ syllable = note .pronunciation or DEFAULT_PHONEME ,
145
+ pitch_bends = self .generate_note_pitch (note , params .pitch ),
88
146
)
147
+
148
+ def generate_note_pitch (self , note : Note , pitch : ParamCurve ) -> list [float ]:
149
+ note_start_time = self .synchronizer .get_actual_secs_from_ticks (note .start_pos )
150
+ note_end_time = self .synchronizer .get_actual_secs_from_ticks (note .end_pos )
151
+ key_interval_dict = PiecewiseIntervalDict ()
152
+ secs_step = 1024 / 44100
153
+ prev_secs = None
154
+ prev_key : float = - 1
155
+ for point in pitch .points .root :
156
+ if point .x - self .first_bar_length < note .start_pos :
157
+ continue
158
+ elif point .x - self .first_bar_length > note .end_pos :
159
+ break
160
+ if point .y == - 100 :
161
+ prev_secs = None
162
+ prev_key = 0
163
+ else :
164
+ secs = self .synchronizer .get_actual_secs_from_ticks (point .x - self .first_bar_length )
165
+ key = point .y / 100
166
+ if prev_secs is not None :
167
+ key_interval_dict [portion .openclosed (prev_secs , secs )] = functools .partial (
168
+ linear_interpolation ,
169
+ start = (prev_secs , prev_key - note .key_number ),
170
+ end = (secs , key - note .key_number ),
171
+ )
172
+ else :
173
+ key_interval_dict [portion .singleton (secs )] = key - note .key_number
174
+ prev_secs = secs
175
+ prev_key = key
176
+ pitch_bends = [
177
+ key_interval_dict .get (secs , 0 )
178
+ for secs in more_itertools .numeric_range (note_start_time , note_end_time , secs_step )
179
+ ]
180
+ return pitch_bends if any (pitch_bends ) else []
0 commit comments