Skip to content

Commit

Permalink
Adding Tone and RTTTL command support
Browse files Browse the repository at this point in the history
  • Loading branch information
enesbcs committed Jun 30, 2019
1 parent ea3580b commit 7f1ab25
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 3 deletions.
2 changes: 1 addition & 1 deletion _P001_Switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def plugin_write(self,cmd):
res = False
cmdarr = cmd.split(",")
cmdarr[0] = cmdarr[0].strip().lower()
if cmdarr[0].strip().lower() in ["gpio","pwm","pulse","longpulse"]:
if cmdarr[0].strip().lower() in ["gpio","pwm","pulse","longpulse","tone","rtttl"]:
res = gpiohelper.gpio_commands(cmd)
return res

Expand Down
2 changes: 1 addition & 1 deletion _P029_DomoOutput.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,6 @@ def plugin_write(self,cmd):
res = False
cmdarr = cmd.split(",")
cmdarr[0] = cmdarr[0].strip().lower()
if cmdarr[0].strip().lower() in ["gpio","pwm","pulse","longpulse"]:
if cmdarr[0].strip().lower() in ["gpio","pwm","pulse","longpulse","tone","rtttl"]:
res = gpiohelper.gpio_commands(cmd)
return res
69 changes: 69 additions & 0 deletions lib/lib_gpiohelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import misc
import gpios
import commands
import lib.lib_rtttl as rtttllib
import threading

def syncvalue(bcmpin,value):
for x in range(0,len(Settings.Tasks)):
Expand Down Expand Up @@ -137,4 +139,71 @@ def gpio_commands(cmd):
rarr = [pin,(1-val)]
rpieTime.addsystemtimer(dur,timercb,rarr)
res = True
elif cmdarr[0]=="tone":
pin = -1
freq = -1
dur = 0
try:
pin = int(cmdarr[1].strip())
freq = int(cmdarr[2].strip())
dur = int(cmdarr[3].strip())
except:
pin = -1
freq = -1
dur = 0
if pin>-1 and freq>-1 and dur>0:
suc = False
try:
suc = True
misc.addLog(rpieGlobals.LOG_LEVEL_DEBUG,"BCM"+str(pin)+" "+str(freq)+"Hz")
play_tone(pin,freq,dur)
gpios.HWPorts.output_pwm(pin,0,0) # stop sound
except Exception as e:
misc.addLog(rpieGlobals.LOG_LEVEL_ERROR,"BCM"+str(pin)+" Tone "+str(e))
suc = False
res = True

elif cmdarr[0]=="rtttl":
cmdarr = cmd.replace(":",",").split(",")
pin = -1
try:
pin = int(cmdarr[1].strip())
except:
pin = -1
if pin>-1:
suc = False
try:
sp = cmd.find(":")
if sp > -1:
# play_rtttl(pin,"t"+cmd[sp:])
rtproc = threading.Thread(target=play_rtttl, args=(pin,"t"+cmd[sp:])) # play in background - no blocking
rtproc.daemon = True
rtproc.start()
suc = True
except Exception as e:
misc.addLog(rpieGlobals.LOG_LEVEL_ERROR,str(e))
suc = False
res = True

return res

def play_tone(pin,freq,delay):
gpios.HWPorts.output_pwm(pin,50,freq) # generate 'freq' sound
s = float(delay/1000)
time.sleep(s)

def play_rtttl(pin,notestr):
# print("DEBUG ",notestr)
notes = []
try:
notes = rtttllib.parse_rtttl(notestr)
except Exception as e:
misc.addLog(rpieGlobals.LOG_LEVEL_ERROR,"RTTTL parse failed: "+str(e))
return false
if 'notes' in notes:
for note in notes['notes']:
try:
play_tone(pin,int(note['frequency']),float(note['duration']))
except:
pass
gpios.HWPorts.output_pwm(pin,0,0) # stop sound
144 changes: 144 additions & 0 deletions lib/lib_rtttl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Based on:
# https://github.com/asdwsda/rtttl
# GNU GENERAL PUBLIC LICENSE
# Version 3, 29 June 2007

import re

def remove_whitespaces(string):
return ''.join(string.split())

def parse_rtttl(rtttl_str, strict_note_syntax=False):
"""Parse RTTTL ringtone
Args:
rtttl_str (str): RTTTL string to parse
strict_note_syntax (bool): Some RTTTL generator doesn't strictly follow
the RTTTL specification and generates a form like
<note> := [<duration>] <note> [<dot>] [<octave>] <delimiter>
instead of
<note> := [<duration>] <note> [<octave>] [<dot>] <delimiter>
parse_rtttl accepts both forms by default, but it can be set to
accept valid rtttl notes only.
Returns:
A dict containing the ringtone title and notes as frequency, duration pairs.
For example:
{'notes': [{'duration': 952.381 , 'frequency': 1396.8},
{'duration': 178.571, 'frequency': 1661.2},
{'duration': 1904.762, 'frequency': 0},
{'duration': 952.381, 'frequency': 1975.6}],
'title': 'Example ringtone'}
"""
rt = rtttl_str.split("=")
pp = -1
try:
pp = rt[3].find(":")
except:
return {'title':False,'notes':False}
if pp==-1:
rt[3] = rt[3].replace(",",":",1)
rtttl_str = rt[0]+"="+rt[1]+"="+rt[2]+"="+rt[3]
rtttl_parts = rtttl_str.split(':')
if len(rtttl_parts) != 3:
return {'title':False,'notes':False}
if len(rtttl_parts[2]) == 0:
return {'title':False,'notes':False}
defaults = parse_defaults(remove_whitespaces(rtttl_parts[1]))
parsed_notes = parse_data(remove_whitespaces(rtttl_parts[2]).lower(), strict_note_syntax)
converted_notes = [convert_note(note, defaults) for note in parsed_notes]
return {'title': rtttl_parts[0], 'notes': converted_notes}

def parse_defaults(defaults_str):
if defaults_str == '':
return {'duration': 4, 'octave': 6, 'bpm': 63}
try:
if re.match(r'^(d=\d{1,2},o=\d,b=\d{1,3})?$', defaults_str):
defaults = dict([d.split('=') for d in defaults_str.split(',')])
parsed_defaults = {
'duration': parse_duration(defaults['d']),
'octave': parse_octave(defaults['o']),
'bpm': parse_bpm(defaults['b'])
}
else:
return {'duration': 4, 'octave': 6, 'bpm': 63}
except InvalidElementError as element:
return {'duration': 4, 'octave': 6, 'bpm': 63}
return parsed_defaults

def parse_data(notes, strict_note_syntax):
raw_notes = notes.split(',')
if not strict_note_syntax:
raw_notes = [correct_note_syntax(note) for note in raw_notes]
return [parse_note(note) for note in raw_notes]

def correct_note_syntax(note):
return re.sub(r'^(\d{0,2})([pbeh]|[cdfga]#?)(\.?)(\d*)$', r'\1\2\4\3', note)

def parse_note(note_str):
try:
elements = re.findall(r'^(\d{0,2})([pbeh]|[cdfga]#?)(\d?)(\.?)$', note_str)[0]
funcs = (parse_duration, parse_pitch, parse_octave, has_dot)
elements = [func(element) for func, element in zip(funcs, elements)]
except:
return False
keys = ('duration', 'pitch', 'octave', 'dot')
return dict(zip(keys, elements))

def parse_duration(duration):
allowed_duration = [1, 2, 4, 8, 16, 32]
return parse_int(duration, allowed_duration)

def parse_octave(octave):
allowed_octave = [4, 5, 6, 7]
return parse_int(octave, allowed_octave)

def parse_bpm(bpm):
allowed_bpm = [
25, 28, 31, 35, 40, 45, 50, 56, 63, 70, 80, 90,
100, 112, 125, 140, 160, 180, 200, 225, 250, 285,
320, 355, 400, 450, 500, 565, 635, 715, 800, 900]
return parse_int(bpm, allowed_bpm)

def parse_pitch(pitch):
allowed_pitch = ['p', 'c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'h', 'b']
return parse_element(pitch, allowed_pitch)

def parse_int(element, allowed):
if element:
return parse_element(int(element), allowed)
return None

def parse_element(element, allowed):
if element in allowed:
return element
else:
return False

def has_dot(dot):
return dot == '.'

def convert_note(note, defaults):
octave_multiplier = {4: 1, 5: 2, 6: 4, 7: 8}
pitch_frequencies = {
'p': 0,
'c': 261.6,
'c#': 277.2,
'd': 293.7,
'd#': 311.1,
'e': 329.6,
'f': 349.2,
'f#': 370.0,
'g': 392.0,
'g#': 415.3,
'a': 440.0,
'a#': 466.2,
'b': 493.9,
'h': 493.9
}
msec_per_beat = (60.0 / defaults['bpm']) * 4 * 1000
frequency = pitch_frequencies[note['pitch']] * octave_multiplier[note['octave'] or defaults['octave']]
if note['dot']:
multiplier = 1.5
else:
multiplier = 1
duration = round((msec_per_beat / (note['duration'] or defaults['duration'])) * multiplier, 3)
return {'frequency': frequency, 'duration': duration}
2 changes: 1 addition & 1 deletion rpieGlobals.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Copyright (C) 2018-2019 by Alexander Nagy - https://bitekmindenhol.blog.hu/
#
PROGNAME = "RPIEasy"
BUILD = 19179
BUILD = 19181
PROGVER = "0."+str(BUILD/1000)

gpMenu = []
Expand Down

0 comments on commit 7f1ab25

Please sign in to comment.