-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathplay.py
executable file
·153 lines (134 loc) · 5.49 KB
/
play.py
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
147
148
149
150
151
152
#!/usr/bin/env python3
from __future__ import division, print_function
import string, time
import sounddevice as sd
import numpy as np
from scipy import io
import scipy.io.wavfile
import morse
SPS = 8000
LETTERS = string.ascii_uppercase
FREQ = 750
WPM = 25
FS = 10
AUDIO_PADDING = 0.5 # Seconds
CLICK_SMOOTH = 2 # Tone periods
def main(message, freq, wpm, fs, prompt, outFile):
if prompt:
# Load spoken letter WAV files
letterNames = loadLetterNames()
sps = letterNames[LETTERS[0]][0]
else:
sps = SPS
print('Audio samples per second =', sps)
print('Tone period =', round(1000/freq, 1), 'ms')
dps = morse.wpmToDps(wpm) # Dots per second
mspd = 1000/dps # Dot duration in milliseconds
farnsworthScale = morse.farnsworthScaleFactor(wpm, fs)
print('Dot width =', round(mspd, 1), 'ms')
print('Dash width =', int(round(mspd * morse.DASH_WIDTH)), 'ms')
print('Character space =', int(round(mspd * morse.CHAR_SPACE * farnsworthScale)), 'ms')
print('Word space =', int(round(mspd * morse.WORD_SPACE * farnsworthScale)), 'ms')
# Compute morse code audio from plain text
audio = stringToMorseAudio(message, sps, wpm, fs, freq, 0.5,
letterPrompts=letterNames if prompt else None, promptVolume=0.3)
audio /= 2
if outFile:
io.wavfile.write(outFile, sps, (audio * 2**15).astype(np.int16))
else:
playBlock(audio, sps)
time.sleep(0.1)
def addAudio(base, new, offset):
if base is None:
base = np.array([], dtype=np.float32)
assert offset >= 0
lenBase, lenNew = len(base), len(new)
if offset+lenNew > lenBase:
# Make base longer by padding with zeros
base = np.append(base, np.zeros(offset+lenNew-lenBase))
base[offset:offset+lenNew] += new
return base
def boolArrToSwitchedTone(boolArr, freq, sps, volume=1.0):
''' Create the tone audio from a bool array representation of morse code. '''
weightLen = int(CLICK_SMOOTH*sps/freq)
if weightLen % 2 == 0:
weightLen += 1 # Make sure the weight array is odd length
smoothingWeights = np.concatenate((np.arange(1, weightLen//2+1), np.arange(weightLen//2+1, 0, -1)))
smoothingWeights = smoothingWeights / np.sum(smoothingWeights)
numSamplesPadding = int(sps*AUDIO_PADDING) + int((weightLen-1)/2)
padding = np.zeros(numSamplesPadding, dtype=np.bool)
boolArr = np.concatenate((padding, boolArr, padding)).astype(np.float32)
if CLICK_SMOOTH <= 0:
smoothBoolArr = boolArr
else:
smoothBoolArr = np.correlate(boolArr, smoothingWeights, 'valid')
numSamples = len(smoothBoolArr)
x = np.arange(numSamples)
toneArr = np.sin(x * (freq*2*np.pi/sps)) * volume
toneArr *= smoothBoolArr
return toneArr
def stringToMorseAudio(message, sps=SPS, wpm=WPM, fs=FS, freq=FREQ, volume=1.0, letterPrompts=None, promptVolume=1.0):
message = message.upper()
code = morse.stringToMorse(message)
boolArr = morse.morseToBoolArr(code, sps, wpm, fs)
audio = boolArrToSwitchedTone(boolArr, freq, sps, volume)
numSamplesPadding = int(sps*AUDIO_PADDING)
if letterPrompts is not None:
for i in range(len(message)):
l = message[i]
if l in letterPrompts:
offsetPlus = morse.morseSampleDuration(morse.stringToMorse(message[:i+1]), sps, wpm, fs)
letterDuration = morse.morseSampleDuration(morse.letterToMorse(message[i]), sps, wpm, fs)
offset = numSamplesPadding + offsetPlus - letterDuration
audio = addAudio(audio, letterPrompts[l][1]*promptVolume, offset)
return audio
def loadLetterNames(pathTemplate='audio/letter-names/%s_.wav', letters=LETTERS):
out = {}
for letter in letters:
fName = pathTemplate % letter
out[letter] = loadWav(fName)
return out
def loadWav(fName):
rate, data = io.wavfile.read(fName, mmap=True)
dataScale = data.astype(np.float32) / maxDtypeVolume(data.dtype)
return rate, dataScale
def maxDtypeVolume(dtype):
try:
return np.iinfo(dtype).max # Integer data type
except ValueError:
return 1.0 # Float data type
def playLetterNamesBlock(letterNames):
sps = letterNames[LETTERS[0]][0]
letterNameList = [letterNames[l][1] for l in LETTERS]
alphabetAudio = np.concatenate(letterNameList)
playBlock(alphabetAudio, sps)
def genTone(frequency, duration, sps=SPS, volume=1.0):
return np.sin(np.arange(sps*duration)*(frequency*2*np.pi/sps))*volume
def playTone(*args, **kwargs):
play(genTone(*args, **kwargs))
def play(array, sps=SPS):
sd.play(array.astype(np.float32), sps)
def waitFor(array, sps=SPS):
duration = len(array) / sps
time.sleep(duration)
def playBlock(array, sps=SPS):
play(array, sps)
waitFor(array, sps)
if __name__ == '__main__':
import sys, argparse
parser = argparse.ArgumentParser(description='Convert text to morse code audio.')
parser.add_argument('-f', type=float, default=FREQ, help='Tone frequency')
parser.add_argument('--wpm', type=float, default=WPM, help='Words per minute')
parser.add_argument('--fs', type=float, default=FS, help='Farnsworth speed')
parser.add_argument('-p', action='store_true', default=False, help='Say letters along with morse code')
parser.add_argument('-o', type=str, default='', help='Output to given WAV file instead of playing sound')
parser.add_argument('message', nargs='*', help='Text to translate or blank to take from stdin')
args = parser.parse_args()
if len(args.message) > 0:
message = ' '.join(args.message)
else:
message = sys.stdin.read()
if not message:
print('Specify a message through the command line or stdin.')
message = 'Specify a message.'
main(message, args.f, args.wpm, args.fs, args.p, args.o)