2021-04-11 12:40:28 +00:00
|
|
|
from owrx.config.core import CoreConfig
|
|
|
|
from owrx.audio import AudioChopperProfile
|
2021-08-31 20:46:11 +00:00
|
|
|
from owrx.audio.queue import DecoderQueue
|
2021-04-11 12:40:28 +00:00
|
|
|
import threading
|
|
|
|
import wave
|
|
|
|
import os
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from queue import Full
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger.setLevel(logging.INFO)
|
|
|
|
|
|
|
|
|
|
|
|
class WaveFile(object):
|
2021-05-05 20:55:20 +00:00
|
|
|
def __init__(self, writer_id):
|
|
|
|
self.timestamp = datetime.utcnow()
|
|
|
|
self.writer_id = writer_id
|
|
|
|
tmp_dir = CoreConfig().get_temporary_directory()
|
|
|
|
self.filename = "{tmp_dir}/openwebrx-audiochopper-master-{id}-{timestamp}.wav".format(
|
|
|
|
tmp_dir=tmp_dir,
|
|
|
|
id=self.writer_id,
|
|
|
|
timestamp=self.timestamp.strftime("%y%m%d_%H%M%S"),
|
|
|
|
)
|
|
|
|
self.waveFile = wave.open(self.filename, "wb")
|
2021-04-11 12:40:28 +00:00
|
|
|
self.waveFile.setnchannels(1)
|
|
|
|
self.waveFile.setsampwidth(2)
|
|
|
|
self.waveFile.setframerate(12000)
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
self.waveFile.close()
|
|
|
|
|
|
|
|
def getFileName(self):
|
|
|
|
return self.filename
|
|
|
|
|
2021-05-05 20:55:20 +00:00
|
|
|
def getTimestamp(self):
|
|
|
|
return self.timestamp
|
|
|
|
|
2021-04-11 12:40:28 +00:00
|
|
|
def writeframes(self, data):
|
|
|
|
return self.waveFile.writeframes(data)
|
|
|
|
|
|
|
|
def unlink(self):
|
|
|
|
os.unlink(self.filename)
|
|
|
|
self.waveFile = None
|
|
|
|
|
|
|
|
|
|
|
|
class AudioWriter(object):
|
2021-08-31 20:46:11 +00:00
|
|
|
def __init__(self, chopper, interval, profiles: List[AudioChopperProfile]):
|
|
|
|
self.chopper = chopper
|
2021-04-11 12:40:28 +00:00
|
|
|
self.interval = interval
|
|
|
|
self.profiles = profiles
|
|
|
|
self.wavefile = None
|
|
|
|
self.switchingLock = threading.Lock()
|
|
|
|
self.timer = None
|
|
|
|
|
|
|
|
def getWaveFile(self):
|
2021-05-05 20:55:20 +00:00
|
|
|
return WaveFile(id(self))
|
2021-04-11 12:40:28 +00:00
|
|
|
|
|
|
|
def getNextDecodingTime(self):
|
2021-05-01 22:06:50 +00:00
|
|
|
# add one second to have the intervals tick over one second earlier
|
|
|
|
# this avoids filename collisions, but also avoids decoding wave files with less than one second of audio
|
|
|
|
t = datetime.utcnow() + timedelta(seconds=1)
|
2021-04-11 12:40:28 +00:00
|
|
|
zeroed = t.replace(minute=0, second=0, microsecond=0)
|
|
|
|
delta = t - zeroed
|
|
|
|
seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval
|
|
|
|
t = zeroed + timedelta(seconds=seconds)
|
|
|
|
logger.debug("scheduling: {0}".format(t))
|
|
|
|
return t
|
|
|
|
|
|
|
|
def cancelTimer(self):
|
|
|
|
if self.timer:
|
|
|
|
self.timer.cancel()
|
|
|
|
self.timer = None
|
|
|
|
|
|
|
|
def _scheduleNextSwitch(self):
|
|
|
|
self.cancelTimer()
|
|
|
|
delta = self.getNextDecodingTime() - datetime.utcnow()
|
|
|
|
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
|
|
|
|
self.timer.start()
|
|
|
|
|
|
|
|
def switchFiles(self):
|
|
|
|
with self.switchingLock:
|
|
|
|
file = self.wavefile
|
|
|
|
self.wavefile = self.getWaveFile()
|
|
|
|
|
2022-11-21 04:33:13 +00:00
|
|
|
if file is not None:
|
|
|
|
file.close()
|
|
|
|
tmp_dir = CoreConfig().get_temporary_directory()
|
|
|
|
|
|
|
|
for profile in self.profiles:
|
|
|
|
# create hardlinks for the individual profiles
|
|
|
|
filename = "{tmp_dir}/openwebrx-audiochopper-{pid}-{timestamp}.wav".format(
|
|
|
|
tmp_dir=tmp_dir,
|
|
|
|
pid=id(profile),
|
|
|
|
timestamp=file.getTimestamp().strftime(profile.getFileTimestampFormat()),
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
os.link(file.getFileName(), filename)
|
|
|
|
except OSError:
|
|
|
|
logger.exception("Error while linking job files")
|
|
|
|
continue
|
|
|
|
|
|
|
|
job = self.chopper.createJob(profile, filename)
|
|
|
|
try:
|
|
|
|
DecoderQueue.getSharedInstance().put(job)
|
|
|
|
except Full:
|
|
|
|
logger.warning("decoding queue overflow; dropping one file")
|
|
|
|
job.unlink()
|
2021-04-11 12:40:28 +00:00
|
|
|
|
2021-04-29 23:20:33 +00:00
|
|
|
try:
|
2022-11-21 04:33:13 +00:00
|
|
|
# our master can be deleted now, the profiles will delete their hardlinked copies after processing
|
|
|
|
file.unlink()
|
2021-04-29 23:20:33 +00:00
|
|
|
except OSError:
|
2022-11-21 04:33:13 +00:00
|
|
|
logger.exception("Error while unlinking job files")
|
2021-04-11 12:40:28 +00:00
|
|
|
|
2022-11-21 04:33:13 +00:00
|
|
|
self._scheduleNextSwitch()
|
2021-04-11 12:40:28 +00:00
|
|
|
|
|
|
|
def start(self):
|
|
|
|
self.wavefile = self.getWaveFile()
|
|
|
|
self._scheduleNextSwitch()
|
|
|
|
|
|
|
|
def write(self, data):
|
|
|
|
with self.switchingLock:
|
|
|
|
self.wavefile.writeframes(data)
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.cancelTimer()
|
|
|
|
try:
|
|
|
|
self.wavefile.close()
|
|
|
|
except Exception:
|
|
|
|
logger.exception("error closing wave file")
|
|
|
|
try:
|
|
|
|
with self.switchingLock:
|
|
|
|
self.wavefile.unlink()
|
|
|
|
except Exception:
|
|
|
|
logger.exception("error removing undecoded file")
|
|
|
|
self.wavefile = None
|