├── .gitignore ├── LICENSE ├── clap.csd ├── test_clap.py ├── clap.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 iver56 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /clap.csd: -------------------------------------------------------------------------------- 1 | 2 | 3 | -iadc 4 | --nosound 5 | 6 | 7 | 8 | sr = 44100 9 | ksmps = 32 10 | nchnls = 1 11 | 0dbfs = 1 12 | 13 | pyinit 14 | 15 | instr 1 16 | 17 | pyruni {{ 18 | import os, sys 19 | sys.path.append(os.getcwd()) 20 | 21 | from clap import ClapAnalyzer 22 | clap_analyzer = ClapAnalyzer(note_lengths=[0.25, 0.125, 0.125, 0.25, 0.25]) 23 | 24 | def clap_detected(): 25 | print 'Clap detected' 26 | 27 | def clap_sequence_detected(): 28 | print 'Matching clap sequence detected!' 29 | 30 | clap_analyzer.on_clap(clap_detected) 31 | clap_analyzer.on_clap_sequence(clap_sequence_detected) 32 | }} 33 | 34 | kLastRms init 0 35 | kLastAttack init 0 36 | iRmsDiffThreshold init .1 37 | 38 | kTime times 39 | 40 | aIn in 41 | 42 | kRmsOrig rms aIn 43 | 44 | kSmoothingFreq linseg 5, 1, 0.01 ;quicker smoothing to start with 45 | kSmoothRms tonek kRmsOrig, kSmoothingFreq 46 | kSmoothRms max kSmoothRms, 0.001 47 | 48 | aNorm = 0.1 * aIn / a(kSmoothRms) 49 | 50 | kRms rms aNorm 51 | kRmsDiff = kRms - kLastRms 52 | 53 | if (kRmsDiff > iRmsDiffThreshold && kTime - kLastAttack > 0.09) then 54 | kLastAttack times 55 | pycall "clap_analyzer.clap", kLastAttack 56 | endif 57 | 58 | out aNorm 59 | kLastRms = kRms 60 | 61 | endin 62 | 63 | 64 | 65 | i 1 0 500 66 | e 67 | 68 | 69 | -------------------------------------------------------------------------------- /test_clap.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from clap import ClapAnalyzer 3 | 4 | 5 | class TestClapAnalyzer(unittest.TestCase): 6 | def setUp(self): 7 | self.clap_analyzer = ClapAnalyzer( 8 | note_lengths=[1./4, 1./8, 1./8, 1./4, 1./4], 9 | deviation_threshold=0.1 10 | ) 11 | self.clap_analyzer.on_clap_sequence(self.clap_sequence_callback) 12 | self.num_clap_sequences_detected = 0 13 | 14 | def clap_sequence_callback(self): 15 | self.num_clap_sequences_detected += 1 16 | return None 17 | 18 | def test_scenario1(self): 19 | self.clap_analyzer.clap(0.414) 20 | self.clap_analyzer.clap(0.660) 21 | self.clap_analyzer.clap(0.796) 22 | self.clap_analyzer.clap(0.905) 23 | self.assertEquals(self.num_clap_sequences_detected, 0) 24 | self.clap_analyzer.clap(1.155) 25 | self.assertEquals(self.num_clap_sequences_detected, 1) 26 | 27 | def test_scenario2(self): 28 | self.clap_analyzer.clap(0.617) 29 | self.clap_analyzer.clap(0.984) 30 | self.clap_analyzer.clap(1.163) 31 | self.clap_analyzer.clap(1.355) 32 | self.assertEquals(self.num_clap_sequences_detected, 0) 33 | self.clap_analyzer.clap(1.724) 34 | self.assertEquals(self.num_clap_sequences_detected, 1) 35 | 36 | self.clap_analyzer.clap(2.899) 37 | self.clap_analyzer.clap(3.224) 38 | self.clap_analyzer.clap(3.416) 39 | self.clap_analyzer.clap(3.608) 40 | self.assertEquals(self.num_clap_sequences_detected, 1) 41 | self.clap_analyzer.clap(3.967) 42 | self.assertEquals(self.num_clap_sequences_detected, 2) 43 | 44 | self.clap_analyzer.clap(4.645) 45 | self.clap_analyzer.clap(5.016) 46 | 47 | def test_scenario3(self): 48 | self.clap_analyzer.clap(0.0101) 49 | 50 | self.clap_analyzer.clap(0.689) 51 | self.clap_analyzer.clap(0.984) 52 | self.clap_analyzer.clap(1.094) 53 | self.clap_analyzer.clap(1.458) 54 | self.clap_analyzer.clap(1.663) 55 | self.clap_analyzer.clap(1.880) 56 | 57 | self.clap_analyzer.clap(4.307) 58 | self.clap_analyzer.clap(4.499) 59 | self.clap_analyzer.clap(4.494) 60 | self.clap_analyzer.clap(4.907) 61 | self.clap_analyzer.clap(4.907) 62 | self.clap_analyzer.clap(5.114) 63 | self.clap_analyzer.clap(5.511) 64 | 65 | self.assertEquals(self.num_clap_sequences_detected, 0) 66 | 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /clap.py: -------------------------------------------------------------------------------- 1 | class ClapAnalyzer: 2 | def __init__(self, note_lengths, deviation_threshold=0.1): 3 | """ 4 | :param note_lengths: Relative note lengths in the rhythmic pattern. F.ex. [2, 1, 1, 2, 2] 5 | :param deviation_threshold: How much deviation from the pattern should be considered failure 6 | :return: 7 | """ 8 | self.buffer_size = len(note_lengths) 9 | self.pattern = self.note_lengths_to_normalized_pauses(note_lengths) 10 | self.pattern_sum = sum(self.pattern) 11 | self.min_pattern_time = .1 * self.pattern_sum # min 100 ms between fastest clap in sequence 12 | self.max_pattern_time = .5 * self.pattern_sum # max 500 ms between fastest clap in sequence 13 | self.clap_times = [None] * self.buffer_size 14 | self.deviation_threshold = deviation_threshold 15 | self.current_index = 0 16 | self.clap_listeners = set() 17 | self.clap_sequence_listeners = set() 18 | 19 | @staticmethod 20 | def note_lengths_to_normalized_pauses(note_lengths): 21 | note_lengths.pop() # Because the length of the last note doesn't matter 22 | min_note_length = float(min(note_lengths)) 23 | return map(lambda x: x / min_note_length, note_lengths) 24 | 25 | def on_clap(self, fn): 26 | self.clap_listeners.add(fn) 27 | 28 | def on_clap_sequence(self, fn): 29 | self.clap_sequence_listeners.add(fn) 30 | 31 | def clap(self, time): 32 | """ 33 | Tell ClapAnalyzer that a clap has been detected at the specified time 34 | :param time: Absolute time in seconds. Must be float. 35 | :return: 36 | """ 37 | for fn in self.clap_listeners: 38 | fn() 39 | 40 | self.current_index = (self.current_index + 1) % self.buffer_size 41 | self.clap_times[self.current_index] = time 42 | 43 | first_clap_in_sequence = self.clap_times[self.current_index - self.buffer_size + 1] 44 | if first_clap_in_sequence is None: 45 | return # waiting for more claps 46 | 47 | time_diff = time - first_clap_in_sequence 48 | avg_time_per_clap_unit = time_diff / self.pattern_sum 49 | if self.min_pattern_time <= time_diff <= self.max_pattern_time: 50 | total_deviation = 0 51 | j = 0 52 | for i in range(self.current_index - self.buffer_size + 1, self.current_index): 53 | clap_time_diff = self.clap_times[i + 1] - self.clap_times[i] 54 | relative_clap_time_diff = clap_time_diff / avg_time_per_clap_unit 55 | total_deviation += (relative_clap_time_diff - self.pattern[j]) ** 2 56 | j += 1 57 | 58 | if total_deviation < self.deviation_threshold: 59 | for fn in self.clap_sequence_listeners: 60 | fn() 61 | return # clap sequence detected! 62 | else: 63 | return # clap sequence didn't match accurately enough with the pattern 64 | else: 65 | return # clap sequence too short or too long 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clap-detection 2 | Clap sequence/rhythm/pattern detection on Raspberry Pi using Csound and Python. This was made as a quick experiment. Don't use it for anything serious. 3 | 4 | ## Why 5 | 6 | Some examples of things you can have your Raspberry Pi do when a matching clap sequence is detected: 7 | 8 | * Dim the lights and start playing smooth jazz music 9 | * Turn on/off the TV 10 | * Broadcast a yo 11 | * Project the weather forecast on the wall 12 | 13 | ## How 14 | 15 | Csound takes in audio from a microphone live and checks the audio for transients. Whenever a transient (rapidly ascending amplitude) is detected, Csound will notice ClapAnalyzer, a class implemented in Python. ClapAnalyzer looks for a specific rhytmic clap sequence. ClapAnalyzer will notice all listeners whenever a matching clap sequence is detected. 16 | 17 | ## Setup 18 | 19 | * Install Python 2.7 20 | * Install Csound 21 | 22 | ## Usage 23 | 24 | ### Python 25 | 26 | Let's use the following rhythmic sequence as example: 27 | 28 | ![clap](https://cloud.githubusercontent.com/assets/1470603/9700905/a6de8d6a-5415-11e5-81f6-f81e4034a939.png) 29 | 30 | The note lengths are 1/4, 1/8, 1/8, 1/4, 1/4, so we set the `note_lengths` parameter to [0.25, 0.125, 0.125, 0.25, 0.25]. 31 | 32 | ```python 33 | from clap import ClapAnalyzer 34 | 35 | clap_analyzer = ClapAnalyzer(note_lengths=[0.25, 0.125, 0.125, 0.25, 0.25]) 36 | 37 | def clap_sequence_detected(): 38 | print 'Matching clap sequence detected!' 39 | 40 | clap_analyzer.on_clap_sequence(clap_sequence_detected) 41 | 42 | # You can now start calling clap_analyzer.clap(time) 43 | ``` 44 | 45 | Basically, this is the python code that is used in `clap.csd` 46 | 47 | ### Csound 48 | 49 | Start csound from your command line. By default, the csound instrument will get live audio input: 50 | 51 | `csound clap.csd` 52 | 53 | If you want to quickly analyze a wav file, you can use that file instead of live audio input. This is good for testing: 54 | 55 | `csound clap.csd -i myfile.wav` 56 | 57 | PS: The file must be mono, not stereo, for this to work. And if your sound file is long, then you should modify the amount of time the Csound instrument stays alive accordingly, in order to analyze the whole file. 58 | 59 | ## Troubleshooting 60 | 61 | ### "no module named clap" 62 | 63 | Try adding the directory with the python module dynamically: 64 | 65 | ``` 66 | pyruni "import sys, os" 67 | pyruni "sys.path.append('/path/to/clap-detection')" 68 | ``` 69 | 70 | Edit `/path/to/clap-detection` to the place where clap.py is located. 71 | 72 | ### "Segmentation fault" or "Unable to set number of channels on soundcard" 73 | 74 | Check if your input device is mono or stereo. If it is mono (i.e. has only one channel), then you should set `nchnls = 1` in your csound file, and you should use the `in` opcode instead of `ins`. If your input device is stereo, then you should set `nchnls = 2`. 75 | 76 | ### ALSA and/or PortAudio warnings 77 | 78 | Use the `-+rtaudio=alsa` option 79 | 80 | ### Stuttering/crackling/noise/"Buffer underrun" 81 | 82 | Let Csound use a large buffer in both software and hardware. In other word, use the following options: `-b2048 -B2048` 83 | 84 | ### Input device error 85 | 86 | Run `arecord -l` and check the list of sound cards and subdevices that are available. If you, for example, want to use card 1, subdevice 0, then you should use the following csound option: `-i adc:hw:1,0` 87 | --------------------------------------------------------------------------------