not1mm 24.4.30__py3-none-any.whl → 24.5.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- not1mm/__main__.py +40 -67
- not1mm/bandmap.py +3 -1
- not1mm/checkwindow.py +5 -1
- not1mm/data/new_contest.ui +5 -0
- not1mm/lib/playsound.py +296 -0
- not1mm/lib/version.py +1 -1
- not1mm/logwindow.py +5 -1
- not1mm/playsoundtest.py +15 -0
- not1mm/plugins/10_10_fall_cw.py +2 -0
- not1mm/plugins/10_10_spring_cw.py +2 -0
- not1mm/plugins/10_10_summer_phone.py +2 -0
- not1mm/plugins/10_10_winter_phone.py +2 -0
- not1mm/plugins/arrl_10m.py +2 -0
- not1mm/plugins/arrl_dx_cw.py +2 -0
- not1mm/plugins/arrl_dx_ssb.py +2 -0
- not1mm/plugins/arrl_ss_cw.py +2 -0
- not1mm/plugins/arrl_ss_phone.py +2 -0
- not1mm/plugins/arrl_vhf_jan.py +2 -0
- not1mm/plugins/arrl_vhf_jun.py +2 -0
- not1mm/plugins/arrl_vhf_sep.py +2 -0
- not1mm/plugins/canada_day.py +2 -0
- not1mm/plugins/cq_160_cw.py +2 -0
- not1mm/plugins/cq_160_ssb.py +2 -0
- not1mm/plugins/cq_wpx_cw.py +39 -0
- not1mm/plugins/cq_wpx_ssb.py +2 -0
- not1mm/plugins/cq_ww_cw.py +2 -0
- not1mm/plugins/cq_ww_ssb.py +2 -0
- not1mm/plugins/cwt.py +26 -0
- not1mm/plugins/general_logging.py +2 -0
- not1mm/plugins/iaru_hf.py +2 -0
- not1mm/plugins/icwc_mst.py +372 -0
- not1mm/plugins/jidx_cw.py +31 -0
- not1mm/plugins/jidx_ph.py +2 -0
- not1mm/plugins/naqp_cw.py +32 -0
- not1mm/plugins/naqp_ssb.py +2 -0
- not1mm/plugins/stew_perry_topband.py +2 -0
- not1mm/radio.py +12 -9
- not1mm/vfo.py +5 -1
- not1mm/voice_keying.py +103 -0
- {not1mm-24.4.30.dist-info → not1mm-24.5.9.dist-info}/METADATA +9 -21
- {not1mm-24.4.30.dist-info → not1mm-24.5.9.dist-info}/RECORD +45 -41
- {not1mm-24.4.30.dist-info → not1mm-24.5.9.dist-info}/LICENSE +0 -0
- {not1mm-24.4.30.dist-info → not1mm-24.5.9.dist-info}/WHEEL +0 -0
- {not1mm-24.4.30.dist-info → not1mm-24.5.9.dist-info}/entry_points.txt +0 -0
- {not1mm-24.4.30.dist-info → not1mm-24.5.9.dist-info}/top_level.txt +0 -0
not1mm/__main__.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
|
3
|
+
not1mm Contest logger
|
4
|
+
Email: michael.bridak@gmail.com
|
5
|
+
GPL V3
|
6
|
+
Purpose: Provides main logging window and a crap ton more.
|
4
7
|
"""
|
5
8
|
# pylint: disable=unused-import, c-extension-no-member, no-member, invalid-name, too-many-lines, no-name-in-module
|
6
9
|
# pylint: disable=logging-fstring-interpolation, logging-not-lazy, line-too-long, bare-except
|
@@ -35,7 +38,6 @@ except OSError as exception:
|
|
35
38
|
print(exception)
|
36
39
|
print("portaudio is not installed")
|
37
40
|
sd = None
|
38
|
-
import soundfile as sf
|
39
41
|
from PyQt6 import QtCore, QtGui, QtWidgets, uic
|
40
42
|
from PyQt6.QtCore import QDir, Qt, QThread
|
41
43
|
from PyQt6.QtGui import QFontDatabase, QColorConstants, QPalette, QColor
|
@@ -75,6 +77,7 @@ from not1mm.checkwindow import CheckWindow
|
|
75
77
|
from not1mm.bandmap import BandMapWindow
|
76
78
|
from not1mm.vfo import VfoWindow
|
77
79
|
from not1mm.radio import Radio
|
80
|
+
from not1mm.voice_keying import Voice
|
78
81
|
|
79
82
|
poll_time = datetime.datetime.now()
|
80
83
|
|
@@ -188,6 +191,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
188
191
|
current_palette = None
|
189
192
|
|
190
193
|
radio_thread = QThread()
|
194
|
+
voice_thread = QThread()
|
191
195
|
|
192
196
|
rig_control = None
|
193
197
|
log_window = None
|
@@ -479,6 +483,18 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
479
483
|
with open(fsutils.APP_DATA_PATH / "cty.json", "rt", encoding="utf-8") as c_file:
|
480
484
|
self.ctyfile = loads(c_file.read())
|
481
485
|
self.readpreferences()
|
486
|
+
|
487
|
+
self.voice_process = Voice()
|
488
|
+
self.voice_process.moveToThread(self.voice_thread)
|
489
|
+
self.voice_thread.started.connect(self.voice_process.run)
|
490
|
+
self.voice_thread.finished.connect(self.voice_process.deleteLater)
|
491
|
+
self.voice_process.ptt_on.connect(self.ptt_on)
|
492
|
+
self.voice_process.ptt_off.connect(self.ptt_off)
|
493
|
+
self.voice_process.current_op = self.current_op
|
494
|
+
self.voice_process.data_path = fsutils.USER_DATA_PATH
|
495
|
+
self.voice_process.sounddevice = self.pref.get("sounddevice", "default")
|
496
|
+
self.voice_thread.start()
|
497
|
+
|
482
498
|
self.dbname = fsutils.USER_DATA_PATH / self.pref.get(
|
483
499
|
"current_database", "ham.db"
|
484
500
|
)
|
@@ -492,6 +508,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
492
508
|
self.station = {}
|
493
509
|
self.contact = self.database.empty_contact
|
494
510
|
self.current_op = self.station.get("Call", "")
|
511
|
+
self.voice_process.current_op = self.current_op
|
495
512
|
self.make_op_dir()
|
496
513
|
self.read_cw_macros()
|
497
514
|
self.clearinputs()
|
@@ -922,6 +939,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
922
939
|
if self.station is None:
|
923
940
|
self.station = {}
|
924
941
|
self.current_op = self.station.get("Call", "")
|
942
|
+
self.voice_process.current_op = self.current_op
|
925
943
|
self.make_op_dir()
|
926
944
|
cmd = {}
|
927
945
|
cmd["cmd"] = "NEWDB"
|
@@ -958,6 +976,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
958
976
|
if self.station.get("Call", "") == "":
|
959
977
|
self.edit_station_settings()
|
960
978
|
self.current_op = self.station.get("Call", "")
|
979
|
+
self.voice_process.current_op = self.current_op
|
961
980
|
self.make_op_dir()
|
962
981
|
cmd = {}
|
963
982
|
cmd["cmd"] = "NEWDB"
|
@@ -1231,6 +1250,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
1231
1250
|
else:
|
1232
1251
|
self.cw_speed.hide()
|
1233
1252
|
|
1253
|
+
self.clearinputs()
|
1234
1254
|
cmd = {}
|
1235
1255
|
cmd["cmd"] = "NEWDB"
|
1236
1256
|
cmd["station"] = platform.node()
|
@@ -1832,6 +1852,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
1832
1852
|
self.contact = self.database.empty_contact
|
1833
1853
|
self.heading_distance.setText("")
|
1834
1854
|
self.dx_entity.setText("")
|
1855
|
+
|
1835
1856
|
if self.contest:
|
1836
1857
|
mults = self.contest.show_mults(self)
|
1837
1858
|
qsos = self.contest.show_qso(self)
|
@@ -1840,13 +1861,16 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
1840
1861
|
score = self.contest.calc_score(self)
|
1841
1862
|
self.score.setText(str(score))
|
1842
1863
|
self.contest.reset_label(self)
|
1864
|
+
if self.contest.name != "ICWC Medium Speed Test":
|
1865
|
+
if self.current_mode == "CW":
|
1866
|
+
self.sent.setText("599")
|
1867
|
+
self.receive.setText("599")
|
1868
|
+
else:
|
1869
|
+
self.sent.setText("59")
|
1870
|
+
self.receive.setText("59")
|
1871
|
+
else:
|
1872
|
+
self.sent.setText("")
|
1843
1873
|
self.callsign.clear()
|
1844
|
-
if self.current_mode == "CW":
|
1845
|
-
self.sent.setText("599")
|
1846
|
-
self.receive.setText("599")
|
1847
|
-
else:
|
1848
|
-
self.sent.setText("59")
|
1849
|
-
self.receive.setText("59")
|
1850
1874
|
self.other_1.clear()
|
1851
1875
|
self.other_2.clear()
|
1852
1876
|
self.callsign.setFocus()
|
@@ -2118,6 +2142,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
2118
2142
|
self.settings_dialog.close()
|
2119
2143
|
if self.current_op == "":
|
2120
2144
|
self.current_op = self.station.get("Call", "")
|
2145
|
+
self.voice_process.current_op = self.current_op
|
2121
2146
|
self.make_op_dir()
|
2122
2147
|
contest_count = self.database.fetch_all_contests()
|
2123
2148
|
if len(contest_count) == 0:
|
@@ -2199,61 +2224,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
2199
2224
|
)
|
2200
2225
|
return macro
|
2201
2226
|
|
2202
|
-
def voice_string(self, the_string: str) -> None:
|
2203
|
-
"""
|
2204
|
-
voices string using nato phonetics.
|
2205
|
-
|
2206
|
-
Parameters
|
2207
|
-
----------
|
2208
|
-
the_string : str
|
2209
|
-
String to voicify.
|
2210
|
-
|
2211
|
-
Returns
|
2212
|
-
-------
|
2213
|
-
None
|
2214
|
-
"""
|
2215
|
-
|
2216
|
-
logger.debug("Voicing: %s", the_string)
|
2217
|
-
if sd is None:
|
2218
|
-
logger.warning("Sounddevice/portaudio not installed.")
|
2219
|
-
return
|
2220
|
-
op_path = fsutils.USER_DATA_PATH / self.current_op
|
2221
|
-
if "[" in the_string:
|
2222
|
-
sub_string = the_string.strip("[]").lower()
|
2223
|
-
filename = f"{str(op_path)}/{sub_string}.wav"
|
2224
|
-
if Path(filename).is_file():
|
2225
|
-
logger.debug("Voicing: %s", filename)
|
2226
|
-
data, _fs = sf.read(filename, dtype="float32")
|
2227
|
-
self.ptt_on()
|
2228
|
-
try:
|
2229
|
-
sd.default.device = self.pref.get("sounddevice", "default")
|
2230
|
-
sd.default.samplerate = 44100.0
|
2231
|
-
sd.play(data, blocking=False)
|
2232
|
-
# _status = sd.wait()
|
2233
|
-
# https://snyk.io/advisor/python/sounddevice/functions/sounddevice.PortAudioError
|
2234
|
-
except sd.PortAudioError as err:
|
2235
|
-
logger.warning("%s", f"{err}")
|
2236
|
-
|
2237
|
-
self.ptt_off()
|
2238
|
-
return
|
2239
|
-
self.ptt_on()
|
2240
|
-
for letter in the_string.lower():
|
2241
|
-
if letter in "abcdefghijklmnopqrstuvwxyz 1234567890":
|
2242
|
-
if letter == " ":
|
2243
|
-
letter = "space"
|
2244
|
-
filename = f"{str(op_path)}/{letter}.wav"
|
2245
|
-
if Path(filename).is_file():
|
2246
|
-
logger.debug("Voicing: %s", filename)
|
2247
|
-
data, _fs = sf.read(filename, dtype="float32")
|
2248
|
-
try:
|
2249
|
-
sd.default.device = self.pref.get("sounddevice", "default")
|
2250
|
-
sd.default.samplerate = 44100.0
|
2251
|
-
sd.play(data, blocking=False)
|
2252
|
-
logger.debug("%s", f"{sd.wait()}")
|
2253
|
-
except sd.PortAudioError as err:
|
2254
|
-
logger.warning("%s", f"{err}")
|
2255
|
-
self.ptt_off()
|
2256
|
-
|
2257
2227
|
def ptt_on(self) -> None:
|
2258
2228
|
"""
|
2259
2229
|
Turn on ptt for rig.
|
@@ -2285,6 +2255,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
2285
2255
|
-------
|
2286
2256
|
None
|
2287
2257
|
"""
|
2258
|
+
|
2288
2259
|
logger.debug("PTT Off")
|
2289
2260
|
if self.rig_control:
|
2290
2261
|
self.leftdot.setPixmap(self.reddot)
|
@@ -2309,7 +2280,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
2309
2280
|
if self.n1mm:
|
2310
2281
|
self.n1mm.radio_info["FunctionKeyCaption"] = function_key.text()
|
2311
2282
|
if self.radio_state.get("mode") in ["LSB", "USB", "SSB"]:
|
2312
|
-
self.voice_string(self.process_macro(function_key.toolTip()))
|
2283
|
+
self.voice_process.voice_string(self.process_macro(function_key.toolTip()))
|
2284
|
+
# self.voice_string(self.process_macro(function_key.toolTip()))
|
2313
2285
|
return
|
2314
2286
|
if self.cw:
|
2315
2287
|
if self.pref.get("cwtype") == 3 and self.rig_control is not None:
|
@@ -3046,15 +3018,16 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
3046
3018
|
if mode == "CW":
|
3047
3019
|
if self.current_mode != "CW":
|
3048
3020
|
self.current_mode = "CW"
|
3049
|
-
# self.mode.setText("CW")
|
3050
3021
|
self.sent.setText("599")
|
3051
3022
|
self.receive.setText("599")
|
3052
3023
|
self.read_cw_macros()
|
3024
|
+
if self.contest:
|
3025
|
+
if self.contest.name == "ICWC Medium Speed Test":
|
3026
|
+
self.contest.prefill(self)
|
3053
3027
|
return
|
3054
3028
|
if mode == "SSB":
|
3055
3029
|
if self.current_mode != "SSB":
|
3056
3030
|
self.current_mode = "SSB"
|
3057
|
-
# self.mode.setText("SSB")
|
3058
3031
|
self.sent.setText("59")
|
3059
3032
|
self.receive.setText("59")
|
3060
3033
|
self.read_cw_macros()
|
@@ -3062,7 +3035,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
3062
3035
|
if mode == "RTTY":
|
3063
3036
|
if self.current_mode != "RTTY":
|
3064
3037
|
self.current_mode = "RTTY"
|
3065
|
-
# self.mode.setText("RTTY")
|
3066
3038
|
self.sent.setText("59")
|
3067
3039
|
self.receive.setText("59")
|
3068
3040
|
|
@@ -3103,6 +3075,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
3103
3075
|
|
3104
3076
|
if self.opon_dialog.NewOperator.text():
|
3105
3077
|
self.current_op = self.opon_dialog.NewOperator.text().upper()
|
3078
|
+
self.voice_process.current_op = self.current_op
|
3106
3079
|
self.opon_dialog.close()
|
3107
3080
|
logger.debug("New Op: %s", self.current_op)
|
3108
3081
|
self.make_op_dir()
|
not1mm/bandmap.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
|
3
|
+
not1mm Contest logger
|
4
4
|
Email: michael.bridak@gmail.com
|
5
5
|
GPL V3
|
6
|
+
Class: BandMapWindow
|
7
|
+
Purpose: Onscreen widget to show realtime spots from an AR cluster.
|
6
8
|
"""
|
7
9
|
|
8
10
|
# pylint: disable=unused-import, c-extension-no-member, no-member, invalid-name, too-many-lines
|
not1mm/checkwindow.py
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
|
3
|
+
not1mm Contest logger
|
4
|
+
Email: michael.bridak@gmail.com
|
5
|
+
GPL V3
|
6
|
+
Class: CheckWindow
|
7
|
+
Purpose: Onscreen widget to show possible matches to callsigns entered in the main window.
|
4
8
|
"""
|
5
9
|
# pylint: disable=no-name-in-module, unused-import, no-member, invalid-name, c-extension-no-member
|
6
10
|
# pylint: disable=logging-fstring-interpolation, line-too-long
|
not1mm/data/new_contest.ui
CHANGED
not1mm/lib/playsound.py
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
import logging
|
2
|
+
from platform import system
|
3
|
+
|
4
|
+
logger = logging.getLogger(__name__)
|
5
|
+
|
6
|
+
|
7
|
+
class PlaysoundException(Exception):
|
8
|
+
pass
|
9
|
+
|
10
|
+
|
11
|
+
def _canonicalizePath(path):
|
12
|
+
"""
|
13
|
+
Support passing in a pathlib.Path-like object by converting to str.
|
14
|
+
"""
|
15
|
+
import sys
|
16
|
+
|
17
|
+
if sys.version_info[0] >= 3:
|
18
|
+
return str(path)
|
19
|
+
else:
|
20
|
+
# On earlier Python versions, str is a byte string, so attempting to
|
21
|
+
# convert a unicode string to str will fail. Leave it alone in this case.
|
22
|
+
return path
|
23
|
+
|
24
|
+
|
25
|
+
def _playsoundWin(sound, block=True):
|
26
|
+
"""
|
27
|
+
Utilizes windll.winmm. Tested and known to work with MP3 and WAVE on
|
28
|
+
Windows 7 with Python 2.7. Probably works with more file formats.
|
29
|
+
Probably works on Windows XP thru Windows 10. Probably works with all
|
30
|
+
versions of Python.
|
31
|
+
|
32
|
+
Inspired by (but not copied from) Michael Gundlach <gundlach@gmail.com>'s mp3play:
|
33
|
+
https://github.com/michaelgundlach/mp3play
|
34
|
+
|
35
|
+
I never would have tried using windll.winmm without seeing his code.
|
36
|
+
"""
|
37
|
+
sound = '"' + _canonicalizePath(sound) + '"'
|
38
|
+
|
39
|
+
from ctypes import create_unicode_buffer, windll, wintypes
|
40
|
+
|
41
|
+
windll.winmm.mciSendStringW.argtypes = [
|
42
|
+
wintypes.LPCWSTR,
|
43
|
+
wintypes.LPWSTR,
|
44
|
+
wintypes.UINT,
|
45
|
+
wintypes.HANDLE,
|
46
|
+
]
|
47
|
+
windll.winmm.mciGetErrorStringW.argtypes = [
|
48
|
+
wintypes.DWORD,
|
49
|
+
wintypes.LPWSTR,
|
50
|
+
wintypes.UINT,
|
51
|
+
]
|
52
|
+
|
53
|
+
def winCommand(*command):
|
54
|
+
bufLen = 600
|
55
|
+
buf = create_unicode_buffer(bufLen)
|
56
|
+
command = " ".join(command)
|
57
|
+
errorCode = int(
|
58
|
+
windll.winmm.mciSendStringW(command, buf, bufLen - 1, 0)
|
59
|
+
) # use widestring version of the function
|
60
|
+
if errorCode:
|
61
|
+
errorBuffer = create_unicode_buffer(bufLen)
|
62
|
+
windll.winmm.mciGetErrorStringW(
|
63
|
+
errorCode, errorBuffer, bufLen - 1
|
64
|
+
) # use widestring version of the function
|
65
|
+
exceptionMessage = (
|
66
|
+
"\n Error " + str(errorCode) + " for command:"
|
67
|
+
"\n " + command + "\n " + errorBuffer.value
|
68
|
+
)
|
69
|
+
logger.error(exceptionMessage)
|
70
|
+
raise PlaysoundException(exceptionMessage)
|
71
|
+
return buf.value
|
72
|
+
|
73
|
+
try:
|
74
|
+
logger.debug("Starting")
|
75
|
+
winCommand("open {}".format(sound))
|
76
|
+
winCommand("play {}{}".format(sound, " wait" if block else ""))
|
77
|
+
logger.debug("Returning")
|
78
|
+
finally:
|
79
|
+
try:
|
80
|
+
winCommand("close {}".format(sound))
|
81
|
+
except PlaysoundException:
|
82
|
+
logger.warning("Failed to close the file: {}".format(sound))
|
83
|
+
# If it fails, there's nothing more that can be done...
|
84
|
+
pass
|
85
|
+
|
86
|
+
|
87
|
+
def _handlePathOSX(sound):
|
88
|
+
sound = _canonicalizePath(sound)
|
89
|
+
|
90
|
+
if "://" not in sound:
|
91
|
+
if not sound.startswith("/"):
|
92
|
+
from os import getcwd
|
93
|
+
|
94
|
+
sound = getcwd() + "/" + sound
|
95
|
+
sound = "file://" + sound
|
96
|
+
|
97
|
+
try:
|
98
|
+
# Don't double-encode it.
|
99
|
+
sound.encode("ascii")
|
100
|
+
return sound.replace(" ", "%20")
|
101
|
+
except UnicodeEncodeError:
|
102
|
+
try:
|
103
|
+
from urllib.parse import quote # Try the Python 3 import first...
|
104
|
+
except ImportError:
|
105
|
+
from urllib import (
|
106
|
+
quote,
|
107
|
+
) # Try using the Python 2 import before giving up entirely...
|
108
|
+
|
109
|
+
parts = sound.split("://", 1)
|
110
|
+
return parts[0] + "://" + quote(parts[1].encode("utf-8")).replace(" ", "%20")
|
111
|
+
|
112
|
+
|
113
|
+
def _playsoundOSX(sound, block=True):
|
114
|
+
"""
|
115
|
+
Utilizes AppKit.NSSound. Tested and known to work with MP3 and WAVE on
|
116
|
+
OS X 10.11 with Python 2.7. Probably works with anything QuickTime supports.
|
117
|
+
Probably works on OS X 10.5 and newer. Probably works with all versions of
|
118
|
+
Python.
|
119
|
+
|
120
|
+
Inspired by (but not copied from) Aaron's Stack Overflow answer here:
|
121
|
+
http://stackoverflow.com/a/34568298/901641
|
122
|
+
|
123
|
+
I never would have tried using AppKit.NSSound without seeing his code.
|
124
|
+
"""
|
125
|
+
try:
|
126
|
+
from AppKit import NSSound
|
127
|
+
except ImportError:
|
128
|
+
logger.warning(
|
129
|
+
"playsound could not find a copy of AppKit - falling back to using macOS's system copy."
|
130
|
+
)
|
131
|
+
sys.path.append(
|
132
|
+
"/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC"
|
133
|
+
)
|
134
|
+
from AppKit import NSSound
|
135
|
+
|
136
|
+
from Foundation import NSURL
|
137
|
+
from time import sleep
|
138
|
+
|
139
|
+
sound = _handlePathOSX(sound)
|
140
|
+
url = NSURL.URLWithString_(sound)
|
141
|
+
if not url:
|
142
|
+
raise PlaysoundException("Cannot find a sound with filename: " + sound)
|
143
|
+
|
144
|
+
for i in range(5):
|
145
|
+
nssound = NSSound.alloc().initWithContentsOfURL_byReference_(url, True)
|
146
|
+
if nssound:
|
147
|
+
break
|
148
|
+
else:
|
149
|
+
logger.debug("Failed to load sound, although url was good... " + sound)
|
150
|
+
else:
|
151
|
+
raise PlaysoundException(
|
152
|
+
"Could not load sound with filename, although URL was good... " + sound
|
153
|
+
)
|
154
|
+
nssound.play()
|
155
|
+
|
156
|
+
if block:
|
157
|
+
sleep(nssound.duration())
|
158
|
+
|
159
|
+
|
160
|
+
def _playsoundNix(sound, block=True):
|
161
|
+
"""Play a sound using GStreamer.
|
162
|
+
|
163
|
+
Inspired by this:
|
164
|
+
https://gstreamer.freedesktop.org/documentation/tutorials/playback/playbin-usage.html
|
165
|
+
"""
|
166
|
+
sound = _canonicalizePath(sound)
|
167
|
+
|
168
|
+
# pathname2url escapes non-URL-safe characters
|
169
|
+
from os.path import abspath, exists
|
170
|
+
|
171
|
+
try:
|
172
|
+
from urllib.request import pathname2url
|
173
|
+
except ImportError:
|
174
|
+
# python 2
|
175
|
+
from urllib import pathname2url
|
176
|
+
|
177
|
+
import gi
|
178
|
+
|
179
|
+
gi.require_version("Gst", "1.0")
|
180
|
+
from gi.repository import Gst
|
181
|
+
|
182
|
+
Gst.init(None)
|
183
|
+
|
184
|
+
playbin = Gst.ElementFactory.make("playbin", "playbin")
|
185
|
+
if sound.startswith(("http://", "https://")):
|
186
|
+
playbin.props.uri = sound
|
187
|
+
else:
|
188
|
+
path = abspath(sound)
|
189
|
+
if not exists(path):
|
190
|
+
raise PlaysoundException("File not found: {}".format(path))
|
191
|
+
playbin.props.uri = "file://" + pathname2url(path)
|
192
|
+
|
193
|
+
set_result = playbin.set_state(Gst.State.PLAYING)
|
194
|
+
if set_result != Gst.StateChangeReturn.ASYNC:
|
195
|
+
raise PlaysoundException("playbin.set_state returned " + repr(set_result))
|
196
|
+
|
197
|
+
# FIXME: use some other bus method than poll() with block=False
|
198
|
+
# https://lazka.github.io/pgi-docs/#Gst-1.0/classes/Bus.html
|
199
|
+
logger.debug("Starting play")
|
200
|
+
if block:
|
201
|
+
bus = playbin.get_bus()
|
202
|
+
try:
|
203
|
+
bus.poll(Gst.MessageType.EOS, Gst.CLOCK_TIME_NONE)
|
204
|
+
finally:
|
205
|
+
playbin.set_state(Gst.State.NULL)
|
206
|
+
|
207
|
+
logger.debug("Finishing play")
|
208
|
+
|
209
|
+
|
210
|
+
def _playsoundAnotherPython(otherPython, sound, block=True, macOS=False):
|
211
|
+
"""
|
212
|
+
Mostly written so that when this is run on python3 on macOS, it can invoke
|
213
|
+
python2 on macOS... but maybe this idea could be useful on linux, too.
|
214
|
+
"""
|
215
|
+
from inspect import getsourcefile
|
216
|
+
from os.path import abspath, exists
|
217
|
+
from subprocess import check_call
|
218
|
+
from threading import Thread
|
219
|
+
|
220
|
+
sound = _canonicalizePath(sound)
|
221
|
+
|
222
|
+
class PropogatingThread(Thread):
|
223
|
+
def run(self):
|
224
|
+
self.exc = None
|
225
|
+
try:
|
226
|
+
self.ret = self._target(*self._args, **self._kwargs)
|
227
|
+
except BaseException as e:
|
228
|
+
self.exc = e
|
229
|
+
|
230
|
+
def join(self, timeout=None):
|
231
|
+
super().join(timeout)
|
232
|
+
if self.exc:
|
233
|
+
raise self.exc
|
234
|
+
return self.ret
|
235
|
+
|
236
|
+
# Check if the file exists...
|
237
|
+
if not exists(abspath(sound)):
|
238
|
+
raise PlaysoundException("Cannot find a sound with filename: " + sound)
|
239
|
+
|
240
|
+
playsoundPath = abspath(getsourcefile(lambda: 0))
|
241
|
+
t = PropogatingThread(
|
242
|
+
target=lambda: check_call(
|
243
|
+
[otherPython, playsoundPath, _handlePathOSX(sound) if macOS else sound]
|
244
|
+
)
|
245
|
+
)
|
246
|
+
t.start()
|
247
|
+
if block:
|
248
|
+
t.join()
|
249
|
+
|
250
|
+
|
251
|
+
system = system()
|
252
|
+
|
253
|
+
if system == "Windows":
|
254
|
+
playsound = _playsoundWin
|
255
|
+
elif system == "Darwin":
|
256
|
+
playsound = _playsoundOSX
|
257
|
+
import sys
|
258
|
+
|
259
|
+
if sys.version_info[0] > 2:
|
260
|
+
try:
|
261
|
+
from AppKit import NSSound
|
262
|
+
except ImportError:
|
263
|
+
logger.warning(
|
264
|
+
"playsound is relying on a python 2 subprocess. Please use `pip3 install PyObjC` if you want playsound to run more efficiently."
|
265
|
+
)
|
266
|
+
playsound = lambda sound, block=True: _playsoundAnotherPython(
|
267
|
+
"/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python",
|
268
|
+
sound,
|
269
|
+
block,
|
270
|
+
macOS=True,
|
271
|
+
)
|
272
|
+
else:
|
273
|
+
playsound = _playsoundNix
|
274
|
+
if (
|
275
|
+
__name__ != "__main__"
|
276
|
+
): # Ensure we don't infinitely recurse trying to get another python instance.
|
277
|
+
try:
|
278
|
+
import gi
|
279
|
+
|
280
|
+
gi.require_version("Gst", "1.0")
|
281
|
+
from gi.repository import Gst
|
282
|
+
except:
|
283
|
+
logger.warning(
|
284
|
+
"playsound is relying on another python subprocess. Please use `pip install pygobject` if you want playsound to run more efficiently."
|
285
|
+
)
|
286
|
+
playsound = lambda sound, block=True: _playsoundAnotherPython(
|
287
|
+
"/usr/bin/python3", sound, block, macOS=False
|
288
|
+
)
|
289
|
+
|
290
|
+
del system
|
291
|
+
|
292
|
+
if __name__ == "__main__":
|
293
|
+
# block is always True if you choose to run this from the command line.
|
294
|
+
from sys import argv
|
295
|
+
|
296
|
+
playsound(argv[1])
|
not1mm/lib/version.py
CHANGED
not1mm/logwindow.py
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
|
3
|
+
not1mm Contest logger
|
4
|
+
Email: michael.bridak@gmail.com
|
5
|
+
GPL V3
|
6
|
+
Class: LogWindow
|
7
|
+
Purpose: Onscreen widget to show and edit logged contacts.
|
4
8
|
"""
|
5
9
|
# pylint: disable=no-name-in-module, unused-import, no-member, c-extension-no-member
|
6
10
|
# pylint: disable=logging-fstring-interpolation, too-many-lines
|
not1mm/playsoundtest.py
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
NOT1MM Logger
|
4
|
+
Purpose: test alternative sound playing interface
|
5
|
+
"""
|
6
|
+
# pylint: disable=unused-import, c-extension-no-member, no-member, invalid-name, too-many-lines, no-name-in-module
|
7
|
+
# pylint: disable=logging-fstring-interpolation, logging-not-lazy, line-too-long, bare-except
|
8
|
+
|
9
|
+
from not1mm.lib.playsound import playsound
|
10
|
+
|
11
|
+
import not1mm.fsutils as fsutils
|
12
|
+
|
13
|
+
filename = fsutils.APP_DATA_PATH / "phonetics/cq.wav"
|
14
|
+
|
15
|
+
playsound(filename, True)
|
not1mm/plugins/10_10_fall_cw.py
CHANGED
@@ -48,6 +48,8 @@ def interface(self):
|
|
48
48
|
self.field2.show()
|
49
49
|
self.field3.hide()
|
50
50
|
self.field4.show()
|
51
|
+
self.snt_label.setText("SNT")
|
52
|
+
self.field1.setAccessibleName("RST Sent")
|
51
53
|
label = self.field4.findChild(QtWidgets.QLabel)
|
52
54
|
label.setText("Name 1010# SPC")
|
53
55
|
self.field4.setAccessibleName("Name 10 10 # SPC")
|
@@ -47,6 +47,8 @@ def interface(self):
|
|
47
47
|
self.field2.show()
|
48
48
|
self.field3.hide()
|
49
49
|
self.field4.show()
|
50
|
+
self.snt_label.setText("SNT")
|
51
|
+
self.field1.setAccessibleName("RST Sent")
|
50
52
|
label = self.field4.findChild(QtWidgets.QLabel)
|
51
53
|
label.setText("Name 1010# SPC")
|
52
54
|
self.field4.setAccessibleName("Name 10 10 # SPC")
|
@@ -49,6 +49,8 @@ def interface(self):
|
|
49
49
|
self.field2.show()
|
50
50
|
self.field3.hide()
|
51
51
|
self.field4.show()
|
52
|
+
self.snt_label.setText("SNT")
|
53
|
+
self.field1.setAccessibleName("RST Sent")
|
52
54
|
label = self.field4.findChild(QtWidgets.QLabel)
|
53
55
|
label.setText("Name 1010# SPC")
|
54
56
|
self.field4.setAccessibleName("Name 10 10 # SPC")
|
@@ -49,6 +49,8 @@ def interface(self):
|
|
49
49
|
self.field2.show()
|
50
50
|
self.field3.hide()
|
51
51
|
self.field4.show()
|
52
|
+
self.snt_label.setText("SNT")
|
53
|
+
self.field1.setAccessibleName("RST Sent")
|
52
54
|
label = self.field4.findChild(QtWidgets.QLabel)
|
53
55
|
label.setText("Name 1010# SPC")
|
54
56
|
self.field4.setAccessibleName("Name 10 10 # SPC")
|
not1mm/plugins/arrl_10m.py
CHANGED
@@ -94,6 +94,8 @@ def interface(self):
|
|
94
94
|
self.field2.show()
|
95
95
|
self.field3.show()
|
96
96
|
self.field4.show()
|
97
|
+
self.snt_label.setText("SNT")
|
98
|
+
self.field1.setAccessibleName("RST Sent")
|
97
99
|
label = self.field3.findChild(QtWidgets.QLabel)
|
98
100
|
label.setText("SentNR")
|
99
101
|
self.field3.setAccessibleName("Sent Number")
|
not1mm/plugins/arrl_dx_cw.py
CHANGED
@@ -53,6 +53,8 @@ def interface(self):
|
|
53
53
|
self.field2.show()
|
54
54
|
self.field3.hide()
|
55
55
|
self.field4.show()
|
56
|
+
self.snt_label.setText("SNT")
|
57
|
+
self.field1.setAccessibleName("RST Sent")
|
56
58
|
label = self.field4.findChild(QtWidgets.QLabel)
|
57
59
|
label.setText("Power")
|
58
60
|
self.field4.setAccessibleName("Power")
|
not1mm/plugins/arrl_dx_ssb.py
CHANGED
@@ -53,6 +53,8 @@ def interface(self):
|
|
53
53
|
self.field2.show()
|
54
54
|
self.field3.hide()
|
55
55
|
self.field4.show()
|
56
|
+
self.snt_label.setText("SNT")
|
57
|
+
self.field1.setAccessibleName("RST Sent")
|
56
58
|
label = self.field4.findChild(QtWidgets.QLabel)
|
57
59
|
label.setText("Power")
|
58
60
|
self.field4.setAccessibleName("Power")
|
not1mm/plugins/arrl_ss_cw.py
CHANGED
@@ -52,6 +52,8 @@ def interface(self):
|
|
52
52
|
self.field2.show()
|
53
53
|
self.field3.show()
|
54
54
|
self.field4.show()
|
55
|
+
self.snt_label.setText("SNT")
|
56
|
+
self.field1.setAccessibleName("RST Sent")
|
55
57
|
label = self.field3.findChild(QtWidgets.QLabel)
|
56
58
|
label.setText("SentNR")
|
57
59
|
self.field3.setAccessibleName("Sent Number")
|