|
@@ -0,0 +1,99 @@
|
|
1
|
+#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+# Render image to Oscilloscope XY vector audio
|
|
4
|
+#
|
|
5
|
+# ----------------------------------------------------------------------------
|
|
6
|
+# Copyright (c) 2024 Thomas Buck (thomas@xythobuz.de)
|
|
7
|
+#
|
|
8
|
+# This program is free software: you can redistribute it and/or modify
|
|
9
|
+# it under the terms of the GNU General Public License as published by
|
|
10
|
+# the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+# (at your option) any later version.
|
|
12
|
+#
|
|
13
|
+# This program is distributed in the hope that it will be useful,
|
|
14
|
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+# GNU General Public License for more details.
|
|
17
|
+#
|
|
18
|
+# See <http://www.gnu.org/licenses/>.
|
|
19
|
+# ----------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+import sys
|
|
22
|
+import wave
|
|
23
|
+
|
|
24
|
+import pyaudio
|
|
25
|
+from PIL import Image
|
|
26
|
+
|
|
27
|
+samplerate = 44100 #192000
|
|
28
|
+default_duration = 5.0
|
|
29
|
+
|
|
30
|
+def read_image(image):
|
|
31
|
+ print("image: width={} height={} total={}".format(image.width, image.height, image.width * image.height))
|
|
32
|
+
|
|
33
|
+ # resize coordinates for conversion to amplitude values
|
|
34
|
+ max_len = max(image.width, image.height)
|
|
35
|
+ fact = 32767 / max_len
|
|
36
|
+ sw, sh = int(image.width * fact), int(image.height * fact)
|
|
37
|
+ print("amplitude: width={} height={}".format(sw, sh))
|
|
38
|
+
|
|
39
|
+ data = bytearray()
|
|
40
|
+ for x in range(0, image.width):
|
|
41
|
+ for y in range(0, image.height):
|
|
42
|
+ if image.getpixel((x, y))[3] > 127:
|
|
43
|
+ xc, yc = int((x - (image.width / 2)) * fact), int((y - (image.height / 2)) * fact)
|
|
44
|
+ data.extend(yc.to_bytes(2, byteorder="little", signed=True))
|
|
45
|
+ data.extend(xc.to_bytes(2, byteorder="little", signed=True))
|
|
46
|
+ return data
|
|
47
|
+
|
|
48
|
+def play_waveform(data):
|
|
49
|
+ pa = pyaudio.PyAudio()
|
|
50
|
+
|
|
51
|
+ # int16
|
|
52
|
+ stream = pa.open(format=pa.get_format_from_width(2),
|
|
53
|
+ channels=2,
|
|
54
|
+ rate=samplerate,
|
|
55
|
+ output=True)
|
|
56
|
+
|
|
57
|
+ stream.write(data, int(len(data) / 4), True)
|
|
58
|
+
|
|
59
|
+ stream.stop_stream()
|
|
60
|
+ stream.close()
|
|
61
|
+
|
|
62
|
+ pa.terminate()
|
|
63
|
+
|
|
64
|
+def write_waveform(data):
|
|
65
|
+ with wave.open("out.wav", "w") as f:
|
|
66
|
+ f.setnchannels(2)
|
|
67
|
+ f.setsampwidth(2)
|
|
68
|
+ f.setframerate(samplerate)
|
|
69
|
+ f.writeframes(data)
|
|
70
|
+
|
|
71
|
+def main():
|
|
72
|
+ if len(sys.argv) <= 1:
|
|
73
|
+ print("Usage:")
|
|
74
|
+ print("\t" + sys.argv[0] + " image.png [seconds]")
|
|
75
|
+ sys.exit(1)
|
|
76
|
+
|
|
77
|
+ if len(sys.argv) >= 3:
|
|
78
|
+ duration = float(sys.argv[2])
|
|
79
|
+ else:
|
|
80
|
+ duration = default_duration
|
|
81
|
+
|
|
82
|
+ with Image.open(sys.argv[1]) as image:
|
|
83
|
+ wave = read_image(image)
|
|
84
|
+
|
|
85
|
+ samplecount = int(len(wave) / 2 / 2) # stereo, int16
|
|
86
|
+ drawrate = samplerate / samplecount
|
|
87
|
+ drawcount = drawrate * duration
|
|
88
|
+ print("len={} samples={} drawrate={:.2f} count={:.2f}".format(len(wave), samplecount, drawrate, drawcount))
|
|
89
|
+
|
|
90
|
+ data = bytearray()
|
|
91
|
+ for n in range(0, int(drawcount)):
|
|
92
|
+ data.extend(wave)
|
|
93
|
+ print("len={}".format(len(data)))
|
|
94
|
+
|
|
95
|
+ #play_waveform(bytes(data))
|
|
96
|
+ write_waveform(bytes(data))
|
|
97
|
+
|
|
98
|
+if __name__ == "__main__":
|
|
99
|
+ main()
|