starfighter006 Posted 6 hours ago Posted 6 hours ago Hi all, with the help of AI (Google Gemini), I was able to create some powershell scripts to mass-exchange the audio files for callsigns, i.e. T1FLGT, T1Grop, T1Lead, F1Lead, A1LDCS, by using the windows internal AI voices. Of course, the speech isn't perfect, but it's good enough for me to give the game some individual flavour. I tested it now within several missions and had no issues or CTDs so far. Maybe this is of interest to some of you, so I will share it with you. I did this on Windows 11 using Windows powershell, so this might not work on older operating systems. In general to identify which AI voices are available on your system, you can use this command in the powershell: Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name } On my system, it will show the following voices: Microsoft Hedda Desktop Microsoft Stefan Microsoft David Microsoft Katja Microsoft David Desktop Microsoft Zira Desktop Note: You might not have all voices on your system available at first, due to some Windows restrictions, but you can change that. Unfortunately, I don't remember what you had to do, but I did ask the AI about it and it provided me with the corresponding commands/script. I generally recommend to execute the powershell with Admin rights. This way, you are able to do more modifications to your system, especially regarding the adding of AI voices. Now let's get to the scripts. Below you will find a script to create 140 T1FLGT WAV files for your callsigns, e.g. Maverick 1 - 4, Hunter 1 - 4, etc... Here is a quick description of what you need to do: $Callsigns -> Here you enter your list of Callsigns you want to create $Voice -> Here you define your desired AI Voice And that's basically it. You save your script as "YourScript.ps1" and execute it within the powershell by entering the command: .\YourScript.ps1 The script will create the required T1FLGT001 - T1FLGT140 WAV Files for you, which you will need to copy to the Speech sub-folder of your Mod. The script also gives some basic radio noise and a volume boost. You can change this in $noise and $boost. In the same manner, you can create D1FLGT and W1FLGT files. You just need to rename the file in $Filename accordingly and change the voice from female to male AI, for example. Important note: Please be aware that if you also want to change the subtitles, you have to modify the SPEECHTEXT.STR with a hex editor and replace the original callsigns. But you have to be extra careful, as the string length of each callsign must never change, otherwise the subtitles will break. This means, that you need to add empty spaces (i.e. by pressing the space bar) to fill out the characters to exactly match the string size of your old callsign if your new callsign is shorter or you must abbreviate if it is to long. So again, if the string size is changed, i.e. lengthened or shortened, subtitles will break. I was able to change them all, so it is possible, but I recommend making backups inbetween. Script for changing T1FLGT (or D1FLGT, W1FLGT) # --- HIER DEINE OPTIMIERTE RUFZEICHEN-LISTE --- $Callsigns = @( # DefaultCallsign "Ente", "Gänserich", "Er-pel", "Schwan", "Kücken", # CallsignSet001 "Donald", "Dagobert", "Gustav", "Emil", "Fritz", # CallsignSet002 "Entenhausen", "Erpelheim", "Gansbach", "Erpelfurt", "Hundenhausen", # CallsignSet003 "Falke", "Adler", "Möwe", "Mäusebussard", "Habicht", # CallsignSet004 "Tiger", "Panther", "Puhma", "Löwe", "Leopard", # CallsignSet005 "Haifisch", "Walross", "Delphin", "Walfisch", "Hammerhai", # CallsignSet006 "Wolf", "Marder", "Luchs", "Fuchs", "Eule" ) $Voice = "Microsoft Hedda Desktop" Write-Host "Generiere 140 Funk-Dateien (4 Durchgänge für die Nummern 1 bis 4)..." Write-Host "------------------------------------------------------------------" Add-Type -AssemblyName System.Speech $Synth = New-Object System.Speech.Synthesis.SpeechSynthesizer try { $Synth.SelectVoice($Voice) } catch { Write-Host "Hinweis: Stimme '$Voice' nicht gefunden." -ForegroundColor Yellow } # Funktion für den sauberen PCM-Header und maximalen Lautstärke-Boost function Clean-WavForStrikeFighters { param([string]$FilePath) $bytes = [System.IO.File]::ReadAllBytes($FilePath) $rand = New-Object System.Random # Wir suchen dynamisch nach dem "data"-Chunk im Original-Header $dataPos = -1 for ($i = 12; $i -lt $bytes.Length - 8; $i++) { if ($bytes[$i] -eq 100 -and $bytes[$i+1] -eq 97 -and $bytes[$i+2] -eq 116 -and $bytes[$i+3] -eq 97) { # "data" in ASCII $dataPos = $i break } } # Falls kein gültiger Audio-Datenblock gefunden wurde, abbrechen if ($dataPos -eq -1) { return } # Die Position, an der die echten Audiodaten beginnen (nach "data" + 4 Bytes Längenangabe) $audioStart = $dataPos + 8 # Wir bearbeiten NUR die echten Audiodaten als saubere Ganzzahlen for ($i = $audioStart; $i -lt $bytes.Length - 1; $i += 2) { # 16-Bit Sample sauber auslesen $sample = [BitConverter]::ToInt16($bytes, $i) # Reiner, stabiler Lautstärke-Boost ohne Fließkomma-Drift + leichtes Funkrauschen $noise = $rand.Next(-80, 80) $boosted = [int]($sample * 3.5) + $noise # Harter Schutz vor Übersteuerung (Clipping-Grenzen für Int16) if ($boosted -gt 32767) { $boosted = 32767 } if ($boosted -lt -32768) { $boosted = -32768 } # Zurückschreiben in das originale Byte-Array $newBytes = [BitConverter]::GetBytes([int16]$boosted) $bytes[$i] = $newBytes[0] $bytes[$i+1] = $newBytes[1] } # Überschreibt die Datei mit dem perfekt erhaltenen Original-Header und den modifizierten Daten [System.IO.File]::WriteAllBytes($FilePath, $bytes) } # Globaler Dateizähler (für 001 bis 140) $FileCounter = 1 # Äußere Schleife: Die Nummern 1 bis 4 durchgehen for ($FlightNum = 1; $FlightNum -le 4; $FlightNum++) { Write-Host "--- Starte Durchgang für Nummer $FlightNum ---" -ForegroundColor Cyan # Innere Schleife: Alle 35 Rufzeichen sprechen for ($i = 0; $i -lt $Callsigns.Count; $i++) { $Name = $Callsigns[$i] # Setzt den Namen und die aktuelle Flight-Nummer (1, 2, 3 oder 4) fließend zusammen $SpokenText = $Name + " " + $FlightNum # Formatiert den globalen Zähler dreistellig (z.B. 001, 036, 071, 106) $NumString = $FileCounter.ToString("000") $Filename = "T1FLGT$NumString.wav" Write-Host "Erstelle ($FileCounter/140): '$Name $FlightNum' -> $Filename" $FullPath = Join-Path $PSScriptRoot $Filename $Format = New-Object System.Speech.AudioFormat.SpeechAudioFormatInfo(11025, 16, 1) $Synth.SetOutputToWaveFile($FullPath, $Format) $Synth.Speak($SpokenText) $Synth.SetOutputToNull() Clean-WavForStrikeFighters -FilePath $FullPath # Zähler für die nächste Datei erhöhen $FileCounter++ } } Write-Host "------------------------------------------------------------------" Write-Host "Fertig! Es wurden exakt 140 fortlaufende Dateien erzeugt." Read-Host "Drücken Sie Enter zum Beenden..." For creating F1Lead, A1LDCS, T1Lead or T1Group, I created a slightly different script, because I wanted to use e.g. the german word "Rottenführer" instead of the english word "lead". Therefore, the script creates two words "Rottenführer" and the callsign and merges it to one WAV file. This was necessary to put a little pause between both word to make it sound better. However, in your desired language, this might not necessary and you can probably change the order or just adapt the previous script. But here is a description on how the below script works: $Callsigns -> Again your list of callsigns $VoiceDE -> The desired AI voice $A1Filename -> "A1LDCS$NumString.wav" -> It created A1LDCS001 - A1LDCS036 $F1Filename -> "F1LEAD$NumString.wav" -> I used the same voice also for F1 Lead, so it just copies the files and renames them. $Synth.Speak("Rottenführer") -> Here you can enter whatever word you like in your preferred language. # --- DEINE RUFZEICHEN-LISTE (35 Einträge) --- $Callsigns = @( # DefaultCallsign "Ente", "Gänserich", "Er-pel", "Schwan", "Kücken", # CallsignSet001 "Donald", "Dagobert", "Gustav", "Emil", "Fritz", # CallsignSet002 "Entenhausen", "Erpelheim", "Gansbach", "Erpelfurt", "Hundenhausen", # CallsignSet003 "Falke", "Adler", "Möwe", "Mäusebussard", "Habicht", # CallsignSet004 "Tiger", "Panther", "Puhma", "Löwe", "Leopard", # CallsignSet005 "Haifisch", "Walross", "Delphin", "Walfisch", "Hammerhai", # CallsignSet006 "Wolf", "Marder", "Luchs", "Fuchs", "Eule" ) # Männliche deutsche Stimme für den F1-Rottenführer (Ohne Pitch-Anpassung) $VoiceDE = "Microsoft Stefan" Write-Host "Generiere 35 F1-ROTTENFÜHRER-Funkdateien mit STEFAN (100% CTD-SICHER)..." Write-Host "Erstellt parallel: A1LDCS001.wav bis A1LDCS035.wav UND F1LEAD001.wav bis F1LEAD035.wav" Write-Host "------------------------------------------------------------------" Add-Type -AssemblyName System.Speech $Synth = New-Object System.Speech.Synthesis.SpeechSynthesizer $Format = New-Object System.Speech.AudioFormat.SpeechAudioFormatInfo(11025, 16, 1) # Pfade für die temporären Schnipsel $TmpPart1 = Join-Path $PSScriptRoot "tmp_f1leader.wav" $TmpPart2 = Join-Path $PSScriptRoot "tmp_f1callsign.wav" function Merge-AndBoostWav { param([string]$Part1, [string]$Part2, [string]$OutPath) $bytes1 = [System.IO.File]::ReadAllBytes($Part1) $bytes2 = [System.IO.File]::ReadAllBytes($Part2) $rand = New-Object System.Random # Dynamischer Audio-Finder (Absoluter Schutz vor Headervarianzen) function Get-AudioPos([byte[]]$b) { for ($i = 12; $i -lt $b.Length - 8; $i++) { if ($b[$i] -eq 100 -and $b[$i+1] -eq 97 -and $b[$i+2] -eq 116 -and $b[$i+3] -eq 97) { return $i + 8 } } return -1 } $start1 = Get-AudioPos $bytes1 $start2 = Get-AudioPos $bytes2 # Sicherheits-Abbruch bei beschädigten TTS-Rohdaten if ($start1 -eq -1 -or $start2 -eq -1) { return } # --- SCHRITTSICHERES PAUSEN-KAPPEN (Noise Gate) --- # 1. Teil ("Rottenführer") hinten kappen $cutoff1 = $bytes1.Length $foundVoice1 = $false for ($i = $bytes1.Length - 2; $i -ge $start1; $i -= 2) { $sample = [math]::Abs([BitConverter]::ToInt16($bytes1, $i)) if ($sample -gt 200) { $cutoff1 = $i + 800 # Natürlicher Ausklang-Puffer if ($cutoff1 -gt $bytes1.Length) { $cutoff1 = $bytes1.Length } $foundVoice1 = $true break } } if (-not $foundVoice1) { $cutoff1 = $bytes1.Length } # 2. Teil (Rufzeichen) vorne kappen $cutoff2Start = $start2 $foundVoice2 = $false for ($i = $start2; $i -lt $bytes2.Length - 1; $i += 2) { $sample = [math]::Abs([BitConverter]::ToInt16($bytes2, $i)) if ($sample -gt 200) { $cutoff2Start = $i - 400 # Sicherheitsabstand vor Sprache if ($cutoff2Start -lt $start2) { $cutoff2Start = $start2 } if (($cutoff2Start - $start2) % 2 -ne 0) { $cutoff2Start-- } # 16-Bit Raster-Garantie $foundVoice2 = $true break } } if (-not $foundVoice2) { $cutoff2Start = $start2 } # Längenberechnungen auf das Byte genau $dataLen1 = $cutoff1 - $start1 $dataLen2 = $bytes2.Length - $cutoff2Start $totalDataLen = $dataLen1 + $dataLen2 $totalFileLen = $start1 + $totalDataLen - 8 # Neues Array initialisieren & Header klonen $outBytes = New-Object byte[] ($start1 + $totalDataLen) [Array]::Copy($bytes1, 0, $outBytes, 0, $start1) # Header-Längen mathematisch absolut exakt überschreiben $riffSize = [BitConverter]::GetBytes([int32]$totalFileLen) $outBytes[4] = $riffSize[0]; $outBytes[5] = $riffSize[1]; $outBytes[6] = $riffSize[2]; $outBytes[7] = $riffSize[3] $dataSize = [BitConverter]::GetBytes([int32]$totalDataLen) $outBytes[$start1 - 4] = $dataSize[0]; $outBytes[$start1 - 3] = $dataSize[1]; $outBytes[$start1 - 2] = $dataSize[2]; $outBytes[$start1 - 1] = $dataSize[3] # Daten nahtlos und phasenkorrekt verbinden [Array]::Copy($bytes1, $start1, $outBytes, $start1, $dataLen1) [Array]::Copy($bytes2, $cutoff2Start, $outBytes, ($start1 + $dataLen1), $dataLen2) # Audio-Filter & Funk-Effekt (3.5x Boost + leichtes Rauschen) for ($i = $start1; $i -lt $outBytes.Length - 1; $i += 2) { $sample = [BitConverter]::ToInt16($outBytes, $i) $noise = $rand.Next(-80, 80) $boosted = [int]($sample * 3.5) + $noise if ($boosted -gt 32767) { $boosted = 32767 } if ($boosted -lt -32768) { $boosted = -32768 } $newBytes = [BitConverter]::GetBytes([int16]$boosted) $outBytes[$i] = $newBytes[0] $outBytes[$i+1] = $newBytes[1] } [System.IO.File]::WriteAllBytes($OutPath, $outBytes) } # Hauptschleife über alle 35 Rufzeichen for ($i = 0; $i -lt $Callsigns.Count; $i++) { $Callsign = $Callsigns[$i] $NumString = ($i + 1).ToString("000") # Pfade für beide Dateitypen definieren $A1Filename = "A1LDCS$NumString.wav" $F1Filename = "F1LEAD$NumString.wav" $A1Path = Join-Path $PSScriptRoot $A1Filename $F1Path = Join-Path $PSScriptRoot $F1Filename Write-Host "Erstelle ($($i+1)/35): Stefan -> 'Rottenführer' + '$Callsign' -> $A1Filename & $F1Filename..." try { $Synth.SelectVoice($VoiceDE) } catch {} # Teil 1: Das Wort "Rottenführer" generieren $Synth.SetOutputToWaveFile($TmpPart1, $Format) $Synth.Speak("Rottenführer") $Synth.SetOutputToNull() # Teil 2: Das jeweilige Rufzeichen generieren $Synth.SetOutputToWaveFile($TmpPart2, $Format) $Synth.Speak($Callsign) $Synth.SetOutputToNull() # Erst die primäre A1LDCS-Datei schreiben (Stream wird danach sofort geschlossen!) Merge-AndBoostWav -Part1 $TmpPart1 -Part2 $TmpPart2 -OutPath $A1Path # Dann die fertige, fehlerfreie Datei direkt als F1LEAD duplizieren Copy-Item -Path $A1Path -Destination $F1Path -Force } # Endgültiges Aufräumen der Temporärdateien if (Test-Path $TmpPart1) { Remove-Item $TmpPart1 } if (Test-Path $TmpPart2) { Remove-Item $TmpPart2 } Write-Host "------------------------------------------------------------------" Write-Host "Erfolg! Alle 70 Dateien (35x A1LDCS + 35x F1LEAD) wurden absolut CTD-sicher erzeugt." Read-Host "Druecken Sie Enter zum Beenden..." In the end, I have now all my custom callsigns in voice and subtitles and it was fun doing it. I hope, this information might be useful to some, but do it at your own risk You can play around with it if you are familiar with scripting and modify the voices further, e.g. by changing the pitch or other parameters. It's up to you. Thanks and kind regards. 3
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now