Merge pull request #33 from pimoroni/noise
authorPhilip Howard <phil@pimoroni.com>
Wed, 5 Feb 2020 14:01:24 +0000 (14:01 +0000)
committerGitHub <noreply@github.com>
Wed, 5 Feb 2020 14:01:24 +0000 (14:01 +0000)
Noise library and examples for basic FFT and frequency binning

examples/noise-amps-at-freqs.py [new file with mode: 0755]
examples/noise-profile.py [new file with mode: 0755]
library/enviroplus/noise.py
library/setup.cfg

diff --git a/examples/noise-amps-at-freqs.py b/examples/noise-amps-at-freqs.py
new file mode 100755 (executable)
index 0000000..8b1ddd5
--- /dev/null
@@ -0,0 +1,44 @@
+import ST7735
+from PIL import Image, ImageDraw
+from enviroplus.noise import Noise
+
+print("""noise-amps-at-freqs.py - Measure amplitude from specific frequency bins
+
+This example retrieves the median amplitude from 3 user-specified frequency ranges and plots them in Blue, Green and Red on the Enviro+ display.
+
+As you play a continuous rising tone on your phone, you should notice peaks that correspond to the frequency entering each range.
+
+Press Ctrl+C to exit!
+
+""")
+
+noise = Noise()
+
+disp = ST7735.ST7735(
+        port=0,
+        cs=ST7735.BG_SPI_CS_FRONT,
+        dc=9,
+        backlight=12,
+        rotation=90)
+
+disp.begin()
+
+img = Image.new('RGB', (disp.width, disp.height), color=(0, 0, 0))
+draw = ImageDraw.Draw(img)
+
+
+while True:
+    amps = noise.get_amplitudes_at_frequency_ranges([
+        (100, 200),
+        (500, 600),
+        (1000, 1200)
+    ])
+    amps = [n * 32 for n in amps]
+    img2 = img.copy()
+    draw.rectangle((0, 0, disp.width, disp.height), (0, 0, 0))
+    img.paste(img2, (1, 0))
+    draw.line((0, 0, 0, amps[0]), fill=(0, 0, 255))
+    draw.line((0, 0, 0, amps[1]), fill=(0, 255, 0))
+    draw.line((0, 0, 0, amps[2]), fill=(255, 0, 0))
+
+    disp.display(img)
diff --git a/examples/noise-profile.py b/examples/noise-profile.py
new file mode 100755 (executable)
index 0000000..1afdff5
--- /dev/null
@@ -0,0 +1,40 @@
+import ST7735
+from PIL import Image, ImageDraw
+from enviroplus.noise import Noise
+
+print("""noise-profile.py - Get a simple noise profile.
+
+This example grabs a basic 3-bin noise profile of low, medium and high frequency noise, plotting the noise characteristics as coloured bars.
+
+Press Ctrl+C to exit!
+
+""")
+
+noise = Noise()
+
+disp = ST7735.ST7735(
+        port=0,
+        cs=ST7735.BG_SPI_CS_FRONT,
+        dc=9,
+        backlight=12,
+        rotation=90)
+
+disp.begin()
+
+img = Image.new('RGB', (disp.width, disp.height), color=(0, 0, 0))
+draw = ImageDraw.Draw(img)
+
+
+while True:
+    low, mid, high, amp = noise.get_noise_profile()
+    low *= 128
+    mid *= 128
+    high *= 128
+    amp *= 64
+
+    img2 = img.copy()
+    draw.rectangle((0, 0, disp.width, disp.height), (0, 0, 0))
+    img.paste(img2, (1, 0))
+    draw.line((0, 0, 0, amp), fill=(int(low), int(mid), int(high)))
+
+    disp.display(img)
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2e7472d1ea8120eec7739efeee989d358d7e728f 100644 (file)
@@ -0,0 +1,90 @@
+import sounddevice
+import numpy
+
+
+class Noise():
+    def __init__(self,
+                 sample_rate=16000,
+                 duration=0.5):
+        """Noise measurement.
+
+        :param sample_rate: Sample rate in Hz
+        :param duraton: Duration, in seconds, of noise sample capture
+
+        """
+
+        self.duration = duration
+        self.sample_rate = sample_rate
+
+    def get_amplitudes_at_frequency_ranges(self, ranges):
+        """Return the mean amplitude of frequencies in the given ranges.
+
+        :param ranges: List of ranges including a start and end range
+
+        """
+        recording = self._record()
+        magnitude = numpy.abs(numpy.fft.rfft(recording[:, 0], n=self.sample_rate))
+        result = []
+        for r in ranges:
+            start, end = r
+            result.append(numpy.mean(magnitude[start:end]))
+        return result
+
+    def get_amplitude_at_frequency_range(self, start, end):
+        """Return the mean amplitude of frequencies in the specified range.
+
+        :param start: Start frequency (in Hz)
+        :param end: End frequency (in Hz)
+
+        """
+        n = self.sample_rate // 2
+        if start > n or end > n:
+            raise ValueError("Maxmimum frequency is {}".format(n))
+
+        recording = self._record()
+        magnitude = numpy.abs(numpy.fft.rfft(recording[:, 0], n=self.sample_rate))
+        return numpy.mean(magnitude[start:end])
+
+    def get_noise_profile(self,
+                          noise_floor=100,
+                          low=0.12,
+                          mid=0.36,
+                          high=None):
+        """Returns a noise charateristic profile.
+
+        Bins all frequencies into 3 weighted groups expressed as a percentage of the total frequency range.
+
+        :param noise_floor: "High-pass" frequency, exclude frequencies below this value
+        :param low: Percentage of frequency ranges to count in the low bin (as a float, 0.5 = 50%)
+        :param mid: Percentage of frequency ranges to count in the mid bin (as a float, 0.5 = 50%)
+        :param high: Optional percentage for high bin, effectively creates a "Low-pass" if total percentage is less than 100%
+
+        """
+
+        if high is None:
+            high = 1.0 - low - mid
+
+        recording = self._record()
+        magnitude = numpy.abs(numpy.fft.rfft(recording[:, 0], n=self.sample_rate))
+
+        sample_count = (self.sample_rate // 2) - noise_floor
+
+        mid_start = noise_floor + int(sample_count * low)
+        high_start = mid_start + int(sample_count * mid)
+        noise_ceiling = high_start + int(sample_count * high)
+
+        amp_low = numpy.mean(magnitude[self.noise_floor:mid_start])
+        amp_mid = numpy.mean(magnitude[mid_start:high_start])
+        amp_high = numpy.mean(magnitude[high_start:noise_ceiling])
+        amp_total = (low + mid + high) / 3.0
+
+        return amp_low, amp_mid, amp_high, amp_total
+
+    def _record(self):
+        return sounddevice.rec(
+            int(self.duration * self.sample_rate),
+            samplerate=self.sample_rate,
+            blocking=True,
+            channels=1,
+            dtype='float64'
+        )
index ed7cef7f7147fd335bac58669cf8127af414212c..c48c7badd908ec278ce053344f1396833130a9fa 100644 (file)
@@ -33,6 +33,7 @@ install_requires =
        ltr559
        st7735
        ads1015
+       sounddevice
 
 [flake8]
 exclude =
@@ -51,13 +52,16 @@ py2deps =
        python-numpy
        python-smbus
        python-pil
+       libportaudio2
 py3deps =
        python3-pip
        python3-numpy
        python3-smbus
        python3-pil
+       libportaudio2
 configtxt =
        dtoverlay=pi3-miniuart-bt
+       dtoverlay=adau7002-simple
 commands =
        printf "Setting up i2c and SPI..\n"
        raspi-config nonint do_spi 0