From a664d67c7ca1113835aab018bc8bbc845ebfdf80 Mon Sep 17 00:00:00 2001 From: Martin Aronsen Date: Fri, 5 Sep 2025 15:15:02 +0200 Subject: [PATCH 1/6] Implementert feiringer --- oving_3/main.py | 29 ++++++++++++++++++-- oving_3/rtttl_player.py | 60 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 oving_3/rtttl_player.py diff --git a/oving_3/main.py b/oving_3/main.py index 9e9700f..e4b38bf 100644 --- a/oving_3/main.py +++ b/oving_3/main.py @@ -7,6 +7,10 @@ from pybricks.robotics import DriveBase from pybricks.media.ev3dev import SoundFile, ImageFile +from rtttl_player import RTTTLPlayer +from random import randint + + # This program requires LEGO EV3 MicroPython v2.0 or higher. # Click "Open user guide" on the EV3 extension tab for more information. @@ -15,6 +19,27 @@ # Create your objects here. ev3 = EV3Brick() +# Eksempelmelodi (Nokia) +#take_on_me = "Take On Me:o=5,d=8,b=160,b=160:f#,f#,f#,d,p,b4,p,e,p,e,p,e,g#,g#,a,b,a,a,a,e,p,d,p,f#,p,f#,p,f#,e,e,f#,e,f#,f#,f#,d,p,b4,p,e,p,e,p,e,g#,g#,a,b,a,a,a,e,p,d,p,f#,p,f#,p,f#,e,e5" +#postman_pat ="Postman Pat:o=5,d=16,b=100,b=100:f#,p,a,p,8b,8p,f#,p,a,p,8b,8p,f#,p,a,p,b,p,d6,d6,c#6,c#6,a,p,4b.,8p,32f#,g,p,a,p,b,p,g,p,8f#.,8e,8p,32f#,g,p,a,p,32b.,32b.,g,p,8f#.,8e,8p,32f#,g,p,a,p,b,p,g,p,f#,p,e,p,d,p,c#,p,2d" +#simpsons = "Simpsons:o=5,d=8,b=160,b=160:c6.,4e6,4f#6,a6,4g6.,4e6,4c6,a,f#,f#,f#,2g,p,p,f#,f#,f#,g,4a#.,c6,c6,c6,4c6" +#mission_impossible = "Mission Impossible:o=5,d=16,b=100,b=100:32d,32d#,32d,32d#,32d,32d#,32d,32d#,32d,32d,32d#,32e,32f,32f#,32g,g,8p,g,8p,a#,p,c6,p,g,8p,g,8p,f,p,f#,p,g,8p,g,8p,a#,p,c6,p,g,8p,g,8p,f,p,f#,p,a#,g,2d,32p,a#,g,2c#,32p,a#,g,2c,p,a#4,c" + +songs = [ + "Take On Me:o=5,d=8,b=160,b=160:f#,f#,f#,d,p,b4,p,e,p,e,p,e,g#,g#,a,b,a,a,a,e,p,d,p,f#,p,f#,p,f#,e,e,f#,e,f#,f#,f#,d,p,b4,p,e,p,e,p,e,g#,g#,a,b,a,a,a,e,p,d,p,f#,p,f#,p,f#,e,e5", + "Postman Pat:o=5,d=16,b=100,b=100:f#,p,a,p,8b,8p,f#,p,a,p,8b,8p,f#,p,a,p,b,p,d6,d6,c#6,c#6,a,p,4b.,8p,32f#,g,p,a,p,b,p,g,p,8f#.,8e,8p,32f#,g,p,a,p,32b.,32b.,g,p,8f#.,8e,8p,32f#,g,p,a,p,b,p,g,p,f#,p,e,p,d,p,c#,p,2d", + "Simpsons:o=5,d=8,b=160,b=160:c6.,4e6,4f#6,a6,4g6.,4e6,4c6,a,f#,f#,f#,2g,p,p,f#,f#,f#,g,4a#.,c6,c6,c6,4c6", + "Mission Impossible:o=5,d=16,b=100,b=100:32d,32d#,32d,32d#,32d,32d#,32d,32d#,32d,32d,32d#,32e,32f,32f#,32g,g,8p,g,8p,a#,p,c6,p,g,8p,g,8p,f,p,f#,p,g,8p,g,8p,a#,p,c6,p,g,8p,g,8p,f,p,f#,p,a#,g,2d,32p,a#,g,2c#,32p,a#,g,2c,p,a#4,c" +] + +# Lag spiller og kjør +player = RTTTLPlayer(ev3, songs[3]) +#ev3.screen.print("Spiller", player.name, "...") +#player.play() + +leftMotor = Motor(port=Port.A) +rightMotor = Motor(port=Port.B) + +robotDriveBase = DriveBase(leftMotor, rightMotor, wheel_diameter=56, axle_track=110) -# Write your program here. -ev3.speaker.beep() +robotDriveBase.drive(1000, 0) \ No newline at end of file diff --git a/oving_3/rtttl_player.py b/oving_3/rtttl_player.py new file mode 100644 index 0000000..8ca0e4f --- /dev/null +++ b/oving_3/rtttl_player.py @@ -0,0 +1,60 @@ +import re + + +class RTTTLPlayer: + NOTE_FREQS = { + 'c': 16.35, 'c#': 17.32, 'd': 18.35, 'd#': 19.45, + 'e': 20.60, 'f': 21.83, 'f#': 23.12, 'g': 24.50, + 'g#': 25.96, 'a': 27.50, 'a#': 29.14, 'b': 30.87 + } + + def __init__(self, ev3, rtttl: str): + """ + :param ev3: EV3Brick instance from pybricks.hubs + :param rtttl: RTTTL string (name:d=4,o=5,b=100:notes) + """ + self.ev3 = ev3 + self.name, self.defaults, self.notes = self.parse_rtttl(rtttl) + + def parse_rtttl(self, rtttl: str): + name, settings, notes = rtttl.split(':') + defaults = {} + for s in settings.split(','): + k, v = s.split('=') + defaults[k] = int(v) + notes = notes.split(',') + return name, defaults, notes + + def note_to_freq(self, note: str, octave: int): + """ + Convert note name + octave to frequency in Hz. + """ + base = self.NOTE_FREQS[note] + return round(base * (2 ** octave), 2) + + def play(self, tempo_scale: float = 1.0): + """ + Play the parsed RTTTL melody. + :param tempo_scale: Scale factor for speed (1.0 = normal, 2.0 = twice as fast, 0.5 = half speed) + """ + bpm = self.defaults.get('b', 63) + whole_note = (60 / bpm) * 4 * 1000 # ms + + for raw in self.notes: + match = re.match(r'(\d+)?([a-gp][#]?)(\d)?(\.*)?', raw.strip(), re.I) + if not match: + continue + + dur, note, octave, dot = match.groups() + dur = int(dur) if dur else self.defaults.get('d', 4) + octave = int(octave) if octave else self.defaults.get('o', 6) + + duration = (whole_note / dur) * tempo_scale + if dot: + duration *= 1.5 + + if note.lower() == 'p': # pause + self.ev3.speaker.beep(0, int(duration)) + else: + freq = self.note_to_freq(note.lower(), octave) + self.ev3.speaker.beep(int(freq), int(duration)) \ No newline at end of file From bb5f06ffef86379060ff5a6e515378bc343dc31e Mon Sep 17 00:00:00 2001 From: Odin Grav Date: Wed, 17 Sep 2025 09:11:06 +0200 Subject: [PATCH 2/6] =?UTF-8?q?started=20=C3=A5=20f=C3=A5=20robotten=20til?= =?UTF-8?q?=20=C3=A5=20f=C3=B8lge=20en=20linje?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oving_3/main.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/oving_3/main.py b/oving_3/main.py index e4b38bf..937fbff 100644 --- a/oving_3/main.py +++ b/oving_3/main.py @@ -39,7 +39,19 @@ leftMotor = Motor(port=Port.A) rightMotor = Motor(port=Port.B) - robotDriveBase = DriveBase(leftMotor, rightMotor, wheel_diameter=56, axle_track=110) -robotDriveBase.drive(1000, 0) \ No newline at end of file +startBtn = TouchSensor(port=Port.S1) +usSensor = UltrasonicSensor(port=Port.S3) + +leftColorSensor = ColorSensor(port=Port.S2) +rightColorSensor = ColorSensor(port=Port.S4) + +while True: + if leftColorSensor.color() == Color.BLACK: + robotDriveBase.turn(5) + + elif rightColorSensor.color() == Color.BLACK: + robotDriveBase.turn(-5) + + DriveBase.run(100) \ No newline at end of file From 88998503d1133e6803da670263c1f012100a2ee0 Mon Sep 17 00:00:00 2001 From: Odin Grav Date: Wed, 17 Sep 2025 10:28:16 +0200 Subject: [PATCH 3/6] progress --- oving_3/main.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/oving_3/main.py b/oving_3/main.py index 937fbff..e7c4963 100644 --- a/oving_3/main.py +++ b/oving_3/main.py @@ -9,6 +9,7 @@ from rtttl_player import RTTTLPlayer from random import randint +import time @@ -38,7 +39,7 @@ #player.play() leftMotor = Motor(port=Port.A) -rightMotor = Motor(port=Port.B) +rightMotor = Motor(port=Port.C) robotDriveBase = DriveBase(leftMotor, rightMotor, wheel_diameter=56, axle_track=110) startBtn = TouchSensor(port=Port.S1) @@ -47,11 +48,28 @@ leftColorSensor = ColorSensor(port=Port.S2) rightColorSensor = ColorSensor(port=Port.S4) +BLACK = 9 +WHITE = 85 +threshold = (BLACK + WHITE) / 2 + +lastCorrectino = time.time() +lastDirection = 1 + while True: - if leftColorSensor.color() == Color.BLACK: - robotDriveBase.turn(5) + if time.time() - lastCorrectino > 1: + while leftColorSensor.reflection() > 10 or rightColorSensor.reflection() < 10: + robotDriveBase.drive(-100, 50 * lastDirection) - elif rightColorSensor.color() == Color.BLACK: - robotDriveBase.turn(-5) + if leftColorSensor.reflection() < 10: + robotDriveBase.drive(-100, 50) + lastCorrectino == time.time() + lastDirection = 1 + continue - DriveBase.run(100) \ No newline at end of file + elif rightColorSensor.reflection() < 10: + robotDriveBase.drive(-100, -50) + lastCorrectino = time.time() + lastDirection = -1 + continue + + robotDriveBase.drive(-100, 50) \ No newline at end of file From bdfe43ec00a3d8b0ba948f62f2c592f22bdd04af Mon Sep 17 00:00:00 2001 From: Odin Grav Date: Wed, 17 Sep 2025 12:56:32 +0200 Subject: [PATCH 4/6] =?UTF-8?q?roboten=20f=C3=B8lger=20linjen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oving_3/main.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/oving_3/main.py b/oving_3/main.py index e7c4963..b7189b5 100644 --- a/oving_3/main.py +++ b/oving_3/main.py @@ -34,13 +34,13 @@ ] # Lag spiller og kjør -player = RTTTLPlayer(ev3, songs[3]) +#player = RTTTLPlayer(ev3, songs[3]) #ev3.screen.print("Spiller", player.name, "...") #player.play() leftMotor = Motor(port=Port.A) rightMotor = Motor(port=Port.C) -robotDriveBase = DriveBase(leftMotor, rightMotor, wheel_diameter=56, axle_track=110) +robotDriveBase = DriveBase(leftMotor, rightMotor, wheel_diameter=56, axle_track=160) startBtn = TouchSensor(port=Port.S1) usSensor = UltrasonicSensor(port=Port.S3) @@ -48,27 +48,34 @@ leftColorSensor = ColorSensor(port=Port.S2) rightColorSensor = ColorSensor(port=Port.S4) -BLACK = 9 -WHITE = 85 -threshold = (BLACK + WHITE) / 2 +speaker = ev3.speaker -lastCorrectino = time.time() +leftSensorThreshold = 9 +rigthSensorThreshold = 9 + +currentTime = time.time() +lastCorrectino = currentTime lastDirection = 1 while True: - if time.time() - lastCorrectino > 1: + currentTime = time.time() + + if currentTime - lastCorrectino > 2: while leftColorSensor.reflection() > 10 or rightColorSensor.reflection() < 10: robotDriveBase.drive(-100, 50 * lastDirection) + + lastCorrectino = time.time() + continue - if leftColorSensor.reflection() < 10: - robotDriveBase.drive(-100, 50) - lastCorrectino == time.time() + if leftColorSensor.reflection() < leftSensorThreshold: + robotDriveBase.drive(-100, 25) + lastCorrectino == currentTime lastDirection = 1 continue - elif rightColorSensor.reflection() < 10: - robotDriveBase.drive(-100, -50) - lastCorrectino = time.time() + if rightColorSensor.reflection() < rigthSensorThreshold: + robotDriveBase.drive(-100, -25) + lastCorrectino = currentTime lastDirection = -1 continue From 86e1f13bcdf7a0267e81575b5eb057b7499fc234 Mon Sep 17 00:00:00 2001 From: Odin Grav Date: Wed, 24 Sep 2025 11:15:22 +0200 Subject: [PATCH 5/6] la til triks --- oving_3/main.py | 51 +++++++++++++++++++++++++++++++---------- oving_3/rtttl_player.py | 9 ++++---- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/oving_3/main.py b/oving_3/main.py index b7189b5..5e8bb24 100644 --- a/oving_3/main.py +++ b/oving_3/main.py @@ -38,6 +38,27 @@ #ev3.screen.print("Spiller", player.name, "...") #player.play() +def doRandomTrick(): + trickIndex = randint(0, 1) + + if trickIndex == 1: + spin() + elif trickindex == 2: + playMusic() + +def spin(): + directions = [-1, 1] + directionIndex = randint(0, 1) + robotDriveBase.stop() + speaker.say("Look at my trick") + robotDriveBase.turn(360 * directions[directionIndex]) + +def playMusic(): + robotDriveBase.stop() + player = RTTTLPlayer(ev3, songs[3]) + ev3.screen.print("Spiller", player.name, "...") + player.play() + leftMotor = Motor(port=Port.A) rightMotor = Motor(port=Port.C) robotDriveBase = DriveBase(leftMotor, rightMotor, wheel_diameter=56, axle_track=160) @@ -50,33 +71,39 @@ speaker = ev3.speaker -leftSensorThreshold = 9 -rigthSensorThreshold = 9 +leftSensorThreshold = 8 +rigthSensorThreshold = 7 +speed = 50 currentTime = time.time() lastCorrectino = currentTime lastDirection = 1 +lastTrick = currentTime while True: currentTime = time.time() - if currentTime - lastCorrectino > 2: - while leftColorSensor.reflection() > 10 or rightColorSensor.reflection() < 10: - robotDriveBase.drive(-100, 50 * lastDirection) + """ if currentTime - lastCorrectino > 2: + while leftColorSensor.reflection() > leftSensorThreshold or rightColorSensor.reflection() < rigthSensorThreshold: + robotDriveBase.drive(-speed, 50 * lastDirection) lastCorrectino = time.time() - continue + continue """ + + if currentTime - lastTrick > 10: + doTrick() + lastTrick = time.time() if leftColorSensor.reflection() < leftSensorThreshold: - robotDriveBase.drive(-100, 25) - lastCorrectino == currentTime - lastDirection = 1 + robotDriveBase.drive(-speed, 25) + lastCorrectino == time.time() + lastDirection = -1 continue if rightColorSensor.reflection() < rigthSensorThreshold: - robotDriveBase.drive(-100, -25) + robotDriveBase.drive(-speed, -25) lastCorrectino = currentTime - lastDirection = -1 + lastDirection = 1 continue - robotDriveBase.drive(-100, 50) \ No newline at end of file + robotDriveBase.drive(-speed, 50) \ No newline at end of file diff --git a/oving_3/rtttl_player.py b/oving_3/rtttl_player.py index 8ca0e4f..5551073 100644 --- a/oving_3/rtttl_player.py +++ b/oving_3/rtttl_player.py @@ -8,13 +8,12 @@ class RTTTLPlayer: 'g#': 25.96, 'a': 27.50, 'a#': 29.14, 'b': 30.87 } - def __init__(self, ev3, rtttl: str): + def __init__(self, ev3): """ :param ev3: EV3Brick instance from pybricks.hubs :param rtttl: RTTTL string (name:d=4,o=5,b=100:notes) """ self.ev3 = ev3 - self.name, self.defaults, self.notes = self.parse_rtttl(rtttl) def parse_rtttl(self, rtttl: str): name, settings, notes = rtttl.split(':') @@ -32,15 +31,15 @@ def note_to_freq(self, note: str, octave: int): base = self.NOTE_FREQS[note] return round(base * (2 ** octave), 2) - def play(self, tempo_scale: float = 1.0): + def play(self, name, defaults, notes, tempo_scale: float = 1.0): """ Play the parsed RTTTL melody. :param tempo_scale: Scale factor for speed (1.0 = normal, 2.0 = twice as fast, 0.5 = half speed) """ - bpm = self.defaults.get('b', 63) + bpm = defaults.get('b', 63) whole_note = (60 / bpm) * 4 * 1000 # ms - for raw in self.notes: + for raw in notes: match = re.match(r'(\d+)?([a-gp][#]?)(\d)?(\.*)?', raw.strip(), re.I) if not match: continue From 405a9d883c72d4b4d0c493c1bfa94b1014183d04 Mon Sep 17 00:00:00 2001 From: Martin Aronsen Date: Tue, 30 Sep 2025 14:58:29 +0200 Subject: [PATCH 6/6] =?UTF-8?q?Ferdigstilt=20=C3=B8ving=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oving_2/main.py | 2 +- oving_3/main.py | 62 ++++++++++++++++++++++++----------------- oving_3/rtttl_player.py | 9 +++--- oving_4/rttl_player.py | 60 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 oving_4/rttl_player.py diff --git a/oving_2/main.py b/oving_2/main.py index 44ec493..fd996a8 100644 --- a/oving_2/main.py +++ b/oving_2/main.py @@ -23,7 +23,7 @@ leftMotor = Motor(port=Port.A) rightMotor = Motor(port=Port.C) -robotDriveBase = DriveBase(leftMotor, rightMotor, wheel_diameter=56, axle_track=110) +robotDriveBase = DriveBase(leftMotor, rightMotor, wheel_diameter=56, axle_track=180) startBtn = TouchSensor(port=Port.S1) usSensor = UltrasonicSensor(port=Port.S3) diff --git a/oving_3/main.py b/oving_3/main.py index 5e8bb24..6f6e751 100644 --- a/oving_3/main.py +++ b/oving_3/main.py @@ -38,13 +38,9 @@ #ev3.screen.print("Spiller", player.name, "...") #player.play() -def doRandomTrick(): - trickIndex = randint(0, 1) - - if trickIndex == 1: - spin() - elif trickindex == 2: - playMusic() +def doRandomTrick(actions): + trick = actions[randint(0, len(actions) - 1)] + trick() def spin(): directions = [-1, 1] @@ -59,6 +55,8 @@ def playMusic(): ev3.screen.print("Spiller", player.name, "...") player.play() +actions = [spin, playMusic] + leftMotor = Motor(port=Port.A) rightMotor = Motor(port=Port.C) robotDriveBase = DriveBase(leftMotor, rightMotor, wheel_diameter=56, axle_track=160) @@ -71,39 +69,53 @@ def playMusic(): speaker = ev3.speaker -leftSensorThreshold = 8 -rigthSensorThreshold = 7 -speed = 50 +leftSensorThreshold = 9 +rigthSensorThreshold = 10 +speed = 115 currentTime = time.time() -lastCorrectino = currentTime +lastCorrection = currentTime lastDirection = 1 lastTrick = currentTime +rotationSpeed = 40 while True: currentTime = time.time() - """ if currentTime - lastCorrectino > 2: + ev3.screen.print(leftColorSensor.reflection(), rightColorSensor.reflection()) + + if usSensor.distance() <= 50: + break + """ + if currentTime - lastCorrection > 2: while leftColorSensor.reflection() > leftSensorThreshold or rightColorSensor.reflection() < rigthSensorThreshold: - robotDriveBase.drive(-speed, 50 * lastDirection) - - lastCorrectino = time.time() - continue """ + i = (time.time() - lastCorrection) * 3 + robotDriveBase.drive(-speed, i * lastDirection) + + lastCorrection = time.time() + continue + """ if currentTime - lastTrick > 10: - doTrick() + doRandomTrick(actions) lastTrick = time.time() if leftColorSensor.reflection() < leftSensorThreshold: - robotDriveBase.drive(-speed, 25) - lastCorrectino == time.time() - lastDirection = -1 + robotDriveBase.drive(-speed, rotationSpeed) + lastCorrection == time.time() + lastDirection = 1 continue if rightColorSensor.reflection() < rigthSensorThreshold: - robotDriveBase.drive(-speed, -25) - lastCorrectino = currentTime - lastDirection = 1 + robotDriveBase.drive(-speed, -rotationSpeed) + lastCorrection = currentTime + lastDirection = -1 continue - - robotDriveBase.drive(-speed, 50) \ No newline at end of file + + + + robotDriveBase.drive(-speed, 0) + + +robotDriveBase.stop() +ev3.speaker.play_file(SoundFile.FANFARE) \ No newline at end of file diff --git a/oving_3/rtttl_player.py b/oving_3/rtttl_player.py index 5551073..8ca0e4f 100644 --- a/oving_3/rtttl_player.py +++ b/oving_3/rtttl_player.py @@ -8,12 +8,13 @@ class RTTTLPlayer: 'g#': 25.96, 'a': 27.50, 'a#': 29.14, 'b': 30.87 } - def __init__(self, ev3): + def __init__(self, ev3, rtttl: str): """ :param ev3: EV3Brick instance from pybricks.hubs :param rtttl: RTTTL string (name:d=4,o=5,b=100:notes) """ self.ev3 = ev3 + self.name, self.defaults, self.notes = self.parse_rtttl(rtttl) def parse_rtttl(self, rtttl: str): name, settings, notes = rtttl.split(':') @@ -31,15 +32,15 @@ def note_to_freq(self, note: str, octave: int): base = self.NOTE_FREQS[note] return round(base * (2 ** octave), 2) - def play(self, name, defaults, notes, tempo_scale: float = 1.0): + def play(self, tempo_scale: float = 1.0): """ Play the parsed RTTTL melody. :param tempo_scale: Scale factor for speed (1.0 = normal, 2.0 = twice as fast, 0.5 = half speed) """ - bpm = defaults.get('b', 63) + bpm = self.defaults.get('b', 63) whole_note = (60 / bpm) * 4 * 1000 # ms - for raw in notes: + for raw in self.notes: match = re.match(r'(\d+)?([a-gp][#]?)(\d)?(\.*)?', raw.strip(), re.I) if not match: continue diff --git a/oving_4/rttl_player.py b/oving_4/rttl_player.py new file mode 100644 index 0000000..8ca0e4f --- /dev/null +++ b/oving_4/rttl_player.py @@ -0,0 +1,60 @@ +import re + + +class RTTTLPlayer: + NOTE_FREQS = { + 'c': 16.35, 'c#': 17.32, 'd': 18.35, 'd#': 19.45, + 'e': 20.60, 'f': 21.83, 'f#': 23.12, 'g': 24.50, + 'g#': 25.96, 'a': 27.50, 'a#': 29.14, 'b': 30.87 + } + + def __init__(self, ev3, rtttl: str): + """ + :param ev3: EV3Brick instance from pybricks.hubs + :param rtttl: RTTTL string (name:d=4,o=5,b=100:notes) + """ + self.ev3 = ev3 + self.name, self.defaults, self.notes = self.parse_rtttl(rtttl) + + def parse_rtttl(self, rtttl: str): + name, settings, notes = rtttl.split(':') + defaults = {} + for s in settings.split(','): + k, v = s.split('=') + defaults[k] = int(v) + notes = notes.split(',') + return name, defaults, notes + + def note_to_freq(self, note: str, octave: int): + """ + Convert note name + octave to frequency in Hz. + """ + base = self.NOTE_FREQS[note] + return round(base * (2 ** octave), 2) + + def play(self, tempo_scale: float = 1.0): + """ + Play the parsed RTTTL melody. + :param tempo_scale: Scale factor for speed (1.0 = normal, 2.0 = twice as fast, 0.5 = half speed) + """ + bpm = self.defaults.get('b', 63) + whole_note = (60 / bpm) * 4 * 1000 # ms + + for raw in self.notes: + match = re.match(r'(\d+)?([a-gp][#]?)(\d)?(\.*)?', raw.strip(), re.I) + if not match: + continue + + dur, note, octave, dot = match.groups() + dur = int(dur) if dur else self.defaults.get('d', 4) + octave = int(octave) if octave else self.defaults.get('o', 6) + + duration = (whole_note / dur) * tempo_scale + if dot: + duration *= 1.5 + + if note.lower() == 'p': # pause + self.ev3.speaker.beep(0, int(duration)) + else: + freq = self.note_to_freq(note.lower(), octave) + self.ev3.speaker.beep(int(freq), int(duration)) \ No newline at end of file