mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 22:21:18 +00:00
92 lines
4.5 KiB
PowerShell
92 lines
4.5 KiB
PowerShell
# Generate the two fixed phrases with Windows' local speech synthesizer.
|
|
# Requires ffmpeg and Python; outputs WAV previews plus the checked-in header.
|
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
param(
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$OutputDirectory = 'out/gps-voice',
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$OutputHeader = 'src/helpers/ui/GpsVoiceData.h',
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Voice = 'Microsoft Zira Desktop',
|
|
[ValidateRange(-10, 10)]
|
|
[int]$Rate = -1,
|
|
[switch]$Compress = $true,
|
|
[ValidateRange(0.0625, 1.0)]
|
|
[double]$PeakLimit = 0.12
|
|
)
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# Require a deliberate header target for acoustic experiments. A normal run
|
|
# regenerates the selected Zira / rate -1 / compressed / gain-8 profile.
|
|
# Speech is prerecorded and shared by supported boards; no runtime TTS is needed.
|
|
# Original Zira: -Voice 'Microsoft Zira Desktop' -Compress:$false -PeakLimit 0.8
|
|
# with an explicit -OutputHeader.
|
|
if (-not $PSBoundParameters.ContainsKey('OutputHeader') -and
|
|
($Voice -ne 'Microsoft Zira Desktop' -or $Rate -ne -1 -or
|
|
-not $Compress -or $PeakLimit -ne 0.12)) {
|
|
throw 'Voice variants require an explicit -OutputHeader (use an out/gps-voice path for tests).'
|
|
}
|
|
$gpsDirectory = [IO.Path]::GetFullPath($OutputDirectory)
|
|
$gpsHeader = [IO.Path]::GetFullPath($OutputHeader)
|
|
if ([IO.Path]::GetExtension($gpsHeader) -ine '.h') {
|
|
throw '-OutputHeader must name a .h file.'
|
|
}
|
|
if (Test-Path -LiteralPath $gpsDirectory -PathType Leaf) {
|
|
throw '-OutputDirectory names an existing file.'
|
|
}
|
|
if (Test-Path -LiteralPath $gpsHeader -PathType Container) {
|
|
throw '-OutputHeader names an existing directory.'
|
|
}
|
|
$gpsEncoder = Join-Path $PSScriptRoot 'generate_gps_voice.py'
|
|
if (-not (Test-Path -LiteralPath $gpsEncoder -PathType Leaf)) {
|
|
throw "Voice encoder not found: $gpsEncoder"
|
|
}
|
|
|
|
$gpsFilter = 'highpass=f=350,lowpass=f=3400,silenceremove=start_periods=1:start_threshold=-45dB,areverse,silenceremove=start_periods=1:start_threshold=-45dB,areverse'
|
|
if ($Compress) {
|
|
# Raise quieter consonants without simply clipping the louder vowels.
|
|
$gpsFilter += ',acompressor=threshold=0.08:ratio=6:attack=5:release=70:makeup=4:knee=4:detection=rms'
|
|
}
|
|
$gpsFilter += ',volume=1.4'
|
|
if ($Compress -or $PeakLimit -ne 0.8) {
|
|
# Limit after resampling so the experimental PCM peak has real headroom.
|
|
$gpsFilter += ',aresample=8000'
|
|
}
|
|
$gpsPeak = $PeakLimit.ToString('0.####', [Globalization.CultureInfo]::InvariantCulture)
|
|
$gpsFilter += ",alimiter=limit=${gpsPeak}:level=disabled,apad=pad_dur=0.05"
|
|
# For player gains 4 / 6 / 8, begin at -PeakLimit 0.18 / 0.12 / 0.09
|
|
# (72% nominal full scale after gain). Check decoded/reconstructed peaks too:
|
|
# ADPCM quantization and interpolation can overshoot the source PCM ceiling.
|
|
Write-Verbose "Voice: $Voice; rate: $Rate; output directory: $gpsDirectory"
|
|
Write-Verbose "Output header: $gpsHeader"
|
|
Write-Verbose "FFmpeg filter: $gpsFilter"
|
|
if (-not $PSCmdlet.ShouldProcess($gpsHeader, "Synthesize GPS on/off with $Voice and encode previews from $gpsDirectory")) {
|
|
return
|
|
}
|
|
|
|
Add-Type -AssemblyName System.Speech
|
|
$gpsVoice = New-Object System.Speech.Synthesis.SpeechSynthesizer
|
|
try {
|
|
$gpsVoices = @($gpsVoice.GetInstalledVoices() | Where-Object Enabled | ForEach-Object { $_.VoiceInfo.Name })
|
|
if ($gpsVoices -notcontains $Voice) {
|
|
throw "Voice '$Voice' is unavailable. Installed voices: $($gpsVoices -join ', ')"
|
|
}
|
|
$gpsVoice.SelectVoice($Voice)
|
|
$gpsVoice.Rate = $Rate
|
|
Get-Command ffmpeg, python -ErrorAction Stop | Out-Null
|
|
New-Item -ItemType Directory -Force -Path $gpsDirectory | Out-Null
|
|
New-Item -ItemType Directory -Force -Path ([IO.Path]::GetDirectoryName($gpsHeader)) | Out-Null
|
|
foreach ($gpsState in @('on','off')) {
|
|
$gpsRaw = Join-Path $gpsDirectory "gps-$gpsState-raw.wav"
|
|
$gpsPcm = Join-Path $gpsDirectory "gps-$gpsState.wav"
|
|
$gpsVoice.SetOutputToWaveFile([IO.Path]::GetFullPath($gpsRaw))
|
|
$gpsVoice.Speak("G P S $gpsState")
|
|
$gpsVoice.SetOutputToNull()
|
|
# Keep enough consonant energy for a small beeper; limit clipping.
|
|
& ffmpeg -hide_banner -loglevel error -y -i $gpsRaw -af $gpsFilter -ar 8000 -ac 1 -c:a pcm_s16le $gpsPcm
|
|
if ($LASTEXITCODE -ne 0) { throw 'Voice conversion failed' }
|
|
}
|
|
} finally { $gpsVoice.Dispose() }
|
|
& python $gpsEncoder --on (Join-Path $gpsDirectory 'gps-on.wav') --off (Join-Path $gpsDirectory 'gps-off.wav') --output $gpsHeader --voice $Voice
|
|
if ($LASTEXITCODE -ne 0) { throw 'Voice encoding failed' }
|