55using Il2CppPhoton . Voice . PUN ;
66using Il2CppPhoton . Voice . Unity ;
77using Il2CppRUMBLE . Managers ;
8+ using Il2CppSystem ;
9+ using MelonLoader ;
810using MelonLoader . Utils ;
11+ using NAudio . Wave ;
912using ReplayMod . Replay . Serialization ;
1013using UnityEngine ;
1114
@@ -16,76 +19,165 @@ namespace ReplayMod.Replay;
1619// Kept for future ideas
1720internal static class ReplayVoices
1821{
19- // Remote
2022 private static PunVoiceClient voice ;
21-
23+
2224 private static readonly Dictionary < ( int playerId , int voiceId ) , VoiceStreamWriter > writers = new ( ) ;
23- public static string tempVoiceDir = Path . Combine ( MelonEnvironment . UserDataDirectory , "ReplayMod" , "TempVoices" ) ;
24-
25- public static List < VoiceTrackInfo > voiceTrackInfos = new ( ) ;
2625
27- public static void HookRemote ( )
26+ public static string TempVoiceDir = Path . Combine ( MelonEnvironment . UserDataDirectory , "ReplayMod" , "TempVoices" ) ;
27+
28+ public static List < VoiceTrackInfo > VoiceTrackInfos = new ( ) ;
29+
30+ private static bool isRecording ;
31+ private static bool subscribed ;
32+
33+ public static void StartRecording ( )
2834 {
35+ isRecording = true ;
36+
2937 voice ??= PunVoiceClient . Instance ;
30-
31- voice . RemoteVoiceAdded += ( Il2CppSystem . Action < RemoteVoiceLink > ) ( OnRemoteVoiceAdded ) ;
32-
33- Directory . CreateDirectory ( tempVoiceDir ) ;
38+
39+ if ( voice is not null )
40+ {
41+ if ( ! subscribed )
42+ {
43+ voice . RemoteVoiceAdded += ( Action < RemoteVoiceLink > ) OnVoiceLinkAdded ;
44+ subscribed = true ;
45+ }
46+
47+ foreach ( var remoteVoiceLink in voice . cachedRemoteVoices )
48+ OnVoiceLinkAdded ( remoteVoiceLink ) ;
49+
50+ Directory . CreateDirectory ( TempVoiceDir ) ;
51+ }
3452 }
35-
36- public static void OnRemoteVoiceAdded ( RemoteVoiceLink link )
53+
54+ public static void OnVoiceLinkAdded ( RemoteVoiceLink link )
3755 {
3856 int playerId = link . PlayerId ;
3957 int voiceId = link . VoiceId ;
4058
59+ MelonLogger . Msg ( $ "VoiceAdded actor={ playerId } voice={ voiceId } ") ;
60+
4161 string name = PlayerManager . instance . AllPlayers . ToArray ( )
42- . FirstOrDefault ( p => p . Data . GeneralData . ActorNo == playerId ) ?
43- . Data . GeneralData . PublicUsername
44- ?? $ "Unknown";
62+ . FirstOrDefault ( p => p . Data . GeneralData . ActorNo == playerId ) ?
63+ . Data . GeneralData . PublicUsername
64+ ?? "Unknown" ;
4565
4666 name = Utilities . CleanName ( name ) ;
4767
48- string fileName = $ " { name } _actor_ { playerId } _voice_ { voiceId } .ogg" ;
49-
50- voiceTrackInfos . Add ( new VoiceTrackInfo
68+ var key = ( playerId , voiceId ) ;
69+
70+ link . FloatFrameDecoded += ( Action < FrameOut < float > > ) ( ( FrameOut < float > frame ) =>
5171 {
52- ActorId = playerId ,
53- FileName = fileName ,
54- StartTime = Time . time
55- } ) ;
72+ if ( ! isRecording )
73+ return ;
74+
75+ if ( ! writers . TryGetValue ( key , out var writer ) )
76+ {
77+ string fileName = $ "{ name } _actor_{ playerId } _voice_{ voiceId } _{ Time . frameCount } .wav";
78+ string path = Path . Combine ( TempVoiceDir , fileName ) ;
5679
57- string path = Path . Combine (
58- tempVoiceDir ,
59- fileName
60- ) ;
80+ writer = new VoiceStreamWriter (
81+ playerId ,
82+ link . VoiceInfo . SamplingRate ,
83+ link . VoiceInfo . Channels ,
84+ path
85+ ) ;
6186
62- var writer = new VoiceStreamWriter (
63- playerId ,
64- link . VoiceInfo . SamplingRate ,
65- link . VoiceInfo . Channels ,
66- path
67- ) ;
87+ writers [ key ] = writer ;
6888
69- var key = ( playerId , voiceId ) ;
70- writers [ key ] = writer ;
89+ VoiceTrackInfos . Add ( new VoiceTrackInfo (
90+ playerId ,
91+ fileName ,
92+ Time . time
93+ ) ) ;
7194
72- link . FloatFrameDecoded += ( Il2CppSystem . Action < FrameOut < float > > ) ( ( FrameOut < float > frame ) =>
73- {
74- if ( ! writers . TryGetValue ( key , out var w ) )
75- return ;
95+ MelonLogger . Msg ( $ "VoiceClipStart actor={ playerId } voice={ voiceId } ") ;
96+ }
7697
77- w . Write ( frame . Buf ) ;
98+ writer . Write ( frame . Buf ) ;
7899
79100 if ( frame . EndOfStream )
101+ {
102+ MelonLogger . Msg ( $ "VoiceClipEnd actor={ playerId } voice={ voiceId } ") ;
80103 StopWriter ( key ) ;
104+ }
81105 } ) ;
82106
83- link . RemoteVoiceRemoved += ( Il2CppSystem . Action ) ( ( ) =>
107+ link . RemoteVoiceRemoved += ( Action ) ( ( ) =>
84108 {
109+ MelonLogger . Msg ( $ "VoiceRemoved actor={ playerId } voice={ voiceId } ") ;
85110 StopWriter ( key ) ;
86111 } ) ;
87112 }
88113
114+ public static void StopRecording ( )
115+ {
116+ isRecording = false ;
117+
118+ foreach ( var key in writers . Keys . ToList ( ) )
119+ StopWriter ( key ) ;
120+
121+ MergeVoiceClips ( ) ;
122+
123+ VoiceTrackInfos . Clear ( ) ;
124+ }
125+
126+ private static void MergeVoiceClips ( )
127+ {
128+ if ( VoiceTrackInfos . Count == 0 )
129+ return ;
130+
131+ var groups = VoiceTrackInfos . GroupBy ( v => v . ActorId ) ;
132+
133+ foreach ( var group in groups )
134+ {
135+ var sorted = group . OrderBy ( v => v . StartTime ) . ToList ( ) ;
136+
137+ string outputPath = Path . Combine ( TempVoiceDir , $ "actor_{ group . Key } _merged.wav") ;
138+
139+ int sampleRate = 48000 ;
140+ int channels = 1 ;
141+
142+ using var output = new WaveFileWriter (
143+ outputPath ,
144+ WaveFormat . CreateIeeeFloatWaveFormat ( sampleRate , channels )
145+ ) ;
146+
147+ float lastTime = sorted [ 0 ] . StartTime ;
148+
149+ foreach ( var clip in sorted )
150+ {
151+ string path = Path . Combine ( TempVoiceDir , clip . FileName ) ;
152+
153+ if ( ! File . Exists ( path ) )
154+ continue ;
155+
156+ using var reader = new WaveFileReader ( path ) ;
157+
158+ float gap = clip . StartTime - lastTime ;
159+
160+ if ( gap > 0 )
161+ {
162+ int silentSamples = ( int ) ( gap * sampleRate ) ;
163+ float [ ] silence = new float [ silentSamples ] ;
164+ output . WriteSamples ( silence , 0 , silence . Length ) ;
165+ }
166+
167+ var sampleProvider = reader . ToSampleProvider ( ) ;
168+ float [ ] buffer = new float [ sampleRate ] ;
169+
170+ int read ;
171+ while ( ( read = sampleProvider . Read ( buffer , 0 , buffer . Length ) ) > 0 )
172+ {
173+ output . WriteSamples ( buffer , 0 , read ) ;
174+ }
175+
176+ lastTime = clip . StartTime + ( float ) reader . TotalTime . TotalSeconds ;
177+ }
178+ }
179+ }
180+
89181 private static void StopWriter ( ( int playerId , int voiceId ) key )
90182 {
91183 if ( ! writers . Remove ( key , out var writer ) )
@@ -103,8 +195,8 @@ public class VoiceStreamWriter
103195 public readonly string Path ;
104196 public bool HasFrames { get ; private set ; }
105197
106- private readonly FileStream file ;
107- // private readonly OpusOggWriteStream ogg ;
198+ private readonly FileStream fileStream ;
199+ private readonly WaveFileWriter wav ;
108200
109201 public VoiceStreamWriter (
110202 int actorId ,
@@ -116,21 +208,11 @@ string path
116208 ActorId = actorId ;
117209 Path = path ;
118210
119- file = File . Create ( path ) ;
120-
121- // Causes system.runtime error
122- // var encoder = new OpusEncoder(
123- // sampleRate,
124- // channels,
125- // OpusApplication.OPUS_APPLICATION_VOIP
126- // );
127-
128- // encoder.Bitrate = 24000;
129- // encoder.UseVBR = true;
130- // encoder.UseDTX = true;
131- // encoder.Complexity = 6;
132- //
133- // ogg = new OpusOggWriteStream(encoder, file);
211+ fileStream = File . Create ( path ) ;
212+ wav = new WaveFileWriter (
213+ fileStream ,
214+ WaveFormat . CreateIeeeFloatWaveFormat ( sampleRate , channels )
215+ ) ;
134216 }
135217
136218 public void Write ( float [ ] samples )
@@ -139,13 +221,13 @@ public void Write(float[] samples)
139221 return ;
140222
141223 HasFrames = true ;
142- // ogg .WriteSamples(samples, 0, samples.Length);
224+ wav . WriteSamples ( samples , 0 , samples . Length ) ;
143225 }
144226
145227 public void Dispose ( )
146228 {
147- // ogg.Finish ();
148- file ? . Dispose ( ) ;
229+ wav . Dispose ( ) ;
230+ fileStream ? . Dispose ( ) ;
149231 }
150232 }
151233}
0 commit comments