-
Notifications
You must be signed in to change notification settings - Fork 0
/
beep-file
executable file
·146 lines (123 loc) · 5.14 KB
/
beep-file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#!/usr/bin/env python3
# beep-file: A simple utility for creating PC Speaker music
#
# Copyright (C) 2021 mini_bomba
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import sys
import os
import time
import struct
import dataclasses
from typing import BinaryIO
input_event = struct.Struct("16xHHI")
EV_SND = 0x12
SND_TONE = 0x02
class BeepDriver:
fd: BinaryIO
def __init__(self):
self.fd = open("/dev/input/by-path/platform-pcspkr-event-spkr", "wb", buffering=0)
def __enter__(self):
self.fd.__enter__()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.set_tone(0)
self.fd.__exit__(exc_type, exc_val, exc_tb)
def set_tone(self, freq: int):
self.fd.write(input_event.pack(EV_SND, SND_TONE, freq))
def beep(self, freq: int, duration: float):
self.set_tone(freq)
time.sleep(duration)
self.set_tone(0)
@dataclasses.dataclass(slots=True)
class LoopFrame:
start_pos: int
start_line: int
target: int
i: int
def __init__(self, start_pos: int, start_line: int, target: int):
self.start_pos = start_pos
self.start_line = start_line
self.target = target
self.i = 1
def should_loop(self) -> bool:
return self.i < self.target
def process_file(file_path: str):
with open(file_path, "r") as f:
with BeepDriver() as beep:
line_number = 0
loop_stack: list[LoopFrame] = []
while len(line := f.readline()) > 0:
line_number += 1
# remove comments
try:
line = line[:line.index("#")]
except ValueError:
pass
line = line.strip()
# skip blank or comment lines
if len(line) == 0:
continue
params = line.split()
# match instructions
match params:
case ("[", iterations): # begin loop
if not iterations.isdigit() or int(iterations) < 1:
print(f"Invalid loop iteration count at {line_number}")
iterations = 1
loop_stack.append(LoopFrame(f.tell(), line_number, int(iterations)))
case ("]",): # end loop
try:
frame = loop_stack[-1]
except IndexError:
print(f"No loop to end at line {line_number}")
continue
if frame.should_loop():
frame.i += 1
line_number = frame.start_line
f.seek(frame.start_pos)
else:
del loop_stack[-1]
case (frequency, duration): # play tone
if frequency == "x": # convert x to frequency of 0 (silent)
frequency = 0
elif not frequency.isdigit():
print(f"Invalid frequency at line {line_number}")
frequency = 0
if not duration.isdigit():
print(f"Invalid duration at line {line_number}")
continue
# parse as ints and do final validation
frequency, duration = int(frequency), int(duration)
if not 0 <= frequency < 65536:
print(f"Frequency out of range at line {line_number}")
frequency = 0
if duration <= 0:
print(f"Zero or negative duration at line {line_number}")
continue
# Execute command
beep.set_tone(frequency)
time.sleep(duration / 1000)
case _:
print(f"Invalid instruction at line {line_number}")
if len(loop_stack) > 0: # if anything is left on the loop stack, a loop was not closed
print(f"Unclosed loops at lines: {', '.join(str(frame.start_line) for frame in loop_stack)}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Missing file path with beep pattern argument")
exit(1)
elif not os.path.isfile(sys.argv[1]):
print(f"File '{sys.argv[1]}' is not a file")
exit(1)
process_file(sys.argv[1])