Noise Detector

  
 noise_detector.py 
 # Threshold / Sliding window
# https://raw.githubusercontent.com/jeysonmc/python-google-speech-scripts/master/stt_google.py

# WebSocket streaming:
# https://gist.github.com/fopina/3cefaed1b2d2d79984ad7894aef39a68

import pyaudio
import wave
import audioop
import subprocess
import os
import time
import math
import struct
import threading
import io
import numpy as np
from collections import deque

from lock_manager import Lock_Manager
from util import Util

class Noise_Detector(threading.Thread):
 def __init__(self):
 threading.Thread.__init__(self)

 self.name = self.__class__.__name__

 self.FORMAT = pyaudio.paFloat32
 self.RATE = 48000 # Hz, so samples (bytes) per second
 self.CHUNK_SIZE = 8192 # How many bytes to read from mic each time (stream.read())
 self.CHUNKS_PER_SEC = math.floor(self.RATE / self.CHUNK_SIZE) # How many chunks make a second? (16.000 bytes/s, each chunk is 1.024 bytes, so 1s is 15 chunks)
 self.CHANNELS = 1
 self.HISTORY_LENGTH = 2 # Seconds of audio cache for prepending to records to prevent chopped phrases (history length + observer length = min record length)
 self.OBSERVER_LENGTH = 5 # Time in seconds to be observed for noise
 self.NOTIFICATION_LIMIT = 1 # Seconds before a notification is sent
 self.LIMIT_RECODING = 100 # 최대 Recoding chunk 수 
 self.CURRENT_RECODING_TIME = 0 # 현재 Recoding chunk 수 
 self.REMAIN_RECORDING_FILES = 3 # 10이상 부터 삭제 후 저장 
 self.RECODING_OVER_THRESHOLD = 5 # Recoding 임계값을 연속 넘는 회수로 저장 여부 판단 

 self.archive = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'archive')
 self.current_file = None
 self.chunk = None
 self.record = [] # Stores audio chunks
 self.notified = False # If we already sent a notification

 self.audio = pyaudio.PyAudio()
 self.stream = self.get_stream()

 self.threshold = self.determine_threshold()

 self.lock_manager = Lock_Manager("noise")
 self.detected_at = None

 def __del__(self):
 # Stop recording
 if self.stream:
 self.stream.close()

 if self.audio:
 self.audio.terminate()

 # Remove lock if exists
 self.lock_manager.remove()

 def get_stream(self):
 """
 Open audio stream

 @return PyAudio
 """
 return self.audio.open(
 format=self.FORMAT,
 channels=self.CHANNELS,
 rate=self.RATE,
 input=True,
 frames_per_buffer=self.CHUNK_SIZE
 )

 def determine_threshold(self):
 """
 Determine threshold noise intensity using RMS
 Anything below the threshold is considered silence

 @return float
 """
 Util.log(self.name, "Determining threshold...")

 res = []
 for x in range(50):
 block = self.stream.read(self.CHUNK_SIZE)
 rms = self.get_rms(block)
 res.append(rms)

 # Set threshold to 20% above avergae
 threshold = (sum(res) / len(res)) * 1.7 #1.2
 Util.log(self.name, "Setting threshold to: " + str(threshold))

 return threshold

 def get_rms(self, block):
 """
 Calculate Root Mean Square (noise level) for audio chunk

 @param bytes block
 @return float
 """
 d = np.frombuffer(block, np.float32).astype(np.float)
 return np.sqrt((d * d).sum() / len(d))

 def start_recording(self):
 """
 Setup the recorder
 """
 self.current_file = self.archive + "/" + self.detected_at + ".wav"

 Util.log(self.name, "Noise detected! Recording...")

 def stop_recording(self):
 """
 Reset variables to default
 """
 self.current_file = None
 self.detected_at = None
 self.notified = False
 self.record = []
 self.CURRENT_RECODING_TIME = 0

 def run(self):
 """
 Detect noise from microphone and record
 Noise is defined as sound surrounded by silence (according to threshold)
 """

 # Stores audio intensity of previous sound-chunks
 # If one of these chunks is above threshold, recording gets triggered
 # Keep the last {OBSERVER_LENGTH} seconds in observer
 observer = deque(maxlen=self.OBSERVER_LENGTH * self.CHUNKS_PER_SEC)

 # Prepend audio from before noise was detected
 # Keep the last {HISTORY_LENGTH} seconds in history
 history = deque(maxlen=self.HISTORY_LENGTH * self.CHUNKS_PER_SEC)

 Util.log(self.name, "Listening...")

 try:
 while True:
 # Current chunk of audio data
 self.chunk = self.stream.read(self.CHUNK_SIZE, exception_on_overflow = False)
 history.append(self.chunk)

 # Add noise level of this chunk to the sliding-window
 rms = self.get_rms(self.chunk)
 #Util.log(self.name, "Noise threshold=" + str(rms))
 observer.append(rms)

 if self.detected(sum([x > self.threshold for x in observer]) > self.RECODING_OVER_THRESHOLD) and self.LIMIT_RECODING > self.CURRENT_RECODING_TIME:
 self.CURRENT_RECODING_TIME = self.CURRENT_RECODING_TIME + 1 
 # There's at least one chunk in the sliding-window above threshold
 if not self.recording():
 self.start_recording()

 #Util.log(self.name, "Record.append noise level="+ str(sum([x > self.threshold for x in observer])) + ", time=" + str(self.CURRENT_RECODING_TIME) )
 self.record.append(self.chunk)

 if not self.notified and len(self.record) > self.NOTIFICATION_LIMIT * self.CHUNKS_PER_SEC:
 self.notify()
 elif self.recording():
 # Silence limit was reached, finish recording and save
 self.delete()
 self.save(list(history) + self.record)
 self.stop_recording()

 Util.log(self.name, "Listening...")
 except KeyboardInterrupt:
 Util.log(self.name, "Interrupted.")

 def get_chunk(self):
 """
 Return the current chunk of audio data

 @return bytes
 """
 return self.chunk

 def delete(self):
 """
 delete mic data to a WAV file.
 @param list data
 """
 count = 0
 Util.log(self.name, "Delete audio...")
 for filename in sorted(os.listdir(self.archive), reverse=True):
 if not filename.startswith('.'):
 type = self.get_type(filename)
 if type == "audio":
 count = count + 1
 if self.REMAIN_RECORDING_FILES < count:
 Util.log(self.name, "Delete audio filename=" + filename + ", type=" + type + ", count=" + str(count))
 os.remove(self.archive + "/" + filename)
 
 def get_type(self, filename):
 name, extension = os.path.splitext(filename)
 return 'video' if extension == '.mp4' else 'video' if extension == '.avi' else 'audio' if extension == '.wav' else 'audio' if extension == '.mp3' else 'photo'

 def save(self, data):
 """
 Save mic data to a WAV file.

 @param list data
 """
 Util.log(self.name, "Saving audio...")

 # Flatten the list
 data = b''.join(data)

 # Write converted data to file
 with open(self.current_file, "wb+") as file:
 file.write(self.generate_wav(data))

 # Convert 음질 개떡 
 #self.convert_to_mp3(self.current_file)

 def bytes_to_array(self, bytes, type):
 """
 Convert raw audio data to TypedArray

 @param bytes bytes
 @return numpy-Array
 """
 return np.frombuffer(bytes, dtype=type)

 def generate_wav(self, raw):
 """
 Create WAVE-file from raw audio chunks

 @param bytes raw
 @return bytes
 """
 # Check if input format is supported
 if self.FORMAT not in (pyaudio.paFloat32, pyaudio.paInt16):
 print("Unsupported format")
 return

 # Convert raw audio bytes to typed array
 samples = self.bytes_to_array(raw, np.float32)

 # Get sample size
 sample_size = pyaudio.get_sample_size(self.FORMAT)

 # Get data-length
 byte_count = (len(samples)) * sample_size

 # Get bits/sample
 bits_per_sample = sample_size * 8

 # Calculate frame-size
 frame_size = int(self.CHANNELS * ((bits_per_sample + 7) / 8))

 # Container for WAVE-content
 wav = bytearray()

 # Start RIFF-Header
 wav.extend(struct.pack('<cccc', b'R', b'I', b'F', b'F'))
 # Add chunk size (data-size minus 8)
 wav.extend(struct.pack('<I', byte_count + 0x2c - 8))
 # Add RIFF-type ("WAVE")
 wav.extend(struct.pack('<cccc', b'W', b'A', b'V', b'E'))

 # Start "Format"-part
 wav.extend(struct.pack('<cccc', b'f', b'm', b't', b' '))
 # Add header length (16 bytes)
 wav.extend(struct.pack('<I', 0x10))
 # Add format-tag (e.g. 1 = PCM, 3 = FLOAT)
 wav.extend(struct.pack('<H', 3))
 # Add channel count
 wav.extend(struct.pack('<H', self.CHANNELS))
 # Add sample rate
 wav.extend(struct.pack('<I', self.RATE))
 # Add bytes/second
 wav.extend(struct.pack('<I', self.RATE * frame_size))
 # Add frame size
 wav.extend(struct.pack('<H', frame_size))
 # Add bits/sample
 wav.extend(struct.pack('<H', bits_per_sample))

 # Start data-part
 wav.extend(struct.pack('<cccc', b'd', b'a', b't', b'a'))
 # Add data-length
 wav.extend(struct.pack('<I', byte_count))

 # Add data
 for sample in samples:
 wav.extend(struct.pack("<f", sample))

 return bytes(wav)

 def convert_to_mp3(self, path):
 """
 Convert wav-file to mp3

 @param string path
 """
 Util.log(self.name, "Converting audio...")

 try:
 cmd = 'lame --preset insane "{}" 2> /dev/null && rm "{}"'.format(path, path)
 p = subprocess.Popen(cmd, shell=True)
 (output, err) = p.communicate()

 except subprocess.CalledProcessError:
 Util.log(self.name, "Error converting audio")

 def detected(self, has_noise):
 """
 Check if this or another detector detected something

 @param boolean has_noise
 @return boolean
 """
 if has_noise:
 self.lock_manager.set()
 else:
 self.lock_manager.remove()

 self.detected_at = self.lock_manager.get_lock_time()

 return self.detected_at is not None

 def recording(self):
 """
 Check if currently recording

 @return boolean
 """
 return len(self.record) > 0

 def notify(self):
 """
 Notify
 """
 Util.log(self.name, "Notifying")
 self.notified = True

if __name__ == "__main__":
 nd = Noise_Detector()
 nd.start()