nvdaportable/NVDA/_synthDrivers32/_sonic.py

229 lines
8.2 KiB
Python

# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2025 NV Access Limited, gexgd0419
from ctypes import CDLL, POINTER, Array, c_float, c_int, c_short, c_ubyte, c_void_p, cdll
import os
from typing import TYPE_CHECKING
import globalVars
from logHandler import log
if TYPE_CHECKING:
from ctypes import _Pointer
c_float_p = _Pointer[c_float]
c_short_p = _Pointer[c_short]
c_ubyte_p = _Pointer[c_ubyte]
else:
c_float_p = POINTER(c_float)
c_short_p = POINTER(c_short)
c_ubyte_p = POINTER(c_ubyte)
SONIC_DLL_PATH = os.path.join(globalVars.appDir, "synthDrivers", "sonic.dll")
sonicLib: CDLL | None = None
class SonicStreamP(c_void_p):
pass
def initialize():
"""Initialize the Sonic DLL.
The sonic.dll file should be in the synthDrivers directory.
This can be called more than once."""
global sonicLib
if sonicLib:
return
log.debug("Initializing Sonic library")
sonicLib = cdll.LoadLibrary(SONIC_DLL_PATH)
sonicLib.sonicCreateStream.restype = SonicStreamP
sonicLib.sonicCreateStream.argtypes = [c_int, c_int]
sonicLib.sonicDestroyStream.restype = None
sonicLib.sonicDestroyStream.argtypes = [SonicStreamP]
sonicLib.sonicWriteFloatToStream.restype = c_int
sonicLib.sonicWriteFloatToStream.argtypes = [SonicStreamP, c_void_p, c_int]
sonicLib.sonicWriteShortToStream.restype = c_int
sonicLib.sonicWriteShortToStream.argtypes = [SonicStreamP, c_void_p, c_int]
sonicLib.sonicWriteUnsignedCharToStream.restype = c_int
sonicLib.sonicWriteUnsignedCharToStream.argtypes = [SonicStreamP, c_void_p, c_int]
sonicLib.sonicReadFloatFromStream.restype = c_int
sonicLib.sonicReadFloatFromStream.argtypes = [SonicStreamP, c_void_p, c_int]
sonicLib.sonicReadShortFromStream.restype = c_int
sonicLib.sonicReadShortFromStream.argtypes = [SonicStreamP, c_void_p, c_int]
sonicLib.sonicReadUnsignedCharFromStream.restype = c_int
sonicLib.sonicReadUnsignedCharFromStream.argtypes = [SonicStreamP, c_void_p, c_int]
sonicLib.sonicFlushStream.restype = c_int
sonicLib.sonicFlushStream.argtypes = [SonicStreamP]
sonicLib.sonicSamplesAvailable.restype = c_int
sonicLib.sonicSamplesAvailable.argtypes = [SonicStreamP]
sonicLib.sonicGetSpeed.restype = c_float
sonicLib.sonicGetSpeed.argtypes = [SonicStreamP]
sonicLib.sonicSetSpeed.restype = None
sonicLib.sonicSetSpeed.argtypes = [SonicStreamP, c_float]
sonicLib.sonicGetPitch.restype = c_float
sonicLib.sonicGetPitch.argtypes = [SonicStreamP]
sonicLib.sonicSetPitch.restype = None
sonicLib.sonicSetPitch.argtypes = [SonicStreamP, c_float]
sonicLib.sonicGetRate.restype = c_float
sonicLib.sonicGetRate.argtypes = [SonicStreamP]
sonicLib.sonicSetRate.restype = None
sonicLib.sonicSetRate.argtypes = [SonicStreamP, c_float]
sonicLib.sonicGetVolume.restype = c_float
sonicLib.sonicGetVolume.argtypes = [SonicStreamP]
sonicLib.sonicSetVolume.restype = None
sonicLib.sonicSetVolume.argtypes = [SonicStreamP, c_float]
sonicLib.sonicGetQuality.restype = c_int
sonicLib.sonicGetQuality.argtypes = [SonicStreamP]
sonicLib.sonicSetQuality.restype = None
sonicLib.sonicSetQuality.argtypes = [SonicStreamP, c_int]
sonicLib.sonicGetSampleRate.restype = c_int
sonicLib.sonicGetSampleRate.argtypes = [SonicStreamP]
sonicLib.sonicSetSampleRate.restype = None
sonicLib.sonicSetSampleRate.argtypes = [SonicStreamP, c_int]
sonicLib.sonicGetNumChannels.restype = c_int
sonicLib.sonicGetNumChannels.argtypes = [SonicStreamP]
sonicLib.sonicSetNumChannels.restype = None
sonicLib.sonicSetNumChannels.argtypes = [SonicStreamP, c_int]
class SonicStream:
"""
Audio stream that wraps the Sonic library to process audio,
which is optimised for speeding up speech by high factors.
Audio data are stored internally as 16-bit integers.
"""
def __init__(self, sampleRate: int, channels: int):
self.stream: SonicStreamP = sonicLib.sonicCreateStream(sampleRate, channels)
if not self.stream:
raise MemoryError()
def __del__(self):
sonicLib.sonicDestroyStream(self.stream)
def writeFloat(self, data: c_float_p, numSamples: int) -> None:
"""Write 32-bit floating point data to be processed into the stream,
where each sample must be between -1 and 1.
:param data: A pointer to 32-bit floating point wave data.
:param numSamples: The number of samples.
Multiply this by channel count to get the total number of values.
:raises MemoryError: If memory allocation failed."""
if not sonicLib.sonicWriteFloatToStream(self.stream, data, numSamples):
raise MemoryError()
def writeShort(self, data: c_short_p, numSamples: int) -> None:
"""Write 16-bit integer data to be processed into the stream.
:param data: A pointer to 16-bit integer wave data.
:param numSamples: The number of samples.
Multiply this by channel count to get the total number of values.
:raises MemoryError: If memory allocation failed."""
if not sonicLib.sonicWriteShortToStream(self.stream, data, numSamples):
raise MemoryError()
def writeUnsignedChar(self, data: c_ubyte_p, numSamples: int) -> None:
"""Write 8-bit unsigned integer data to be processed into the stream.
:param data: A pointer to 8-bit integer wave data.
:param numSamples: The number of samples.
Multiply this by channel count to get the total number of values.
:raises MemoryError: If memory allocation failed."""
if not sonicLib.sonicWriteUnsignedCharToStream(self.stream, data, numSamples):
raise MemoryError()
def readFloat(self) -> Array[c_float]:
"""Read processed data from the stream as 32-bit floating point data."""
samples = self.samplesAvailable
arrayLength = samples * self.channels
buffer = (c_float * arrayLength)()
sonicLib.sonicReadFloatFromStream(self.stream, buffer, samples)
return buffer
def readShort(self) -> Array[c_short]:
"""Read processed data from the stream as 16-bit integer data."""
samples = self.samplesAvailable
arrayLength = samples * self.channels
buffer = (c_short * arrayLength)()
sonicLib.sonicReadShortFromStream(self.stream, buffer, samples)
return buffer
def readUnsignedChar(self) -> Array[c_ubyte]:
"""Read processed data from the stream as 8-bit unsigned integer data."""
samples = self.samplesAvailable
arrayLength = samples * self.channels
buffer = (c_ubyte * arrayLength)()
sonicLib.sonicReadUnsignedCharFromStream(self.stream, buffer, samples)
return buffer
def flush(self) -> None:
"""Force the sonic stream to generate output using whatever data it currently has.
No extra delay will be added to the output, but flushing in the middle of words could introduce distortion.
This is usually done when data writing is completed.
:raises MemoryError: If memory allocation failed."""
if not sonicLib.sonicFlushStream(self.stream):
raise MemoryError()
@property
def samplesAvailable(self) -> int:
return sonicLib.sonicSamplesAvailable(self.stream)
@property
def speed(self) -> float:
return sonicLib.sonicGetSpeed(self.stream)
@speed.setter
def speed(self, value: float):
sonicLib.sonicSetSpeed(self.stream, value)
@property
def pitch(self) -> float:
return sonicLib.sonicGetPitch(self.stream)
@pitch.setter
def pitch(self, value: float):
sonicLib.sonicSetPitch(self.stream, value)
@property
def rate(self) -> float:
"""This scales pitch and speed at the same time."""
return sonicLib.sonicGetRate(self.stream)
@rate.setter
def rate(self, value: float):
sonicLib.sonicSetRate(self.stream, value)
@property
def volume(self) -> float:
"""The scaling factor of the stream."""
return sonicLib.sonicGetVolume(self.stream)
@volume.setter
def volume(self, value: float):
sonicLib.sonicSetVolume(self.stream, value)
@property
def quality(self) -> int:
"""Default 0 is virtually as good as 1, but very much faster."""
return sonicLib.sonicGetQuality(self.stream)
@quality.setter
def quality(self, value: int):
sonicLib.sonicSetQuality(self.stream, value)
@property
def sampleRate(self) -> int:
return sonicLib.sonicGetSampleRate(self.stream)
@sampleRate.setter
def sampleRate(self, value: int):
sonicLib.sonicSetSampleRate(self.stream, value)
@property
def channels(self) -> int:
return sonicLib.sonicGetNumChannels(self.stream)
@channels.setter
def channels(self, value: int):
sonicLib.sonicSetNumChannels(self.stream, value)