Added audio backends for Windows and macOS

This commit is contained in:
Mark Qvist 2025-03-11 22:27:37 +01:00
commit cf476cacfb
35 changed files with 182 additions and 5 deletions

View file

@ -0,0 +1,50 @@
The shared libraries for macOS were obtained by first
installing/compiling the standard libraries using Homebrew:
brew install libogg opus opusfile libopusenc libvorbis flac
These libraries were then copied into this directory. However a
number of the libraries were looking for eachother internally.
The paths the libraries are currently expecting can be seen by
executing the command:
otool -L *.dylib
This shows, for example:
libopusfile.0.dylib:
/usr/local/opt/opusfile/lib/libopusfile.0.dylib (compatibility version 5.0.0, current version 5.5.0)
/usr/local/opt/libogg/lib/libogg.0.dylib (compatibility version 9.0.0, current version 9.4.0)
/usr/local/opt/opus/lib/libopus.0.dylib (compatibility version 9.0.0, current version 9.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)
Note the hard-coded location to libogg and libopus. These files are
included with the PyOgg distribution, but not at those locations.
To fix the expected paths, we ran the following commands:
install_name_tool -change /usr/local/opt/libogg/lib/libogg.0.dylib @loader_path/libogg.0.dylib libFLAC.8.dylib
install_name_tool -change /usr/local/opt/opus/lib/libopus.0.dylib @loader_path/libopus.0.dylib libopusenc.0.dylib
install_name_tool -change /usr/local/opt/libogg/lib/libogg.0.dylib @loader_path/libogg.0.dylib libopusfile.0.dylib
install_name_tool -change /usr/local/opt/opus/lib/libopus.0.dylib @loader_path/libopus.0.dylib libopusfile.0.dylib
install_name_tool -change /usr/local/opt/libogg/lib/libogg.0.dylib @loader_path/libogg.0.dylib libvorbis.0.dylib
install_name_tool -change /usr/local/Cellar/libvorbis/1.3.7/lib/libvorbis.0.dylib @loader_path/libvorbis.0.dylib libvorbisenc.2.dylib
install_name_tool -change /usr/local/opt/libogg/lib/libogg.0.dylib @loader_path/libogg.0.dylib libvorbisenc.2.dylib
install_name_tool -change /usr/local/Cellar/libvorbis/1.3.7/lib/libvorbis.0.dylib @loader_path/libvorbis.0.dylib libvorbisfile.3.dylib
install_name_tool -change /usr/local/opt/libogg/lib/libogg.0.dylib @loader_path/libogg.0.dylib libvorbisfile.3.dylib
After changing the locations of the paths, we can rerun:
otool -L *.dylib
Which shows, in the case of libopusfile:
libopusfile.0.dylib:
/usr/local/opt/opusfile/lib/libopusfile.0.dylib (compatibility version 5.0.0, current version 5.5.0)
@loader_path/libogg.0.dylib (compatibility version 9.0.0, current version 9.4.0)
@loader_path/libopus.0.dylib (compatibility version 9.0.0, current version 9.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)
These relative paths mean the libraries will be found within the PyOgg
package.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -23,9 +23,61 @@ class LinuxBackend():
def get_player(self, samples_per_frame=None):
return self.device.player(samplerate=self.samplerate, blocksize=samples_per_frame)
def release_player(self): pass
class DarwinBackend():
SAMPLERATE = 48000
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
import soundcard
self.samplerate = samplerate
self.soundcard = soundcard
if preferred_device:
try: self.device = self.soundcard.get_speaker(preferred_device)
except: self.device = soundcard.default_speaker()
else: self.device = soundcard.default_speaker()
RNS.log(f"Using output device {self.device}", RNS.LOG_DEBUG)
def flush(self):
self.recorder.flush()
def get_player(self, samples_per_frame=None):
return self.device.player(samplerate=self.samplerate, blocksize=samples_per_frame)
def release_player(self): pass
class WindowsBackend():
SAMPLERATE = 48000
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
import soundcard
from pythoncom import CoInitializeEx, CoUninitialize
self.com_init = CoInitializeEx
self.com_release = CoUninitialize
self.samplerate = samplerate
self.soundcard = soundcard
if preferred_device:
try: self.device = self.soundcard.get_speaker(preferred_device)
except: self.device = soundcard.default_speaker()
else: self.device = soundcard.default_speaker()
RNS.log(f"Using output device {self.device}", RNS.LOG_DEBUG)
def flush(self):
self.recorder.flush()
def get_player(self, samples_per_frame=None):
self.com_init(0)
return self.device.player(samplerate=self.samplerate, blocksize=samples_per_frame)
def release_player(self): self.com_release()
def get_backend():
if RNS.vendor.platformutils.is_linux():
return LinuxBackend
elif RNS.vendor.platformutils.is_windows():
return WindowsBackend
elif RNS.vendor.platformutils.is_darwin():
return DarwinBackend
else:
return None
@ -132,5 +184,7 @@ class LineSink(LocalSink):
else:
time.sleep(self.frame_time*0.1)
self.backend.release_player()
class PacketSink(RemoteSink):
pass

BIN
LXST/Sounds/ringer.opus Normal file

Binary file not shown.

BIN
LXST/Sounds/soft.opus Normal file

Binary file not shown.

View file

@ -32,9 +32,65 @@ class LinuxBackend():
def get_recorder(self, samples_per_frame):
return self.device.recorder(samplerate=self.SAMPLERATE, blocksize=samples_per_frame)
def release_recorder(self): pass
class DarwinBackend():
SAMPLERATE = 48000
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
import soundcard
self.samplerate = samplerate
self.soundcard = soundcard
if preferred_device:
try: self.device = self.soundcard.get_microphone(preferred_device)
except: self.device = self.soundcard.default_microphone()
else: self.device = self.soundcard.default_microphone()
self.channels = self.device.channels
self.bitdepth = 32
RNS.log(f"Using input device {self.device}", RNS.LOG_DEBUG)
def flush(self):
self.recorder.flush()
def get_recorder(self, samples_per_frame):
return self.device.recorder(samplerate=self.SAMPLERATE, blocksize=samples_per_frame)
def release_recorder(self): pass
class WindowsBackend():
SAMPLERATE = 48000
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
import soundcard
from pythoncom import CoInitializeEx, CoUninitialize
self.com_init = CoInitializeEx
self.com_release = CoUninitialize
self.samplerate = samplerate
self.soundcard = soundcard
if preferred_device:
try: self.device = self.soundcard.get_microphone(preferred_device)
except: self.device = self.soundcard.default_microphone()
else: self.device = self.soundcard.default_microphone()
self.channels = self.device.channels
self.bitdepth = 32
RNS.log(f"Using input device {self.device}", RNS.LOG_DEBUG)
def flush(self):
self.recorder.flush()
def get_recorder(self, samples_per_frame):
self.com_init(0)
return self.device.recorder(samplerate=self.SAMPLERATE, blocksize=samples_per_frame)
def release_recorder(self): self.com_release()
def get_backend():
if RNS.vendor.platformutils.is_linux():
return LinuxBackend
elif RNS.vendor.platformutils.is_windows():
return WindowsBackend
elif RNS.vendor.platformutils.is_darwin():
return DarwinBackend
else:
return None

View file

@ -98,6 +98,16 @@ class ReticulumTelephone():
if not os.path.isdir(self.storagedir):
os.makedirs(self.storagedir)
ringer_tones = ["ringer.opus", "soft.opus"]
sounds_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "Sounds"))
if os.path.isdir(sounds_path):
import shutil
for filename in ringer_tones:
src_path = os.path.join(sounds_path, filename)
dst_path = os.path.join(self.configdir, filename)
if os.path.isfile(src_path):
RNS.log(f"Copying {src_path} to {dst_path}")
shutil.copy(src_path, dst_path)
if not os.path.isfile(self.configpath):
self.create_default_config()
@ -691,7 +701,7 @@ __default_rnphone_config__ = """# This is an example rnphone config file.
# phone is ringing. Must be in OPUS format, and
# located in the rnphone config directory.
ringtone = ringtone.opus
ringtone = ringer.opus
# You can define the preferred audio devices
# to use as the speaker output, ringer output

View file

@ -1 +1 @@
__version__ = "0.2.4"
__version__ = "0.2.5"

View file

@ -36,7 +36,7 @@ LXST uses encryption provided by [Reticulum](https://reticulum.network), and thu
## Project Status & License
This software is in a very early alpha state, and will change rapidly with ongoing development. Consider no APIs stable. Consider everything explosive.
This software is in a very early alpha state, and will change rapidly with ongoing development. Consider no APIs stable. Consider everything explosive. Not all features are implemented. Nothing is documented. For a fully functional LXST program, take a look at the included `rnphone` program, which provides telephony service over Reticulum. Everything else will currently be a voyage of your own making.
While under early development, the project is kept under a `CC BY-NC-ND 4.0` license.

View file

@ -10,8 +10,14 @@ packages.append("LXST.Utilities")
packages.append("LXST.Primitives.hardware")
packages.append("LXST.Codecs.libs.pydub")
packages.append("LXST.Codecs.libs.pyogg")
print("Packages:")
print(packages)
package_data = {
"": [
"Codecs/libs/pyogg/libs/win_amd64/*",
"Codecs/libs/pyogg/libs/macos/*",
"Sounds/*",
]
}
setuptools.setup(
name="lxst",
@ -23,6 +29,7 @@ setuptools.setup(
long_description_content_type="text/markdown",
url="https://git.unsigned.io/markqvist/lxst",
packages=packages,
package_data=package_data,
classifiers=[
"Programming Language :: Python :: 3",
"License :: Other/Proprietary License",