Skip to content

Commit

Permalink
First commit in a long time, full refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanpeach committed Mar 16, 2024
1 parent aed4f8a commit 70ed2ab
Show file tree
Hide file tree
Showing 13 changed files with 1,909 additions and 403 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: ci

on:
pull_request:
branches:
- main
- master

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
poetry install
- uses: chartboost/ruff-action@v1
- name: mypy
run: mypy dogbarking
43 changes: 43 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
exclude: "docs|.git"
default_stages: [commit]
fail_fast: false

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files
- id: check-merge-conflict
- id: check-json
- id: check-toml
- id: check-yaml
- id: detect-private-key
- id: mixed-line-ending
args: ["--fix=lf"]
- id: pretty-format-json
args: ["--autofix"]

- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.5
hooks:
- id: remove-tabs
- id: remove-crlf

# Flake8 and black
- repo: https://github.com/psf/black
rev: 24.3.0
hooks:
- id: black

- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.3.3
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
types_or: [ python, pyi, jupyter ]
20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2012-2024 Scott Chacon and others

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 changes: 13 additions & 8 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ A simple for recording audio, and playing a frequency when the volume is over so

This app has many uses, but is made originally for training a dog to stop barking. By playing a high pitched, loud sound whenever my computer detected high volume noise, I trained my dog to stop barking while i was gone.

## Usage
# Installation

```bash
# If you are on macosx, be sure to do this, otherwise skip
brew install portaudio

> python3 main.py --help
# Then just install the requirements with poetry
poetry install

## To-Do
# Then install pre-commit (only if you are contributing)
pre-commit install
```

## Usage

- [X] Argument parsing
- [ ] Device Selection
- [ ] Pandas Datetime
- [ ] Output CSV
- [ ] Dash plotting
> python3 -m dog_barking
Empty file added dogbarking/__init__.py
Empty file.
53 changes: 53 additions & 0 deletions dogbarking/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Annotated
from datetime import datetime
import pyaudio
import toml
import typer
from typer_config import use_toml_config

from dogbarking.audio import Player, Recorder
from dogbarking.math import get_rms

app = typer.Typer()


@app.command()
@use_toml_config
def nogui(
volume: Annotated[float, "The percentage volume, must be between 0 and 1."] = 1.0,
thresh: Annotated[float, "The threshold for the sound when playing."] = 0.0001,
frequency: Annotated[float, "The frequency to play for the dog."] = 17000.0,
duration: Annotated[float, "The duration to play the sound."] = 1.0,
sample_freq: Annotated[int, "The sample rate in Hz."] = 44100,
chunk: Annotated[int, "The number of frames per buffer."] = 10240,
channels: Annotated[int, "The number of audio channels."] = 1,
# email: Annotated[Optional[str], "The email to send the alert to."]=None
):
# Start Recording
audio = pyaudio.PyAudio()
p = Player(audio, volume=volume, duration=duration, freq=frequency, fs=sample_freq)
r = Recorder(
audio,
channels=channels,
sample_freq=sample_freq,
input=True,
frames_per_buffer=chunk,
)
r.start()

# If the rms of the waveform is greater than the threshold, play the sound
for waveform in r:
rms = get_rms(waveform)
print(f"RMS: {rms}")
if rms > thresh:
p.start()
p.play_sound()
p.stop()
print(f"Dog Barking at {datetime.now()}")


if __name__ == "__main__":
pyproject_toml = toml.load("pyproject.toml")
NAME = pyproject_toml["tool"]["poetry"]["name"]
DESCRIPTION = pyproject_toml["tool"]["poetry"]["description"]
app()
91 changes: 91 additions & 0 deletions dogbarking/audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import pyaudio
import numpy as np

# REF: https://gist.github.com/mabdrabo/8678538
FORMAT = pyaudio.paFloat32


class Recorder:
def __init__(self, audio, frames_per_buffer, form, channels, sample_freq):
self.audio = audio
self.device_index = self.find_input_device()
self.frames_per_buffer = frames_per_buffer
self.channels = channels
self.sample_freq = sample_freq
self.format = form

def start(self):
self.stream = self.audio.open(
format=self.format,
channels=self.channels,
rate=self.sample_freq,
input=True,
input_device_index=self.device_index,
frames_per_buffer=self.frames_per_buffer,
)

def stop(self):
self.stream.stop_stream()
self.stream.close()

def find_input_device(self) -> int:
device_index = None
for i in range(self.audio.get_device_count()):
devinfo = self.audio.get_device_info_by_index(i)
print("Device %d: %s" % (i, devinfo["name"]))

for keyword in ["mic", "input"]:
if keyword in devinfo["name"].lower():
print("Found an input: device %d - %s" % (i, devinfo["name"]))
device_index = i
return device_index

if device_index is None:
print("No preferred input found; using default input device.")

return device_index

def __next__(self) -> np.ndarray:
"""REF: https://stackoverflow.com/questions/19629496/get-an-audio-sample-as-float-number-from-pyaudio-stream # noqa"""
import numpy

stream = self.stream.open(
format=FORMAT,
channels=self.channels,
rate=self.sample_freq,
input=True,
frames_per_buffer=self.frames_per_buffer,
)
data = stream.read(self.frames_per_buffer * self.sample_freq)
decoded = numpy.fromstring(data, "Float32")
return decoded


class Player:
def __init__(self, audio, volume, duration, freq, fs, form):
self.audio = audio
self.duration = duration
self.freq = freq
self.fs = fs
self.volume = volume
self.format = form

def start(self):
# for paFloat32 sample values must be in range [-1.0, 1.0]
self.stream = self.audio.open(
format=self.format, channels=1, rate=self.fs, output=True
)

def stop(self):
self.stream.stop_stream()
self.stream.close()

def play_sound(self):
"""REF: https://stackoverflow.com/questions/8299303/generating-sine-wave-sound-in-python"""
# generate samples, note conversion to float32 array
samples = (
np.sin(2 * np.pi * np.arange(self.fs * self.duration) * self.freq / self.fs)
).astype(np.float32)

# play. May repeat with different volume values (if done interactively)
self.stream.write(self.volume * samples)
5 changes: 5 additions & 0 deletions dogbarking/math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import numpy as np


def get_rms(waveform: np.ndarray) -> float:
return np.sqrt(np.mean(np.asarray(waveform) ** 2.0))
Loading

0 comments on commit 70ed2ab

Please sign in to comment.