1

Тема: VBS/WSH: Синтез звуков и SAPI

Из любопытства решил поэкспериментироавть с синтезом звуков и проигрыванием с помощью SAPI.SpVoice. Приведенный ниже код генерирует гармонические сигналы заданной частоты и длительности, миксует их, накладывает рандомное эхо, нормализует, обрезает тишину, конвертирует в поток и воспроизводит звук:

Option Explicit

' Стандартные форматы PCM
Const SAFT8kHz8BitMono = 4
Const SAFT8kHz8BitStereo = 5
Const SAFT8kHz16BitMono = 6
Const SAFT8kHz16BitStereo = 7
Const SAFT11kHz8BitMono = 8
Const SAFT11kHz8BitStereo = 9
Const SAFT11kHz16BitMono = 10
Const SAFT11kHz16BitStereo = 11
Const SAFT12kHz8BitMono = 12
Const SAFT12kHz8BitStereo = 13
Const SAFT12kHz16BitMono = 14
Const SAFT12kHz16BitStereo = 15
Const SAFT16kHz8BitMono = 16
Const SAFT16kHz8BitStereo = 17
Const SAFT16kHz16BitMono = 18
Const SAFT16kHz16BitStereo = 19
Const SAFT22kHz8BitMono = 20
Const SAFT22kHz8BitStereo = 21
Const SAFT22kHz16BitMono = 22
Const SAFT22kHz16BitStereo = 23
Const SAFT24kHz8BitMono = 24
Const SAFT24kHz8BitStereo = 25
Const SAFT24kHz16BitMono = 26
Const SAFT24kHz16BitStereo = 27
Const SAFT32kHz8BitMono = 28
Const SAFT32kHz8BitStereo = 29
Const SAFT32kHz16BitMono = 30
Const SAFT32kHz16BitStereo = 31
Const SAFT44kHz8BitMono = 32
Const SAFT44kHz8BitStereo = 33
Const SAFT44kHz16BitMono = 34
Const SAFT44kHz16BitStereo = 35
Const SAFT48kHz8BitMono = 36
Const SAFT48kHz8BitStereo = 37
Const SAFT48kHz16BitMono = 38
Const SAFT48kHz16BitStereo = 39
Dim objNotes, lngSpAudioFormat, dblSndDur, dblSndDelay, dblSndFadeCut, dblSndVolume, lngSamplingFrequency, arrSounds, arrSamples1, objStream1, arrSamples2, objStream2

' вычисленние частот каждой ноты
Set objNotes = GetPitches()

' установка параметров звука
lngSpAudioFormat = SAFT16kHz16BitMono ' формат
dblSndDur = 3 ' длительность звука в секундах
dblSndDelay = 0.45 ' задержка между звуками в секундах
dblSndFadeCut = 0.001 ' уровень отсечки
dblSndVolume = 0.5 ' громкость
lngSpAudioFormat = lngSpAudioFormat And 254 ' принудительно моно
lngSamplingFrequency = Array(8000, 11000, 12000, 16000, 22000, 24000, 32000, 44000, 48000)((lngSpAudioFormat - 4) \ 4) ' определение частоты дискретизации в Гц

arrSounds = Array("Соль-диез 1 октава", "До 2 октава", "Ре-диез 2 октава", "Соль-диез 2 октава") ' частоты звуков в Гц / названия нот
RenderSound lngSamplingFrequency, arrSounds, dblSndDur, dblSndDelay, dblSndFadeCut, arrSamples1
arrSounds = Array("Соль-диез 2 октава", "Ре-диез 2 октава", "До 2 октава", "Соль-диез 1 октава")
RenderSound lngSamplingFrequency, arrSounds, dblSndDur, dblSndDelay, dblSndFadeCut, arrSamples2
Set objStream1 = CreateSpMemStream(lngSpAudioFormat, arrSamples1, dblSndVolume)
Set objStream2 = CreateSpMemStream(lngSpAudioFormat, arrSamples2, dblSndVolume)

PlaySound objStream1
CreateObject("WScript.Shell").PopUp "Press OK to continue", 0, , 64
PlaySound objStream2

Sub PlaySound(objSpMemStream)
    objSpMemStream.Seek 0
    With CreateObject("SAPI.SpVoice")
        .SpeakStream objSpMemStream, 1
        .WaitUntilDone -1
    End With
End Sub

Sub RenderSound(lngSamplingFrequency, arrSounds, dblSndDur, dblSndDelay, dblSndFadeCut, arrSamples)
    Dim n, dblSndFreq, lngDelaySamples, arrTone
    ' генерация и смешивание звуков
    lngDelaySamples = CLng(dblSndDelay * lngSamplingFrequency)
    arrSamples = Array()
    For n = 0 To UBound(arrSounds)
        If IsNumeric(arrSounds(n)) Then dblSndFreq = arrSounds(n) Else dblSndFreq = objNotes(arrSounds(n))
        GenerateTone dblSndFreq, dblSndDur, dblSndFadeCut, lngSamplingFrequency, arrTone
        Mix arrSamples, arrTone, 1, n * lngDelaySamples
    Next
    ' наложение эхо
    Echo arrSamples, lngSamplingFrequency, 12
    ' нормализация
    Normalize arrSamples
    ' обрезка тишины в конце
    TrimSilence arrSamples, dblSndFadeCut
End Sub

Sub GenerateTone(dblSndFreq, dblSndDur, dblSndFadeCut, lngSamplingFrequency, arrSamples)
    Dim i, dblPi, dblRadsPerSample, lngTotalSamples, lngFadeInSamples, lngFadeOutSamples, dblFadeIn, dblFadeOut, dblAmplitude
    ' определение параметров звука
    dblPi = 4 * Atn(1)
    dblRadsPerSample = 2 * dblPi * dblSndFreq / lngSamplingFrequency ' radians per sample
    lngTotalSamples = CLng(dblSndDur * lngSamplingFrequency)
    lngFadeInSamples = CLng(lngTotalSamples * 0.01)
    lngFadeOutSamples = lngTotalSamples - lngFadeInSamples
    dblFadeIn = 1 / Exp(Log(dblSndFadeCut) / lngFadeInSamples)
    dblFadeOut = Exp(Log(dblSndFadeCut) / lngFadeOutSamples)
    arrSamples = Array()
    ReDim arrSamples(lngTotalSamples - 1)
    ' генерация нарастания
    dblAmplitude = dblSndFadeCut
    For i = 0 To lngFadeInSamples - 1
        arrSamples(i) = dblAmplitude * (Sin(i * dblRadsPerSample) + 0.5 * Sin(2 * i * dblRadsPerSample) + 0.15 * Sin(4 * i * dblRadsPerSample))
        ' arrSamples(i) = dblAmplitude * Sin(i * dblRadsPerSample)
        dblAmplitude = dblAmplitude * dblFadeIn
    Next
    ' генерация спада
    dblAmplitude = 1
    For i = lngFadeInSamples To lngTotalSamples - 1
        arrSamples(i) = dblAmplitude * (Sin(i * dblRadsPerSample) + 0.5 * Sin(2 * i * dblRadsPerSample) + 0.15 * Sin(4 * i * dblRadsPerSample))
        ' arrSamples(i) = dblAmplitude * Sin(i * dblRadsPerSample)
        dblAmplitude = dblAmplitude * dblFadeOut
    Next
End Sub

Sub Mix(arrSamples, arrMixing, dblGain, lngDelaySamples)
    Dim i, lngAddSamples
    lngAddSamples = lngDelaySamples + UBound(arrMixing)
    If lngAddSamples > UBound(arrSamples) Then ReDim Preserve arrSamples(lngAddSamples)
    For i = 0 To UBound(arrMixing)
        arrSamples(i + lngDelaySamples) = arrSamples(i + lngDelaySamples) + dblGain * arrMixing(i)
    Next
End Sub

Sub Echo(arrSamples, lngSamplingFrequency, lngDepth)
    Dim n, dblDelay, lngOffset, dblAttenuation, arrReplica
    For n = 1 To lngDepth
        Randomize
        dblDelay = 0.2 + Rnd * 0.7 ' задержка эхо в секундах
        lngOffset = CLng(lngSamplingFrequency * dblDelay)
        dblAttenuation = 10 ^ -(0.75 + Rnd * 0.6) ' уровень ослабления
        arrReplica = arrSamples
        Mix arrSamples, arrReplica, dblAttenuation, lngOffset
    Next
End Sub

Sub Normalize(arrSamples)
    Dim dblPeak, dblRatio, i
    dblPeak = 0
    For i = 0 To UBound(arrSamples)
        If Abs(arrSamples(i)) > dblPeak Then dblPeak = Abs(arrSamples(i))
    Next
    dblRatio = 1 / dblPeak
    For i = 0 To UBound(arrSamples)
        arrSamples(i) = arrSamples(i) * dblRatio
    Next
End Sub

Sub TrimSilence(arrSamples, dblSndFadeCut)
    Dim i
    For i = UBound(arrSamples) To 0 Step -1
        If Abs(arrSamples(i)) > dblSndFadeCut Then Exit For
    Next
    ReDim Preserve arrSamples(i)
End Sub

Function CreateSpMemStream(lngSpAudioFormat, arrSamples, dblSndVolume)
    Dim bool16Bit, lngMax, dblAmplitude, lngValue, lngMSB, lngLSB, i
    ' создание потока
    Set CreateSpMemStream = CreateObject("SAPI.SpMemoryStream")
    CreateSpMemStream.Format.Type = lngSpAudioFormat
    bool16Bit = (lngSpAudioFormat \ 2) Mod 2 ' определение разрядности 8 / 16 бит
    ' преобразование величин и заполнение потока
    If bool16Bit Then ' 16 бит
        lngMax = 2 ^ 15 - 1 ' 32767
        dblAmplitude = dblSndVolume * lngMax
        For i = 0 To UBound(arrSamples)
            lngValue = Fix(arrSamples(i) * dblAmplitude)
            If lngValue < 0 Then lngValue = 65536 + lngValue
            lngMSB = lngValue \ 256
            lngLSB = lngValue Mod 256
            CreateSpMemStream.Write CByte(lngLSB)
            CreateSpMemStream.Write CByte(lngMSB)
        Next
    Else ' 8 бит
        lngMax = 2 ^ 7 - 1
        dblAmplitude = dblSndVolume * lngMax
        For i = 0 To UBound(arrSamples)
            CreateSpMemStream.Write CByte(arrSamples(i) * dblAmplitude + lngMax)
        Next
    End If
End Function

Function GetPitches()
    Dim objDictionary, n, m, f
    Set objDictionary = CreateObject("Scripting.Dictionary")
    For m = 0 To 8 ' октавы
        For n = 0 To 11 ' ноты
            f = 27.5 * 2 ^ (m + (n - 9) / 12)
            objDictionary(Array("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")(n) & m) = f ' scientific pitch notation
            objDictionary(Array("До", "До-диез", "Ре", "Ре-диез", "Ми", "Фа", "Фа-диез", "Соль", "Соль-диез", "Ля", "Ля-диез", "Си")(n) & " " & Array("Субконтpоктава", "Контpоктава", "Большая октава", "Малая октава", "1 октава", "2 октава", "3 октава", "4 октава", "5 октава")(m)) = f
        Next
    Next
    Set GetPitches = objDictionary
End Function

Форматы не ниже SAFT16kHz16BitMono и с частотой дискретизации, кратной 8 кГц, звучат вполне прилично, по крайней мере у меня. Но вот длительность подготовки оставила желать лучшего: вся подготовка заняла 9,5 секунд, из них заливка в поток - 1,7, реверберация - 5 (в общем-то, можно и без эхо, поскольку улучшения незначительны, да и алгоритм примитивный ). Существует немало способов оптимизации быстродействия, но это уже следующий шаг.

Щт Уккщк Куыгьу Туче
’ҐЄгй п Є®¤®ў п бва Ёж : 1251

2

Re: VBS/WSH: Синтез звуков и SAPI

Довольно классно. Достойно коллекции