Initial commit
This commit is contained in:
commit
daa2b218b2
142
.dockerignore
Normal file
142
.dockerignore
Normal file
@ -0,0 +1,142 @@
|
||||
.git
|
||||
Dockerfile
|
||||
.DS_Store
|
||||
.gitignore
|
||||
.dockerignore
|
||||
|
||||
/credentials
|
||||
/cache
|
||||
/store
|
||||
|
||||
/node_modules
|
||||
|
||||
# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
|
||||
# General
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
163
.gitignore
vendored
Normal file
163
.gitignore
vendored
Normal file
@ -0,0 +1,163 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
temp/
|
||||
recordings/
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt /app
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV FLASK_APP=src/app.py
|
||||
|
||||
CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=8081"]
|
14
docker-compose.yaml
Normal file
14
docker-compose.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
version: '2.1'
|
||||
services:
|
||||
rat:
|
||||
container_name: rat
|
||||
image: rat
|
||||
build: .
|
||||
network_mode: host
|
||||
volumes:
|
||||
- "./src:/app/src"
|
||||
- "./temp:/app/temp"
|
||||
- "./recordings:/app/recordings"
|
||||
environment:
|
||||
CLIENT_URL: "http://192.168.0.119:5000"
|
||||
HARPYIA_URL: "http://localhost:8080"
|
15
requirements.txt
Normal file
15
requirements.txt
Normal file
@ -0,0 +1,15 @@
|
||||
blinker==1.7.0
|
||||
certifi==2024.2.2
|
||||
charset-normalizer==3.3.2
|
||||
click==8.1.7
|
||||
Flask==3.0.2
|
||||
idna==3.6
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.3
|
||||
MarkupSafe==2.1.5
|
||||
numpy==2.0.0
|
||||
python-dotenv==1.0.1
|
||||
requests==2.31.0
|
||||
scipy==1.13.1
|
||||
urllib3==2.2.1
|
||||
Werkzeug==3.0.1
|
201
src/app.py
Normal file
201
src/app.py
Normal file
@ -0,0 +1,201 @@
|
||||
from flask import Flask, abort, request, jsonify
|
||||
from tempfile import NamedTemporaryFile
|
||||
from datetime import datetime
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
import base64
|
||||
import io
|
||||
import time
|
||||
import glob
|
||||
import traceback
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from wav_recorder import WavRecorder
|
||||
from wav_composer import WavComposer
|
||||
from channel_connector import ChannelConnector
|
||||
|
||||
load_dotenv()
|
||||
|
||||
CLIENT_URL = os.getenv('CLIENT_URL') or ''
|
||||
HARPYIA_URL = os.getenv('HARPYIA_URL') or ''
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
file_number = 1
|
||||
temp_directory = 'temp/'
|
||||
recordings_directory = 'recordings/'
|
||||
|
||||
def prepare_dirs():
|
||||
os.makedirs(os.path.dirname(temp_directory), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(recordings_directory), exist_ok=True)
|
||||
files = glob.glob(temp_directory + "/*")
|
||||
for f in files:
|
||||
os.remove(f)
|
||||
|
||||
prepare_dirs()
|
||||
|
||||
wav_recorder = WavRecorder(recordings_directory)
|
||||
wav_composer = WavComposer(16000)
|
||||
channel_connector = ChannelConnector(16000)
|
||||
|
||||
@app.route("/")
|
||||
def hello():
|
||||
return "To recognize an audio file, upload it using a POST request with '/save_audio' route."
|
||||
|
||||
@app.route("/time")
|
||||
def get_time():
|
||||
current_timestamp = int(time.time())
|
||||
return {"current_timestamp": current_timestamp}
|
||||
|
||||
@app.route("/aid/ready", methods=['POST'])
|
||||
def count():
|
||||
# requests.post(CLIENT_URL + '/state', json={'state': 1})
|
||||
return jsonify({}), 200
|
||||
|
||||
@app.route('/aid/transcript', methods=['POST'])
|
||||
def handler():
|
||||
data = request.json
|
||||
audio_content = data['audio']['content']
|
||||
channel = data['audio']['channel']
|
||||
|
||||
data['audio']['content'] = ''
|
||||
print(data, file=sys.stderr)
|
||||
|
||||
initial_time = data['audio']['initialTime']
|
||||
micros = data['audio']['micros']
|
||||
|
||||
# Decode base64 audio content
|
||||
audio_bytes = base64.b64decode(audio_content)
|
||||
|
||||
# Save the audio as a WAV file
|
||||
buf = io.BytesIO()
|
||||
wav_composer.compose(buf, audio_bytes, channel=channel)
|
||||
buf.seek(0)
|
||||
|
||||
try:
|
||||
requests.post(CLIENT_URL + '/state', json={'state': 0})
|
||||
|
||||
files = {'file': buf}
|
||||
response = requests.post(HARPYIA_URL + '/recognize', files=files)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
results = data.get('results', [])
|
||||
transcript = ''
|
||||
|
||||
for entity in results:
|
||||
transcript = f'{transcript} {entity.get("transcript")}'
|
||||
|
||||
record_time = initial_time + int(micros / 1000000)
|
||||
record_time_str = datetime.fromtimestamp(record_time).strftime('%Y-%m-%d %H:%M:%S')
|
||||
record_millis_str = str(int(micros / 1000) % 1000).zfill(3)
|
||||
record_micros_str = str(micros % 1000).zfill(3)
|
||||
message = f'[{record_time_str}:{record_millis_str}:{record_micros_str}] {transcript}'
|
||||
print(message, file=sys.stderr)
|
||||
|
||||
requests.post(CLIENT_URL + '/message', json={'message': message, 'results': entity.get('results')})
|
||||
|
||||
return jsonify({'message': transcript})
|
||||
else:
|
||||
return jsonify({'error': 'Error occurred on Harpyia'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/aid/save', methods=['POST'])
|
||||
def save_audio():
|
||||
try:
|
||||
data = request.json
|
||||
audio_content = data['audio']['content']
|
||||
|
||||
micros = data['audio']['micros']
|
||||
initial_time = data['audio']['initialTime']
|
||||
record_time = initial_time + int(micros / 1000000)
|
||||
record_millis_str = str(int(micros / 1000) % 1000).zfill(3)
|
||||
record_micros_str = str(micros % 1000).zfill(3)
|
||||
recording_time = datetime.fromtimestamp(record_time).strftime("%d_%m_%Y__%H_%M_%S")
|
||||
recording_time += f'_{record_millis_str}_{record_micros_str}'
|
||||
|
||||
channel = data['audio']['channel']
|
||||
|
||||
save_path = compose_save_path(recording_time, channel)
|
||||
audio_bytes = base64.b64decode(audio_content)
|
||||
|
||||
wav_composer.compose(save_path, audio_bytes)
|
||||
audio_bytes = wav_composer.remove_initial_segment(audio_bytes, 100)
|
||||
channel_connector.add_audio(audio_bytes, channel)
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Audio saved successfully'})
|
||||
except Exception as e:
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
def compose_save_path(recording_time, channel):
|
||||
global file_number
|
||||
|
||||
save_path = f'{temp_directory}saved_audio{str(file_number)}__channel{channel}__{recording_time}.wav'
|
||||
print(f'The audio file will be saved to {save_path}', file=sys.stderr)
|
||||
|
||||
file_number += 1
|
||||
return save_path
|
||||
|
||||
@app.route("/recorder/client/start")
|
||||
def start_recording():
|
||||
try:
|
||||
data = request.json
|
||||
audio_content = data['content']
|
||||
|
||||
wav_recorder.start_recording()
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Audio file saved'})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
@app.route("/recorder/cheeze/check")
|
||||
def check_recording():
|
||||
try:
|
||||
return jsonify({'status': 'success', 'is_recording': wav_recorder.is_recording()})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
@app.route("/recorder/cheeze/send_header", methods=['POST'])
|
||||
def receive_header():
|
||||
try:
|
||||
data = request.json
|
||||
audio_header = data['header']
|
||||
|
||||
wav_recorder.set_header(audio_header)
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Header received'})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
@app.route("/recorder/cheeze/send_part", methods=['POST'])
|
||||
def receive_part():
|
||||
try:
|
||||
data = request.json
|
||||
audio_content = data['content']
|
||||
|
||||
wav_recorder.append_data(audio_content)
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Content part received'})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
@app.route("/recorder/client/stop")
|
||||
def stop_recording():
|
||||
try:
|
||||
data = request.json
|
||||
audio_content = data['content']
|
||||
|
||||
buf = io.BytesIO()
|
||||
wav_recorder.save_recording(buf=buf)
|
||||
buf.seek(0)
|
||||
|
||||
return jsonify({'status': 'success', 'content': buf})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
50
src/channel_connector.py
Normal file
50
src/channel_connector.py
Normal file
@ -0,0 +1,50 @@
|
||||
import audioop
|
||||
import sys
|
||||
from datetime import datetime
|
||||
import threading
|
||||
|
||||
from sound_localizer import SoundLocalizer
|
||||
|
||||
class ChannelConnector:
|
||||
def __init__(self, sample_rate, sample_width=2):
|
||||
self.cached_audios = {}
|
||||
self.previous_time = None
|
||||
self.lock = threading.Lock()
|
||||
self.timer = None
|
||||
self.sample_width = sample_width
|
||||
self.sound_localizer = SoundLocalizer(sample_rate)
|
||||
|
||||
def add_audio(self, audio_bytes, channel):
|
||||
with self.lock:
|
||||
self.cached_audios[channel] = audio_bytes
|
||||
|
||||
if self.previous_time is None:
|
||||
self.previous_time = datetime.now()
|
||||
# Schedule the reset and comparison after 1 second
|
||||
self.timer = threading.Timer(1.0, self.reset_and_compare)
|
||||
self.timer.start()
|
||||
|
||||
def reset_and_compare(self):
|
||||
with self.lock:
|
||||
if 0 in self.cached_audios and 1 in self.cached_audios:
|
||||
self.compare(self.cached_audios[0], self.cached_audios[1])
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.previous_time = None
|
||||
self.cached_audios = {}
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
self.timer = None
|
||||
|
||||
def compare(self, audio_left, audio_right):
|
||||
left_rms = audioop.rms(audio_left, self.sample_width)
|
||||
right_rms = audioop.rms(audio_right, self.sample_width)
|
||||
|
||||
if left_rms > 30 or right_rms > 30:
|
||||
print(f"Left channel RMS: {left_rms}", file=sys.stderr)
|
||||
print(f"Right channel RMS: {right_rms}", file=sys.stderr)
|
||||
print('left' if left_rms > right_rms else 'right', file=sys.stderr)
|
||||
|
||||
angle = self.sound_localizer.estimate_source_angle(audio_left, audio_right)
|
||||
print(f'Approximate angle: {angle}', file=sys.stderr)
|
52
src/sound_localizer.py
Normal file
52
src/sound_localizer.py
Normal file
@ -0,0 +1,52 @@
|
||||
import numpy as np
|
||||
from scipy.signal import correlate
|
||||
import math
|
||||
import sys
|
||||
|
||||
class SoundLocalizer:
|
||||
def __init__(self, sample_rate, channels=2):
|
||||
self.sample_rate = sample_rate
|
||||
self.channels = channels
|
||||
|
||||
def _compute_tdoa(self, sig1, sig2, framerate):
|
||||
correlation = correlate(sig1, sig2, mode='full')
|
||||
lag = np.argmax(correlation) - (len(sig1) - 1)
|
||||
tdoa = lag / framerate
|
||||
return tdoa
|
||||
|
||||
def _calculate_angle(self, tdoa, speed_of_sound, distance_between_mics, mic_angles):
|
||||
angles = []
|
||||
normalized_tdoa = tdoa * speed_of_sound / distance_between_mics
|
||||
normalized_tdoa = np.clip(normalized_tdoa, -1, 1)
|
||||
tdoa_angle = math.asin(normalized_tdoa) * 180 / math.pi
|
||||
for angle in mic_angles:
|
||||
angles.append(angle + tdoa_angle)
|
||||
return np.mean(angles)
|
||||
|
||||
def estimate_source_angle(self, audio_left, audio_right, \
|
||||
speed_of_sound=343.0, distance_between_mics=0.1):
|
||||
|
||||
audio_left = np.frombuffer(audio_left, dtype=np.int16)
|
||||
audio_left = audio_left.reshape(-1, self.channels)
|
||||
audio_right = np.frombuffer(audio_right, dtype=np.int16)
|
||||
audio_right = audio_right.reshape(-1, self.channels)
|
||||
|
||||
mic1 = audio_left[:, 0]
|
||||
mic2 = audio_left[:, 1]
|
||||
mic3 = audio_right[:, 0]
|
||||
mic4 = audio_right[:, 1]
|
||||
|
||||
tdoa_12 = self._compute_tdoa(mic1, mic2, self.sample_rate)
|
||||
tdoa_13 = self._compute_tdoa(mic1, mic3, self.sample_rate)
|
||||
tdoa_14 = self._compute_tdoa(mic1, mic4, self.sample_rate)
|
||||
print(tdoa_12, tdoa_13, tdoa_14, file=sys.stderr)
|
||||
|
||||
mic_angles = [-33, -11, 11, 33]
|
||||
|
||||
angle_12 = self._calculate_angle(tdoa_12, speed_of_sound, distance_between_mics, mic_angles[:2])
|
||||
angle_13 = self._calculate_angle(tdoa_13, speed_of_sound, distance_between_mics, mic_angles[:3])
|
||||
angle_14 = self._calculate_angle(tdoa_14, speed_of_sound, distance_between_mics, mic_angles)
|
||||
|
||||
final_angle = np.mean([angle_12, angle_13, angle_14])
|
||||
|
||||
return final_angle
|
34
src/wav_composer.py
Normal file
34
src/wav_composer.py
Normal file
@ -0,0 +1,34 @@
|
||||
import wave
|
||||
import audioop
|
||||
|
||||
class WavComposer:
|
||||
def __init__(self, sample_rate, channels=2, sample_width=2):
|
||||
self.sample_rate = sample_rate
|
||||
self.channels = channels
|
||||
self.sample_width = sample_width
|
||||
|
||||
def compose(self, save_path, audio_bytes):
|
||||
with wave.open(save_path, 'wb') as wav_file:
|
||||
wav_file.setnchannels(self.channels) # Stereo audio
|
||||
wav_file.setsampwidth(self.sample_width) # 16-bit audio
|
||||
wav_file.setframerate(self.sample_rate) # Sample rate
|
||||
|
||||
#left_channel = audioop.tomono(audio_bytes, 2, 1, 0)
|
||||
#right_channel = audioop.tomono(audio_bytes, 2, 0, 1)
|
||||
|
||||
audio_bytes = audioop.mul(audio_bytes, 2, 40)
|
||||
|
||||
#left_rms = audioop.rms(left_channel, 2)
|
||||
#right_rms = audioop.rms(right_channel, 2)
|
||||
|
||||
#print(f"Left channel RMS: {left_rms}", file=sys.stderr)
|
||||
#print(f"Right channel RMS: {right_rms}", file=sys.stderr)
|
||||
#print('left' if left_rms > right_rms else 'right', file=sys.stderr)
|
||||
|
||||
wav_file.writeframesraw(audio_bytes)
|
||||
|
||||
def remove_initial_segment(self, audio_bytes, ms):
|
||||
bytes_per_sample = self.sample_width * self.channels
|
||||
num_samples_to_remove = int((ms / 1000.0) * self.sample_rate)
|
||||
bytes_to_remove = num_samples_to_remove * bytes_per_sample
|
||||
return audio_bytes[bytes_to_remove:]
|
40
src/wav_recorder.py
Normal file
40
src/wav_recorder.py
Normal file
@ -0,0 +1,40 @@
|
||||
from wav_composer import WavComposer
|
||||
import datetime
|
||||
|
||||
class WavRecorder:
|
||||
def __init__(self, output_directory):
|
||||
self._header = b''
|
||||
self._data_chunks = []
|
||||
self._output_directory = output_directory
|
||||
self._is_recording = False
|
||||
|
||||
def start_recording(self):
|
||||
self._is_recording = True
|
||||
|
||||
def set_header(self, header):
|
||||
self._header = header
|
||||
|
||||
def append_data(self, data):
|
||||
if not self._is_recording:
|
||||
raise RuntimeError("Cannot append data. Recorder is not recording.")
|
||||
self._data_chunks.append(data)
|
||||
|
||||
def save_recording(self, buf=None):
|
||||
self._write_to_file(buf)
|
||||
self._reset_recorder()
|
||||
|
||||
def _write_to_file(self, buf=None):
|
||||
wav_data = self._header + b''.join(data for data in self._data_chunks)
|
||||
output_path = buf if buf else f'{self._output_directory}/{filename}'
|
||||
WavComposer.compose(output_path, wav_data)
|
||||
|
||||
print(f'Saved recording to {output_path}')
|
||||
|
||||
def _reset_recorder(self):
|
||||
self._header = b''
|
||||
self._data_chunks.clear()
|
||||
self._is_recording = False
|
||||
|
||||
def is_recording(self):
|
||||
return self._is_recording
|
||||
|
Loading…
x
Reference in New Issue
Block a user