-
Notifications
You must be signed in to change notification settings - Fork 10
/
server.py
251 lines (189 loc) · 8.18 KB
/
server.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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
from __future__ import division
import io
import json
import struct
import numpy as np
from tornado.websocket import WebSocketHandler
from pysoundfile import SoundFile
def hann(n):
return 0.5 - 0.5 * np.cos(2.0 * np.pi * np.arange(n) / (n - 1))
def from_bytes(b):
return struct.unpack("@i", b)[0]
def to_bytes(n):
return struct.pack("@i", n)
class JSONWebSocket(WebSocketHandler):
"""A websocket that sends/receives JSON messages.
Each message has a type, a content and optional binary data.
Message type and message content are stored as a JSON object. Type
must be a string and content must be JSON-serializable.
If binary data is present, the message will be sent in binary,
with the first four bytes storing a signed integer containing the
length of the JSON data, then the JSON data, then the binary data.
The binary data will be stored 8-byte aligned.
"""
def check_origin(self, origin):
return True
def open(self):
print("WebSocket opened")
def send_message(self, msg_type, content, data=None):
"""Send a message.
Arguments:
msg_type the message type as string.
content the message content as json-serializable data.
data raw bytes that are appended to the message.
"""
if data is None:
self.write_message(json.dumps({"type": msg_type,
"content": content}).encode())
else:
header = json.dumps({'type': msg_type,
'content': content}).encode()
# append enough spaces so that the payload starts at an 8-byte
# aligned position. The first four bytes will be the length of
# the header, encoded as a 32 bit signed integer:
header += b' ' * (8 - ((len(header) + 4) % 8))
# the length of the header as a binary 32 bit signed integer:
prefix = to_bytes(len(header))
self.write_message(prefix + header + data, binary=True)
def on_message(self, msg):
"""Parses a message
Each message must contain the message type, the message
content, and an optional binary payload. The decoded message
will be forwarded to receive_message().
Arguments:
msg the message, either as str or bytes.
"""
if isinstance(msg, bytes):
header_len = from_bytes(msg[:4])
header = msg[4:header_len + 4].decode()
data = msg[4 + header_len:]
else:
header = msg
data = None
try:
header = json.loads(header)
except ValueError:
print('message {} is not a valid JSON object'.format(msg))
return
if 'type' not in header:
print('message {} does not have a "type" field'.format(header))
elif 'content' not in header:
print('message {} does not have a "content" field'.format(header))
else:
self.receive_message(header['type'], header['content'], data)
def receive_message(self, msg_type, content, data=None):
"""Message dispatcher.
This is meant to be overwritten by subclasses. By itself, it
does nothing but complain.
"""
if msg_type == "information":
print(content)
else:
print(("Don't know what to do with message of type {}" +
"and content {}").format(msg_type, content))
def on_close(self):
print("WebSocket closed")
class SpectrogramWebSocket(JSONWebSocket):
"""A websocket that sends spectrogram data.
It calculates a spectrogram with a given FFT length and overlap
for a requested file. The file can either be supplied as a binary
data blob, or as a file name.
This implements two message types:
- request_file_spectrogram, which needs a filename, and optionally
`nfft` and `overlap`.
- request_data_spectrogram, which needs the file as a binary data
blob, and optionally `nfft` and `overlap`.
"""
def receive_message(self, msg_type, content, data=None):
"""Message dispatcher.
Dispatches
- `request_file_spectrogram` to self.on_file_spectrogram
- `request_data_spectrogram` to self.on_data_spectrogram
Arguments:
msg_type the message type as string.
content the message content as dictionary.
data raw bytes.
"""
if msg_type == 'request_file_spectrogram':
self.on_file_spectrogram(**content)
elif msg_type == 'request_data_spectrogram':
self.on_data_spectrogram(data, **content)
else:
super(self.__class__, self).receive_message(
msg_type, content, data)
def on_file_spectrogram(self, filename, nfft=1024, overlap=0.5):
"""Loads an audio file and calculates a spectrogram.
Arguments:
filename the file name from which to load the audio data.
nfft the FFT length used for calculating the spectrogram.
overlap the amount of overlap between consecutive spectra.
"""
try:
file = SoundFile(filename)
sound = file[:].sum(axis=1)
spec = self.spectrogram(sound, nfft, overlap)
self.send_message('spectrogram',
{'extent': spec.shape,
'fs': file.samplerate,
'length': len(file) / file.samplerate},
spec.tostring())
except RuntimeError as e:
error_msg = 'Filename: {} could not be loaded.\n{}'.format(filename, e)
self.send_message('error', {
'error_msg': error_msg
})
print(error_msg)
def on_data_spectrogram(self, data, nfft=1024, overlap=0.5):
"""Loads an audio file and calculates a spectrogram.
Arguments:
data the content of a file from which to load audio data.
nfft the FFT length used for calculating the spectrogram.
overlap the amount of overlap between consecutive spectra.
"""
file = SoundFile(io.BytesIO(data))
sound = file[:].sum(axis=1)
spec = self.spectrogram(sound, nfft, overlap)
self.send_message('spectrogram',
{'extent': spec.shape,
'fs': file.samplerate,
'length': len(file) / file.samplerate},
spec.tostring())
def spectrogram(self, data, nfft, overlap):
"""Calculate a real spectrogram from audio data
An audio data will be cut up into overlapping blocks of length
`nfft`. The amount of overlap will be `overlap*nfft`. Then,
calculate a real fourier transform of length `nfft` of every
block and save the absolute spectrum.
Arguments:
data audio data as a numpy array.
nfft the FFT length used for calculating the spectrogram.
overlap the amount of overlap between consecutive spectra.
"""
shift = round(nfft * overlap)
num_blocks = int((len(data) - nfft) / shift + 1)
specs = np.zeros((nfft / 2 + 1, num_blocks), dtype=np.float32)
window = hann(nfft)
for idx in range(num_blocks):
specs[:, idx] = np.abs(
np.fft.rfft(
data[idx * shift:idx * shift + nfft] * window,
n=nfft)) / nfft
if idx % 10 == 0:
self.send_message(
"loading_progress", {"progress": idx / num_blocks})
specs[:, -1] = np.abs(
np.fft.rfft(data[num_blocks * shift:], n=nfft)) / nfft
self.send_message("loading_progress", {"progress": 1})
return specs.T
if __name__ == "__main__":
import os
import webbrowser
from tornado.web import Application
from tornado.ioloop import IOLoop
import random
app = Application([("/spectrogram", SpectrogramWebSocket)])
random.seed()
port = random.randrange(49152, 65535)
app.listen(port)
webbrowser.open('file://{}/main.html?port={}'.format(os.getcwd(), port))
IOLoop.instance().start()