# 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)