Audio recording on Linux

This commit is contained in:
Mark Qvist 2024-06-04 14:29:28 +02:00
parent 1a224fd65a
commit 584267dcc5
4 changed files with 108 additions and 35 deletions

View File

@ -1534,8 +1534,12 @@ class SidebandApp(MDApp):
elif audio_field[0] >= LXMF.AM_CODEC2_700C and audio_field[0] <= LXMF.AM_CODEC2_3200: elif audio_field[0] >= LXMF.AM_CODEC2_700C and audio_field[0] <= LXMF.AM_CODEC2_3200:
temp_path = self.sideband.rec_cache+"/msg.ogg" temp_path = self.sideband.rec_cache+"/msg.ogg"
from sideband.audioproc import samples_to_ogg, decode_codec2 from sideband.audioproc import samples_to_ogg, decode_codec2
samples = decode_codec2(audio_field[1], audio_field[0])
if samples_to_ogg(samples, temp_path): target_rate = 8000
if RNS.vendor.platformutils.is_linux():
target_rate = 48000
if samples_to_ogg(decode_codec2(audio_field[1], audio_field[0]), temp_path, input_rate=8000, output_rate=target_rate):
RNS.log("Wrote OGG file to: "+temp_path, RNS.LOG_DEBUG) RNS.log("Wrote OGG file to: "+temp_path, RNS.LOG_DEBUG)
else: else:
RNS.log("OGG write failed", RNS.LOG_DEBUG) RNS.log("OGG write failed", RNS.LOG_DEBUG)
@ -1641,7 +1645,7 @@ class SidebandApp(MDApp):
audio = audio.set_sample_width(2) audio = audio.set_sample_width(2)
samples = audio.get_array_of_samples() samples = audio.get_array_of_samples()
from sideband.audioproc import samples_from_ogg, encode_codec2, decode_codec2 from sideband.audioproc import encode_codec2
encoded = encode_codec2(samples, self.audio_msg_mode) encoded = encode_codec2(samples, self.audio_msg_mode)
ap_duration = time.time() - ap_start ap_duration = time.time() - ap_start

View File

@ -56,7 +56,7 @@ class AndroidAudio(Audio):
else: else:
self._recorder.setAudioSource(AudioSource.DEFAULT) self._recorder.setAudioSource(AudioSource.DEFAULT)
self._recorder.setAudioSamplingRate(48000) self._recorder.setAudioSamplingRate(48000)
self._recorder.setAudioEncodingBitRate(16000) self._recorder.setAudioEncodingBitRate(32000)
self._recorder.setAudioChannels(1) self._recorder.setAudioChannels(1)
self._recorder.setOutputFormat(OutputFormat.OGG) self._recorder.setOutputFormat(OutputFormat.OGG)
self._recorder.setAudioEncoder(AudioEncoder.OPUS) self._recorder.setAudioEncoder(AudioEncoder.OPUS)

View File

@ -1,8 +1,11 @@
import time import time
import threading import threading
import RNS import RNS
import io
from sbapp.plyer.facades.audio import Audio from sbapp.plyer.facades.audio import Audio
from ffpyplayer.player import MediaPlayer from ffpyplayer.player import MediaPlayer
from sbapp.pyogg import OpusFile, OpusBufferedEncoder, OggOpusWriter
import pyaudio
class LinuxAudio(Audio): class LinuxAudio(Audio):
@ -16,7 +19,10 @@ class LinuxAudio(Audio):
self._finished_callback = None self._finished_callback = None
self._loaded_path = None self._loaded_path = None
self.sound = None self.sound = None
self.pa = None
self.is_playing = False self.is_playing = False
self.recorder = None
self.should_record = False
def _check_playback(self): def _check_playback(self):
run = True run = True
@ -33,12 +39,76 @@ class LinuxAudio(Audio):
self._check_thread = None self._check_thread = None
self._finished_callback(self) self._finished_callback(self)
def _record_job(self):
samples_per_second = self.default_rate;
bytes_per_sample = 2; frame_duration_ms = 20
opus_buffered_encoder = OpusBufferedEncoder()
opus_buffered_encoder.set_application("voip")
opus_buffered_encoder.set_sampling_frequency(samples_per_second)
opus_buffered_encoder.set_channels(1)
opus_buffered_encoder.set_frame_size(frame_duration_ms)
ogg_opus_writer = OggOpusWriter(self._file_path, opus_buffered_encoder)
frame_duration = frame_duration_ms/1000
frame_size = int(frame_duration * samples_per_second)
bytes_per_frame = frame_size*bytes_per_sample
read_bytes = 0
pcm_buf = b""
should_continue = True
while self.should_record and self.recorder:
samples_available = self.recorder.get_read_available()
bytes_available = samples_available*bytes_per_sample
if bytes_available > 0:
read_req = bytes_per_frame - len(pcm_buf)
read_n = min(bytes_available, read_req)
read_s = read_n//bytes_per_sample
rb = self.recorder.read(read_s); read_bytes += len(rb)
pcm_buf += rb
if len(pcm_buf) == bytes_per_frame:
ogg_opus_writer.write(memoryview(bytearray(pcm_buf)))
# RNS.log("Wrote frame of "+str(len(pcm_buf))+", expected size "+str(bytes_per_frame))
pcm_buf = b""
# Finish up anything left in buffer
time.sleep(frame_duration)
samples_available = self.recorder.get_read_available()
bytes_available = samples_available*bytes_per_sample
if bytes_available > 0:
read_req = bytes_per_frame - len(pcm_buf)
read_n = min(bytes_available, read_req)
read_s = read_n//bytes_per_sample
rb = self.recorder.read(read_s); read_bytes += len(rb)
pcm_buf += rb
if len(pcm_buf) == bytes_per_frame:
ogg_opus_writer.write(memoryview(bytearray(pcm_buf)))
# RNS.log("Wrote frame of "+str(len(pcm_buf))+", expected size "+str(bytes_per_frame))
pcm_buf = b""
ogg_opus_writer.close()
if self.recorder:
self.recorder.close()
def _start(self): def _start(self):
# TODO: Implement recording self.should_record = True
pass if self.pa == None:
self.pa = pyaudio.PyAudio()
self.default_input_device = self.pa.get_default_input_device_info()
self.default_rate = 48000
# self.default_rate = int(self.default_input_device["defaultSampleRate"])
if self.recorder:
self.recorder.close()
self.recorder = None
self.recorder = self.pa.open(self.default_rate, 1, pyaudio.paInt16, input=True)
threading.Thread(target=self._record_job, daemon=True).start()
def _stop(self): def _stop(self):
if self.sound != None: if self.should_record == True:
self.should_record = False
elif self.sound != None:
self.sound.set_pause(True) self.sound.set_pause(True)
self.sound.seek(0, relative=False) self.sound.seek(0, relative=False)
self.is_playing = False self.is_playing = False

View File

@ -8,7 +8,7 @@ import RNS
import LXMF import LXMF
if RNS.vendor.platformutils.is_android(): if RNS.vendor.platformutils.is_android():
import pyogg from pyogg import OpusFile, OpusBufferedEncoder, OggOpusWriter
from pydub import AudioSegment from pydub import AudioSegment
else: else:
if RNS.vendor.platformutils.is_linux(): if RNS.vendor.platformutils.is_linux():
@ -30,7 +30,7 @@ codec2_modes = {
LXMF.AM_CODEC2_3200: 3200, LXMF.AM_CODEC2_3200: 3200,
} }
def samples_from_ogg(file_path=None): def samples_from_ogg(file_path=None, output_rate=8000):
if file_path != None and os.path.isfile(file_path): if file_path != None and os.path.isfile(file_path):
opus_file = OpusFile(file_path) opus_file = OpusFile(file_path)
audio = AudioSegment( audio = AudioSegment(
@ -41,49 +41,48 @@ def samples_from_ogg(file_path=None):
audio = audio.split_to_mono()[0] audio = audio.split_to_mono()[0]
audio = audio.apply_gain(-audio.max_dBFS) audio = audio.apply_gain(-audio.max_dBFS)
audio = audio.set_frame_rate(8000) audio = audio.set_frame_rate(output_rate)
audio = audio.set_sample_width(2) audio = audio.set_sample_width(2)
return audio.get_array_of_samples() return audio.get_array_of_samples()
def samples_to_ogg(samples=None, file_path=None): def resample(samples, width, channels, input_rate, output_rate, normalize):
audio = AudioSegment(
samples,
frame_rate=input_rate,
sample_width=width,
channels=channels)
if normalize:
audio = audio.apply_gain(-audio.max_dBFS)
resampled_audio = audio.set_frame_rate(output_rate)
return resampled_audio.get_array_of_samples().tobytes()
def samples_to_ogg(samples=None, file_path=None, normalize=False, input_channels=1, input_sample_width=2, input_rate=8000, output_rate=12000, profile="audio"):
try: try:
if file_path != None and samples != None: if file_path != None and samples != None:
if input_rate != output_rate or normalize:
samples = resample(samples, input_sample_width, input_channels, input_rate, output_rate, normalize)
pcm_data = io.BytesIO(samples) pcm_data = io.BytesIO(samples)
channels = input_channels; samples_per_second = output_rate; bytes_per_sample = 2
RNS.log(f"Samples: {len(samples)}") frame_duration_ms = 60
RNS.log(f"Type : {type(samples)}")
channels = 1; samples_per_second = 8000; bytes_per_sample = 2
opus_buffered_encoder = OpusBufferedEncoder() opus_buffered_encoder = OpusBufferedEncoder()
opus_buffered_encoder.set_application("audio") opus_buffered_encoder.set_application(profile)
opus_buffered_encoder.set_sampling_frequency(samples_per_second) opus_buffered_encoder.set_sampling_frequency(samples_per_second)
opus_buffered_encoder.set_channels(channels) opus_buffered_encoder.set_channels(channels)
opus_buffered_encoder.set_frame_size(20) # milliseconds opus_buffered_encoder.set_frame_size(frame_duration_ms)
ogg_opus_writer = OggOpusWriter(file_path, opus_buffered_encoder) ogg_opus_writer = OggOpusWriter(file_path, opus_buffered_encoder)
frame_duration = 0.020 frame_duration = frame_duration_ms/1000.0
frame_size = int(frame_duration * samples_per_second) frame_size = int(frame_duration * samples_per_second)
bytes_per_frame = frame_size*bytes_per_sample bytes_per_frame = frame_size*bytes_per_sample
read_bytes = 0 ogg_opus_writer.write(memoryview(bytearray(samples)))
written_bytes = 0
while True:
pcm = pcm_data.read(bytes_per_frame)
if len(pcm) == 0:
break
else:
read_bytes += len(pcm)
ogg_opus_writer.write(memoryview(bytearray(pcm)))
written_bytes += len(pcm)
ogg_opus_writer.close() ogg_opus_writer.close()
RNS.log(f"Read {read_bytes} bytes")
RNS.log(f"Wrote {written_bytes} bytes")
return True return True
except Exception as e: except Exception as e: