Skip to content

Commit 3ea2949

Browse files
committed
Further refine chorus and reverb.
1 parent 82d3746 commit 3ea2949

4 files changed

Lines changed: 51 additions & 96 deletions

File tree

SpeechService/EddiSpeech.cs

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ public class EddiSpeech
1111
public string eventType { get; private set; }
1212

1313
// Calculated SpeechFX data
14-
public int echoDelay { get; set; }
15-
public int chorusLevel { get; set; }
16-
public int reverbLevel { get; set; }
17-
public int distortionLevel { get; set; }
14+
public int echoDelay { get; }
15+
public int distortionLevel { get; }
1816

1917
public EddiSpeech ( string message, string voice = null, int priority = 3,
20-
string eventType = null, int configFxLevel = 0, LandingPadSize shipSize = null,
18+
string eventType = null, LandingPadSize shipSize = null,
2119
decimal? shipHealth = 100M, bool radio = false, bool distortOnDamage = false )
2220
{
2321
this.message = message;
@@ -27,16 +25,8 @@ public EddiSpeech ( string message, string voice = null, int priority = 3,
2725
this.eventType = eventType;
2826

2927
// Resolve the SpeechFX settings
30-
this.echoDelay = GetEchoDelay( shipSize );
31-
this.chorusLevel = GetChorusLevel( configFxLevel );
32-
this.reverbLevel = GetReverbLevel( configFxLevel );
33-
this.distortionLevel = GetDistortionLevel( distortOnDamage, shipHealth );
34-
}
35-
36-
private static int GetChorusLevel ( int configFxLevel )
37-
{
38-
// This is not affected by ship parameters
39-
return (int)( 60 * ( configFxLevel / 100M ) );
28+
echoDelay = GetEchoDelay( shipSize );
29+
distortionLevel = GetDistortionLevel( distortOnDamage, shipHealth );
4030
}
4131

4232
private static int GetDistortionLevel ( bool distortOnDamage, decimal? shipHealth )
@@ -73,11 +63,5 @@ private static int GetEchoDelay ( LandingPadSize size )
7363

7464
return echoDelayMs;
7565
}
76-
77-
private static int GetReverbLevel ( int configFxLevel )
78-
{
79-
// This is not affected by ship parameters
80-
return (int)( 80 * ( configFxLevel / 100M ) );
81-
}
8266
}
8367
}

SpeechService/SpeechEffects/SpeechFX.cs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ namespace EddiSpeechService.SpeechEffects
1212
{
1313
public static class SpeechFX
1414
{
15-
private static SpeechServiceConfiguration Configuration => SpeechService.Instance.Configuration;
16-
17-
public static IWaveProvider addEffectsToSource ( Stream stream, int chorusLevel, int reverbLevel, int echoDelay, int distortionLevel, bool radio )
15+
public static IWaveProvider addEffectsToSource ( Stream stream, int targetVolume, int fxLevel, int distortionLevel, int echoDelay, bool radio )
1816
{
1917
using ( var source = new WaveFileReader( stream ) )
2018
{
@@ -32,12 +30,14 @@ public static IWaveProvider addEffectsToSource ( Stream stream, int chorusLevel,
3230

3331
var signal = new DiscreteSignal( source.WaveFormat.SampleRate, trimmedBuffer );
3432
var sampleRate = source.WaveFormat.SampleRate;
35-
var damageAdjustedFxLevel = FxParameters.DamageAdjustedFxLevel( distortionLevel, Configuration.EffectsLevel );
33+
var damageAdjustedFxLevel = FxParameters.DamageAdjustedFxLevel( distortionLevel, fxLevel );
34+
35+
Logging.Debug( $"Effects level is {damageAdjustedFxLevel}, echo delay is {echoDelay}" );
3636

37-
Logging.Debug( $"Effects level is {damageAdjustedFxLevel}, chorus level is {chorusLevel}, reverb level is {reverbLevel}, echo delay is {echoDelay}" );
37+
var resolvedReverbLevel = 0;
3838

3939
// Chorus - We always apply chorus effects.
40-
signal = ApplyChorus(signal, chorusLevel, damageAdjustedFxLevel, sampleRate);
40+
signal = ApplyChorus(signal, damageAdjustedFxLevel, sampleRate, out var resolvedChorusLevel);
4141

4242
// Radio (highpass)
4343
if ( radio )
@@ -51,13 +51,13 @@ public static IWaveProvider addEffectsToSource ( Stream stream, int chorusLevel,
5151
signal = ApplyEcho( signal, echoDelay, sampleRate );
5252

5353
// Reverb
54-
signal = ApplyReverb( signal, reverbLevel, damageAdjustedFxLevel, sampleRate );
54+
signal = ApplyReverb( signal, damageAdjustedFxLevel, sampleRate, out resolvedReverbLevel );
5555

5656
// Distortion (apply last)
5757
signal = ApplyDamageDistortion( signal, distortionLevel );
5858
}
5959

60-
signal = new DiscreteSignal( signal.SamplingRate, NormalizeSamples( signal ) );
60+
signal = new DiscreteSignal( signal.SamplingRate, NormalizeSamples( signal, targetVolume ) );
6161

6262
// Generate the processed signal in mono
6363
var processedSampleProvider = new DiscreteSignalSampleProvider( signal ).ToMono();
@@ -71,7 +71,7 @@ public static IWaveProvider addEffectsToSource ( Stream stream, int chorusLevel,
7171
// Convert to 16-bit PCM (typical format for standard audio)
7272
IWaveProvider waveProvider = new SampleToWaveProvider16( processedSampleProvider );
7373

74-
if ( chorusLevel != 0 || reverbLevel != 0 || echoDelay != 0 )
74+
if ( resolvedChorusLevel != 0 || resolvedReverbLevel != 0 || echoDelay != 0 )
7575
{
7676
var extMs = Convert.ToInt32( 500 + Math.Max( 0, ( damageAdjustedFxLevel - 50 ) * 10 ) );
7777
Logging.Debug( "Extending duration by " + extMs + "ms" );
@@ -82,12 +82,13 @@ public static IWaveProvider addEffectsToSource ( Stream stream, int chorusLevel,
8282
}
8383
}
8484

85-
private static DiscreteSignal ApplyChorus ( DiscreteSignal signal, int chorusLevel, int damageAdjustedFxLevel, int sampleRate )
85+
private static DiscreteSignal ApplyChorus ( DiscreteSignal signal, int damageAdjustedFxLevel, int sampleRate, out int resolvedChorusLevel )
8686
{
87-
if ( chorusLevel != 0 )
87+
resolvedChorusLevel = (int)( 60 * ( damageAdjustedFxLevel / 100M ) );
88+
if ( resolvedChorusLevel != 0 )
8889
{
89-
var rate = Math.Max( 0.1f, chorusLevel / 20f ); // e.g., 1–5 Hz Frequency scaling
90-
var width = Math.Max(0.005f, damageAdjustedFxLevel / 10f * .001f); // 5-10 ms delay
90+
var rate = Math.Max( 0.1f, resolvedChorusLevel / 20f ); // e.g., 1–5 Hz Frequency scaling
91+
var width = Math.Max(0.005f, resolvedChorusLevel / 10f * .001f); // 5-10 ms delay
9192

9293
var chorus = new ChorusEffect( sampleRate, new[] { rate }, new[] { width } );
9394
chorus.WetDryMix( damageAdjustedFxLevel / 100f );
@@ -143,14 +144,15 @@ private static DiscreteSignal ApplyRadio (DiscreteSignal signal, int sampleRate)
143144
return signal;
144145
}
145146

146-
private static DiscreteSignal ApplyReverb ( DiscreteSignal signal, int reverbLevel, int damageAdjustedFxLevel, int sampleRate )
147+
private static DiscreteSignal ApplyReverb ( DiscreteSignal signal, int damageAdjustedFxLevel, int sampleRate, out int resolvedReverbLevel )
147148
{
148-
if ( reverbLevel != 0 )
149+
resolvedReverbLevel = (int)( 80 * ( damageAdjustedFxLevel / 100M ) );
150+
if ( resolvedReverbLevel != 0 )
149151
{
150152
// Map reverbLevel to number of echoes and decay
151-
var numEchoes = 1 + ( reverbLevel / 50 ); // 1 to 3 echoes
152-
var baseDecay = 0.1f + ( 0.2f * ( reverbLevel / 100f ) ); // 0.1 to 0.3
153-
var baseDelayMs = 30 + (int)( 120 * ( reverbLevel / 100f ) ); // 30ms to 150ms
153+
var numEchoes = 1 + ( resolvedReverbLevel / 50 ); // 1 to 3 echoes
154+
var baseDecay = 0.1f + ( 0.2f * ( resolvedReverbLevel / 100f ) ); // 0.1 to 0.3
155+
var baseDelayMs = 30 + (int)( 120 * ( resolvedReverbLevel / 100f ) ); // 30ms to 150ms
154156

155157
for ( var i = 0; i < numEchoes; i++ )
156158
{
@@ -166,7 +168,7 @@ private static DiscreteSignal ApplyReverb ( DiscreteSignal signal, int reverbLev
166168
return signal;
167169
}
168170

169-
private static float[] NormalizeSamples ( DiscreteSignal signal )
171+
private static float[] NormalizeSamples ( DiscreteSignal signal, int targetVolume )
170172
{
171173
var samples = signal.Samples;
172174

@@ -189,7 +191,7 @@ private static float[] NormalizeSamples ( DiscreteSignal signal )
189191
}
190192
}
191193

192-
var volumeScale = Math.Max(0, Math.Min(100, Configuration.Volume)) / 100f;
194+
var volumeScale = Math.Max(0, Math.Min(100, targetVolume)) / 100f;
193195
for ( var i = 0; i < samples.Length; i++ )
194196
{
195197
// Apply configuration volume scaling (percent 0-100)

SpeechService/SpeechService.cs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,7 @@ public void Say(Ship ship, string message, int priority = 3, string voice = null
225225
if (string.IsNullOrEmpty(message)) { return; }
226226

227227
// Queue the current speech
228-
var queuingSpeech = new EddiSpeech( message, voice, priority, eventType,
229-
Configuration.EffectsLevel, ship?.Size, ship?.health, radio, Configuration.DistortOnDamage );
228+
var queuingSpeech = new EddiSpeech( message, voice, priority, eventType, ship?.Size, ship?.health, radio, Configuration.DistortOnDamage );
230229
speechQueue.Enqueue( queuingSpeech );
231230

232231
// Check the first item in the speech queue
@@ -298,12 +297,13 @@ internal bool checkSpeechInterrupt ( int peekedSpeechPriority )
298297
return false;
299298
}
300299

301-
public static void Speak(EddiSpeech speech)
300+
public void Speak(EddiSpeech speech)
302301
{
303-
Instance.Speak(speech.message, speech.voice, speech.echoDelay, speech.distortionLevel, speech.chorusLevel, speech.reverbLevel, speech.radio, speech.priority);
302+
Instance.Speak(speech.message, speech.voice, Configuration.EffectsLevel, Configuration.Volume, speech.distortionLevel, speech.echoDelay, speech.priority, speech.radio );
304303
}
305304

306-
public void Speak(string speech, string defaultVoice, int echoDelay, int distortionLevel, int chorusLevel, int reverbLevel, bool radio = false, int priority = 3)
305+
public void Speak ( string speech, string defaultVoice, int fxLevel, int volume = 95,
306+
int distortionLevel = 0, int echoDelay = 0, int priority = 3, bool radio = false )
307307
{
308308
if (speech == null || speech.Trim() == "") { return; }
309309

@@ -314,19 +314,19 @@ public void Speak(string speech, string defaultVoice, int echoDelay, int distort
314314
}
315315

316316
discardPendingSegments = false;
317-
List<string> segments = SpeechFormatter.SeparateSpeechSegments(speech);
317+
var segments = SpeechFormatter.SeparateSpeechSegments(speech);
318318

319-
foreach (string segment in segments)
319+
foreach (var segment in segments)
320320
{
321321
if ( discardPendingSegments ) { continue; }
322322

323323
string voice = null;
324-
string statement = segment;
324+
var statement = segment;
325325

326-
bool isAudio = segment.Contains("<audio"); // This is an audio file, we will disable voice effects processing
326+
var isAudio = segment.Contains("<audio"); // This is an audio file, we will disable voice effects processing
327327
if (isAudio)
328328
{
329-
SpeechFormatter.UnpackAudioTags(segment, out string fileName, out bool async, out decimal? volumeOverride);
329+
SpeechFormatter.UnpackAudioTags(segment, out var fileName, out var async, out var volumeOverride);
330330
try
331331
{
332332
// Play the audio, waiting for the audio to complete unless we're in async mode
@@ -355,21 +355,21 @@ public void Speak(string speech, string defaultVoice, int echoDelay, int distort
355355
continue;
356356
}
357357

358-
bool isRadio = statement.Contains("<transmit") || radio;
358+
var isRadio = statement.Contains("<transmit") || radio;
359359
if (isRadio)
360360
{
361361
// This is a radio transmission, we will enable radio voice effects processing
362362
statement = SpeechFormatter.StripRadioTags( statement );
363363
}
364364

365-
bool isVoice = statement.Contains("<voice");
365+
var isVoice = statement.Contains("<voice");
366366
if (isVoice)
367367
{
368368
// This is a voice override
369369
SpeechFormatter.UnpackVoiceTags( statement, out voice, out statement );
370370
}
371371

372-
using (Stream stream = getSpeechStream(voice ?? defaultVoice, statement))
372+
using (var stream = getSpeechStream(voice ?? defaultVoice, statement))
373373
{
374374
if (stream == null)
375375
{
@@ -388,14 +388,14 @@ public void Speak(string speech, string defaultVoice, int echoDelay, int distort
388388
Logging.Debug("Seeking back to the beginning of the stream");
389389
stream.Seek(0, SeekOrigin.Begin);
390390

391-
var provider = SpeechFX.addEffectsToSource(stream, chorusLevel, reverbLevel, echoDelay, distortionLevel, isRadio);
391+
var provider = SpeechFX.addEffectsToSource(stream, volume, fxLevel, distortionLevel, echoDelay, isRadio );
392392
PlaySpeechStreamAsync( provider, priority ).GetAwaiter().GetResult();
393393
}
394394
}
395395
}
396396

397397
// Obtain the speech memory stream
398-
public Stream getSpeechStream ( string requestedVoice, string speech )
398+
private Stream getSpeechStream ( string requestedVoice, string speech )
399399
{
400400
try
401401
{

Tests/SpeechTests.cs

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void start ()
2929
[DataRow( @"<phoneme alphabet=""ipa"" ph=""iˈlɛktrə"">Electra</phoneme>" )]
3030
public void TestPhonetics (string inputSpeech)
3131
{
32-
SpeechService.Speak(new EddiSpeech(inputSpeech));
32+
SpeechService.Instance.Speak(new EddiSpeech(inputSpeech));
3333
}
3434

3535
[TestMethod, DoNotParallelize]
@@ -126,17 +126,8 @@ public void TestPowerplay ()
126126
[DataRow( 100 )]
127127
public void TestDamageDistortion (int shipHealth)
128128
{
129-
var speech = new EddiSpeech( $"Systems at {shipHealth}%.", null, 0, null,
130-
50, LandingPadSize.Large, shipHealth, false, true );
131-
SpeechService.Speak( speech );
132-
}
133-
134-
[TestMethod, DoNotParallelize]
135-
public void TestVariants ()
136-
{
137-
SpeechService.Instance.Say( ShipDefinitions.FromEDModel( "Vulture" ), "Welcome to your Vulture. Weapons online." );
138-
SpeechService.Instance.Say( ShipDefinitions.FromEDModel( "Python" ), "Welcome to your Python. Scanning at full range." );
139-
SpeechService.Instance.Say( ShipDefinitions.FromEDModel( "Anaconda" ), "Welcome to your Anaconda. All systems operational." );
129+
var speech = new EddiSpeech( $"Systems at {shipHealth}%.", null, 0, null, LandingPadSize.Large, shipHealth, false, true );
130+
SpeechService.Instance.Speak( speech );
140131
}
141132

142133
[DataTestMethod, DoNotParallelize]
@@ -146,17 +137,17 @@ public void TestVariants ()
146137
[DataRow( 60 )]
147138
[DataRow( 80 )]
148139
[DataRow( 100 )]
149-
public void TestChorus ( int chorusLevel )
140+
public void TestFxLevel ( int fxLevel )
150141
{
151-
SpeechService.Instance.Speak( $"Chorus level {chorusLevel}", null, 0, 0, chorusLevel, 0, false, 0 );
142+
SpeechService.Instance.Speak( $"Effects level {fxLevel}", null, fxLevel );
152143
}
153144

154145
[DataTestMethod, DoNotParallelize]
155146
[DataRow( "Your python has touched down." )]
156147
[DataRow( "Anaconda golf foxtrot lima one niner six eight returning from orbit." )]
157148
public void TestRadio (string msg)
158149
{
159-
SpeechService.Speak(new EddiSpeech(msg, radio: true));
150+
SpeechService.Instance.Speak(new EddiSpeech(msg, radio: true));
160151
}
161152

162153
[DataTestMethod, DoNotParallelize]
@@ -166,36 +157,14 @@ public void TestRadio (string msg)
166157
public void TestEchoDelay (int landingPadSize)
167158
{
168159
var shipSize = LandingPadSize.AllOfThem.First( s => s.sizeIndex == landingPadSize );
169-
SpeechService.Speak( new EddiSpeech( $"Echo delay for a {shipSize} ship.", shipSize: shipSize ) );
170-
}
171-
172-
[TestMethod, DoNotParallelize]
173-
public void TestDropOff ()
174-
{
175-
var synth = new SpeechSynthesizer();
176-
using ( var stream = new MemoryStream() )
177-
{
178-
synth.SetOutputToWaveStream( stream );
179-
synth.Speak( "Testing drop-off." );
180-
stream.Seek( 0, SeekOrigin.Begin );
181-
var source = new WaveFileReader(stream);
182-
var waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
183-
var soundOut = new WasapiOut();
184-
soundOut.Init( source );
185-
soundOut.PlaybackStopped += ( s, e ) => waitHandle.Set();
186-
soundOut.Play();
187-
waitHandle.WaitOne();
188-
soundOut.Dispose();
189-
source.Dispose();
190-
}
191-
SpeechService.Instance.Speak( "Testing drop-off.", null, 50, 1, 30, 40, true, 0 );
160+
SpeechService.Instance.Speak( new EddiSpeech( $"Echo delay for a {shipSize} ship.", shipSize: shipSize ) );
192161
}
193162

194163
[TestMethod, DoNotParallelize]
195164
public void TestSpeechNullInvalidVoice ()
196165
{
197-
SpeechService.Speak( new EddiSpeech( "Testing null voice", null ) );
198-
SpeechService.Speak( new EddiSpeech( "Testing non-valid voice", "No such voice" ) );
166+
SpeechService.Instance.Speak( new EddiSpeech( "Testing null voice", null ) );
167+
SpeechService.Instance.Speak( new EddiSpeech( "Testing non-valid voice", "No such voice" ) );
199168
}
200169
}
201170
}

0 commit comments

Comments
 (0)