PySSTV 0.5.5__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.
- PySSTV-0.5.5.dist-info/LICENSE.txt +22 -0
- PySSTV-0.5.5.dist-info/METADATA +95 -0
- PySSTV-0.5.5.dist-info/RECORD +23 -0
- PySSTV-0.5.5.dist-info/WHEEL +5 -0
- PySSTV-0.5.5.dist-info/entry_points.txt +2 -0
- PySSTV-0.5.5.dist-info/top_level.txt +1 -0
- pysstv/__init__.py +3 -0
- pysstv/__main__.py +92 -0
- pysstv/color.py +205 -0
- pysstv/examples/__init__.py +3 -0
- pysstv/examples/codegen.py +167 -0
- pysstv/examples/get_floats.py +22 -0
- pysstv/examples/get_freq_bits.py +23 -0
- pysstv/examples/gimp-plugin.py +253 -0
- pysstv/examples/overlay.py +17 -0
- pysstv/examples/pyaudio_sstv.py +51 -0
- pysstv/examples/repeater.py +73 -0
- pysstv/grayscale.py +40 -0
- pysstv/sstv.py +136 -0
- pysstv/tests/__init__.py +3 -0
- pysstv/tests/common.py +9 -0
- pysstv/tests/test_color.py +44 -0
- pysstv/tests/test_sstv.py +79 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2013 Andras Veres-Szentkiralyi
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
|
4
|
+
obtaining a copy of this software and associated documentation
|
|
5
|
+
files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use,
|
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the
|
|
9
|
+
Software is furnished to do so, subject to the following
|
|
10
|
+
conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be
|
|
13
|
+
included in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: PySSTV
|
|
3
|
+
Version: 0.5.5
|
|
4
|
+
Summary: Python classes for generating Slow-scan Television transmissions
|
|
5
|
+
Home-page: https://github.com/dnet/pySSTV
|
|
6
|
+
Author: András Veres-Szentkirályi
|
|
7
|
+
Author-email: vsza@vsza.hu
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: HAM SSTV slow-scan television Scottie Martin Robot Pasokon
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Topic :: Communications :: Ham Radio
|
|
12
|
+
Classifier: Topic :: Multimedia :: Video :: Conversion
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE.txt
|
|
17
|
+
Requires-Dist: Pillow
|
|
18
|
+
|
|
19
|
+
SSTV generator in pure Python
|
|
20
|
+
=============================
|
|
21
|
+
|
|
22
|
+
PySSTV generates SSTV modulated WAV files from any image that PIL can open
|
|
23
|
+
(PNG, JPEG, GIF, and many others). These WAV files then can be played by any
|
|
24
|
+
audio player connected to a shortwave radio for example.
|
|
25
|
+
|
|
26
|
+
My main motivation was to understand the internals of SSTV in practice, so
|
|
27
|
+
performance is far from optimal. I tried keeping the code readable, and only
|
|
28
|
+
performed such optimizations that wouldn't have complicated the codebase.
|
|
29
|
+
|
|
30
|
+
Command line usage
|
|
31
|
+
------------------
|
|
32
|
+
|
|
33
|
+
$ python -m pysstv -h
|
|
34
|
+
usage: __main__.py [-h]
|
|
35
|
+
[--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120,PD160,PD180,PD240,PD290,Robot8BW,Robot24BW}]
|
|
36
|
+
[--rate RATE] [--bits BITS] [--vox] [--fskid FSKID]
|
|
37
|
+
[--chan CHAN]
|
|
38
|
+
image.png output.wav
|
|
39
|
+
|
|
40
|
+
Converts an image to an SSTV modulated WAV file.
|
|
41
|
+
|
|
42
|
+
positional arguments:
|
|
43
|
+
image.png input image file name
|
|
44
|
+
output.wav output WAV file name
|
|
45
|
+
|
|
46
|
+
optional arguments:
|
|
47
|
+
-h, --help show this help message and exit
|
|
48
|
+
--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120D160,PD180,PD240,Robot8BW,Robot24BW}
|
|
49
|
+
image mode (default: Martin M1)
|
|
50
|
+
--rate RATE sampling rate (default: 48000)
|
|
51
|
+
--bits BITS bits per sample (default: 16)
|
|
52
|
+
--vox add VOX tones at the beginning
|
|
53
|
+
--fskid FSKID add FSKID at the end
|
|
54
|
+
--chan CHAN number of channels (default: mono)
|
|
55
|
+
|
|
56
|
+
Python interface
|
|
57
|
+
----------------
|
|
58
|
+
|
|
59
|
+
The `SSTV` class in the `sstv` module implements basic SSTV-related
|
|
60
|
+
functionality, and the classes of other modules such as `grayscale` and
|
|
61
|
+
`color` extend this. Most instances implement the following methods:
|
|
62
|
+
|
|
63
|
+
- `__init__` takes a PIL image, the samples per second, and the bits per
|
|
64
|
+
sample as a parameter, but doesn't perform any hard calculations
|
|
65
|
+
- `gen_freq_bits` generates tuples that describe a sine wave segment with
|
|
66
|
+
frequency in Hz and duration in ms
|
|
67
|
+
- `gen_values` generates samples between -1 and +1, performing sampling
|
|
68
|
+
according to the samples per second value given during construction
|
|
69
|
+
- `gen_samples` generates discrete samples, performing quantization
|
|
70
|
+
according to the bits per sample value given during construction
|
|
71
|
+
- `write_wav` writes the whole image to a Microsoft WAV file
|
|
72
|
+
|
|
73
|
+
The above methods all build upon those above them, for example `write_wav`
|
|
74
|
+
calls `gen_samples`, while latter calls `gen_values`, so typically, only
|
|
75
|
+
the first and the last, maybe the last two should be called directly, the
|
|
76
|
+
others are just listed here for the sake of completeness and to make the
|
|
77
|
+
flow easier to understand.
|
|
78
|
+
|
|
79
|
+
License
|
|
80
|
+
-------
|
|
81
|
+
|
|
82
|
+
The whole project is available under MIT license.
|
|
83
|
+
|
|
84
|
+
Useful links
|
|
85
|
+
------------
|
|
86
|
+
|
|
87
|
+
- receive-only "counterpart": https://github.com/windytan/slowrx
|
|
88
|
+
- free SSTV handbook: http://www.sstv-handbook.com/
|
|
89
|
+
- robot 36 encoder/decoder in C: https://github.com/xdsopl/robot36/
|
|
90
|
+
|
|
91
|
+
Dependencies
|
|
92
|
+
------------
|
|
93
|
+
|
|
94
|
+
- Python 3.5 or later
|
|
95
|
+
- Python Imaging Library (Debian/Ubuntu package: `python3-pil`)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
pysstv/__init__.py,sha256=v25SUBx1XfOlGVF9AQfXaLhs3tfIrfqHMUFJuPYjFBk,85
|
|
2
|
+
pysstv/__main__.py,sha256=npynfJzxvB22YQkznDluVwLGqsU6qkKqelX2y5F6b5M,3726
|
|
3
|
+
pysstv/color.py,sha256=xR_yJR3spFVIJ6ZMgVpHlITqdMih1c7_uPYg_ifYChM,5396
|
|
4
|
+
pysstv/grayscale.py,sha256=sKYj7pR9mzhekiwp_16aBysUWuDe5woLaAHZsIM0i3k,897
|
|
5
|
+
pysstv/sstv.py,sha256=5GflGizPnthvbt6c5r4WEROniLgYqUdDTG2zyuzatLc,4354
|
|
6
|
+
pysstv/examples/__init__.py,sha256=eYhYvkC9jXkgZDJD1V1jffApef6mXZM18M1CusgfM_Q,73
|
|
7
|
+
pysstv/examples/codegen.py,sha256=BFTX3virfnrjKll99JbVVLMRk23B6iOv_bnUleTTJqc,5936
|
|
8
|
+
pysstv/examples/get_floats.py,sha256=NT3NE2L9xAZCM0W3cChd0bDr39YuVTpda9Rzhi5wReE,559
|
|
9
|
+
pysstv/examples/get_freq_bits.py,sha256=yctUehsXgshxQ_vd27c9jxzBaUMV0N6JYc98NBD9uxg,622
|
|
10
|
+
pysstv/examples/gimp-plugin.py,sha256=n5exX47Gklq6aKtWQfk3YnANiZV0lcHtRfrMP0gKGkY,8073
|
|
11
|
+
pysstv/examples/overlay.py,sha256=FSldscCI9sTFwrCe-Nl0y6cC346kx_GZ7G2uMI0VL7k,533
|
|
12
|
+
pysstv/examples/pyaudio_sstv.py,sha256=3gAqRTKRd-_4U2oiPvUMG5Wd8vW5olMhYs0pFWfaA1k,1408
|
|
13
|
+
pysstv/examples/repeater.py,sha256=W1t0yOr-YRSK9sO5HVw8l_DqTg-xj7mHaWbGxbeWZok,2183
|
|
14
|
+
pysstv/tests/__init__.py,sha256=liZwqN9naCqnQviykDjSLoogdvPUP14wXtvAvaeylvM,71
|
|
15
|
+
pysstv/tests/common.py,sha256=EaO7eJQ4YvO0b70I3WdWk79G6TX90Gbuezcvbo3-5mA,262
|
|
16
|
+
pysstv/tests/test_color.py,sha256=MJomuWvuF3fTroThwAcdyFiFavt8_j8b7yrKQLvl1Hk,1527
|
|
17
|
+
pysstv/tests/test_sstv.py,sha256=SDkzdbIMpaeWfkqzZuh7k3pHyc6-cLV8tSbxLLvraec,2807
|
|
18
|
+
PySSTV-0.5.5.dist-info/LICENSE.txt,sha256=3QTt5CrsmuUm5urLjdgxk2aHj9T_KreABJzDefftcQc,1069
|
|
19
|
+
PySSTV-0.5.5.dist-info/METADATA,sha256=1bxesFyRZY_G-L7DwHcRNZUhFuFPv16zNXvgmVLyWm8,3853
|
|
20
|
+
PySSTV-0.5.5.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
|
21
|
+
PySSTV-0.5.5.dist-info/entry_points.txt,sha256=o1t-uDYe9lprOoofUauxJAcSUth5ImaV2d0W2NI7Yfw,48
|
|
22
|
+
PySSTV-0.5.5.dist-info/top_level.txt,sha256=PrRHl9SFiJU71r-_iI4MnyK62MkbuLghNLAlxW2jMWM,7
|
|
23
|
+
PySSTV-0.5.5.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pysstv
|
pysstv/__init__.py
ADDED
pysstv/__main__.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
from __future__ import print_function, division
|
|
4
|
+
from PIL import Image
|
|
5
|
+
from argparse import ArgumentParser
|
|
6
|
+
from sys import stderr
|
|
7
|
+
from pysstv import color, grayscale
|
|
8
|
+
|
|
9
|
+
SSTV_MODULES = [color, grayscale]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
module_map = build_module_map()
|
|
14
|
+
parser = ArgumentParser(
|
|
15
|
+
description='Converts an image to an SSTV modulated WAV file.')
|
|
16
|
+
parser.add_argument('img_file', metavar='image.png',
|
|
17
|
+
help='input image file name')
|
|
18
|
+
parser.add_argument('wav_file', metavar='output.wav',
|
|
19
|
+
help='output WAV file name')
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
'--mode', dest='mode', default='MartinM1', choices=module_map,
|
|
22
|
+
help='image mode (default: Martin M1)')
|
|
23
|
+
parser.add_argument('--rate', dest='rate', type=int, default=48000,
|
|
24
|
+
help='sampling rate (default: 48000)')
|
|
25
|
+
parser.add_argument('--bits', dest='bits', type=int, default=16,
|
|
26
|
+
help='bits per sample (default: 16)')
|
|
27
|
+
parser.add_argument('--vox', dest='vox', action='store_true',
|
|
28
|
+
help='add VOX tones at the beginning')
|
|
29
|
+
parser.add_argument('--fskid', dest='fskid',
|
|
30
|
+
help='add FSKID at the end')
|
|
31
|
+
parser.add_argument('--chan', dest='chan', type=int,
|
|
32
|
+
help='number of channels (default: mono)')
|
|
33
|
+
parser.add_argument('--resize', dest='resize', action='store_true',
|
|
34
|
+
help='resize the image to the correct size')
|
|
35
|
+
parser.add_argument('--keep-aspect-ratio', dest='keep_aspect_ratio', action='store_true',
|
|
36
|
+
help='keep the original aspect ratio when resizing (and cut off excess pixels)')
|
|
37
|
+
parser.add_argument('--resample', dest='resample', default='lanczos',
|
|
38
|
+
choices=('nearest', 'bicubic', 'lanczos'),
|
|
39
|
+
help='which resampling filter to use for resizing (see Pillow documentation)')
|
|
40
|
+
args = parser.parse_args()
|
|
41
|
+
image = Image.open(args.img_file)
|
|
42
|
+
mode = module_map[args.mode]
|
|
43
|
+
if args.resize and any(i != m for i, m in zip(image.size, (mode.WIDTH, mode.HEIGHT))):
|
|
44
|
+
resample = getattr(Image, args.resample.upper())
|
|
45
|
+
if args.keep_aspect_ratio:
|
|
46
|
+
orig_ratio = image.width / image.height
|
|
47
|
+
mode_ratio = mode.WIDTH / mode.HEIGHT
|
|
48
|
+
crop = orig_ratio != mode_ratio
|
|
49
|
+
else:
|
|
50
|
+
crop = False
|
|
51
|
+
if crop:
|
|
52
|
+
if orig_ratio < mode_ratio:
|
|
53
|
+
w = mode.WIDTH
|
|
54
|
+
h = int(w / orig_ratio)
|
|
55
|
+
else:
|
|
56
|
+
h = mode.HEIGHT
|
|
57
|
+
w = int(orig_ratio * h)
|
|
58
|
+
else:
|
|
59
|
+
w = mode.WIDTH
|
|
60
|
+
h = mode.HEIGHT
|
|
61
|
+
image = image.resize((w, h), resample)
|
|
62
|
+
if crop:
|
|
63
|
+
x = (image.width - mode.WIDTH) / 2
|
|
64
|
+
y = (image.height - mode.HEIGHT) / 2
|
|
65
|
+
image = image.crop((x, y, mode.WIDTH + x, mode.HEIGHT + y))
|
|
66
|
+
elif not all(i >= m for i, m in zip(image.size, (mode.WIDTH, mode.HEIGHT))):
|
|
67
|
+
print(('Image must be at least {m.WIDTH} x {m.HEIGHT} pixels '
|
|
68
|
+
'for mode {m.__name__}').format(m=mode), file=stderr)
|
|
69
|
+
raise SystemExit(1)
|
|
70
|
+
s = mode(image, args.rate, args.bits)
|
|
71
|
+
s.vox_enabled = args.vox
|
|
72
|
+
if args.fskid:
|
|
73
|
+
s.add_fskid_text(args.fskid)
|
|
74
|
+
if args.chan:
|
|
75
|
+
s.nchannels = args.chan
|
|
76
|
+
s.write_wav(args.wav_file)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_module_map():
|
|
80
|
+
try:
|
|
81
|
+
from collections import OrderedDict
|
|
82
|
+
module_map = OrderedDict()
|
|
83
|
+
except ImportError:
|
|
84
|
+
module_map = {}
|
|
85
|
+
for module in SSTV_MODULES:
|
|
86
|
+
for mode in module.MODES:
|
|
87
|
+
module_map[mode.__name__] = mode
|
|
88
|
+
return module_map
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == '__main__':
|
|
92
|
+
main()
|
pysstv/color.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from __future__ import division
|
|
3
|
+
from pysstv.sstv import byte_to_freq, FREQ_BLACK, FREQ_WHITE, FREQ_VIS_START
|
|
4
|
+
from pysstv.grayscale import GrayscaleSSTV
|
|
5
|
+
from itertools import chain
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Color(Enum):
|
|
10
|
+
red = 0
|
|
11
|
+
green = 1
|
|
12
|
+
blue = 2
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ColorSSTV(GrayscaleSSTV):
|
|
16
|
+
def on_init(self):
|
|
17
|
+
self.pixels = self.image.convert('RGB').load()
|
|
18
|
+
|
|
19
|
+
def encode_line(self, line):
|
|
20
|
+
msec_pixel = self.SCAN / self.WIDTH
|
|
21
|
+
image = self.pixels
|
|
22
|
+
for color in self.COLOR_SEQ:
|
|
23
|
+
yield from self.before_channel(color)
|
|
24
|
+
for col in range(self.WIDTH):
|
|
25
|
+
pixel = image[col, line]
|
|
26
|
+
freq_pixel = byte_to_freq(pixel[color.value])
|
|
27
|
+
yield freq_pixel, msec_pixel
|
|
28
|
+
yield from self.after_channel(color)
|
|
29
|
+
|
|
30
|
+
def before_channel(self, color):
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
after_channel = before_channel
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MartinM1(ColorSSTV):
|
|
37
|
+
COLOR_SEQ = (Color.green, Color.blue, Color.red)
|
|
38
|
+
VIS_CODE = 0x2c
|
|
39
|
+
WIDTH = 320
|
|
40
|
+
HEIGHT = 256
|
|
41
|
+
SYNC = 4.862
|
|
42
|
+
SCAN = 146.432
|
|
43
|
+
INTER_CH_GAP = 0.572
|
|
44
|
+
|
|
45
|
+
def before_channel(self, color):
|
|
46
|
+
if color is Color.green:
|
|
47
|
+
yield FREQ_BLACK, self.INTER_CH_GAP
|
|
48
|
+
|
|
49
|
+
def after_channel(self, color):
|
|
50
|
+
yield FREQ_BLACK, self.INTER_CH_GAP
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MartinM2(MartinM1):
|
|
54
|
+
VIS_CODE = 0x28
|
|
55
|
+
WIDTH = 160
|
|
56
|
+
SCAN = 73.216
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ScottieS1(MartinM1):
|
|
60
|
+
VIS_CODE = 0x3c
|
|
61
|
+
SYNC = 9
|
|
62
|
+
INTER_CH_GAP = 1.5
|
|
63
|
+
SCAN = 138.24 - INTER_CH_GAP
|
|
64
|
+
|
|
65
|
+
def horizontal_sync(self):
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
def before_channel(self, color):
|
|
69
|
+
if color is Color.red:
|
|
70
|
+
yield from MartinM1.horizontal_sync(self)
|
|
71
|
+
yield FREQ_BLACK, self.INTER_CH_GAP
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ScottieS2(ScottieS1):
|
|
75
|
+
VIS_CODE = 0x38
|
|
76
|
+
SCAN = 88.064 - ScottieS1.INTER_CH_GAP
|
|
77
|
+
WIDTH = 160
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Robot36(ColorSSTV):
|
|
81
|
+
VIS_CODE = 0x08
|
|
82
|
+
WIDTH = 320
|
|
83
|
+
HEIGHT = 240
|
|
84
|
+
SYNC = 9
|
|
85
|
+
INTER_CH_GAP = 4.5
|
|
86
|
+
Y_SCAN = 88
|
|
87
|
+
C_SCAN = 44
|
|
88
|
+
PORCH = 1.5
|
|
89
|
+
SYNC_PORCH = 3
|
|
90
|
+
INTER_CH_FREQS = [None, FREQ_WHITE, FREQ_BLACK]
|
|
91
|
+
|
|
92
|
+
def on_init(self):
|
|
93
|
+
self.yuv = self.image.convert('YCbCr').load()
|
|
94
|
+
|
|
95
|
+
def encode_line(self, line):
|
|
96
|
+
pixels = [self.yuv[col, line] for col in range(self.WIDTH)]
|
|
97
|
+
channel = 2 - (line % 2)
|
|
98
|
+
y_pixel_time = self.Y_SCAN / self.WIDTH
|
|
99
|
+
uv_pixel_time = self.C_SCAN / self.WIDTH
|
|
100
|
+
return chain(
|
|
101
|
+
[(FREQ_BLACK, self.SYNC_PORCH)],
|
|
102
|
+
((byte_to_freq(p[0]), y_pixel_time) for p in pixels),
|
|
103
|
+
[(self.INTER_CH_FREQS[channel], self.INTER_CH_GAP),
|
|
104
|
+
(FREQ_VIS_START, self.PORCH)],
|
|
105
|
+
((byte_to_freq(p[channel]), uv_pixel_time) for p in pixels))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PasokonP3(ColorSSTV):
|
|
109
|
+
"""
|
|
110
|
+
[ VIS code or horizontal sync here ]
|
|
111
|
+
Back porch - 5 time units of black (1500 Hz).
|
|
112
|
+
Red component - 640 pixels of 1 time unit each.
|
|
113
|
+
Gap - 5 time units of black.
|
|
114
|
+
Green component - 640 pixels of 1 time unit each.
|
|
115
|
+
Gap - 5 time units of black.
|
|
116
|
+
Blue component - 640 pixels of 1 time unit each.
|
|
117
|
+
Front porch - 5 time units of black.
|
|
118
|
+
Horizontal Sync - 25 time units of 1200 Hz.
|
|
119
|
+
"""
|
|
120
|
+
TIMEUNIT = 1000/4800. # ms
|
|
121
|
+
COLOR_SEQ = (Color.red, Color.green, Color.blue)
|
|
122
|
+
VIS_CODE = 0x71
|
|
123
|
+
WIDTH = 640
|
|
124
|
+
HEIGHT = 480+16
|
|
125
|
+
SYNC = 25 * TIMEUNIT
|
|
126
|
+
SCAN = WIDTH * TIMEUNIT
|
|
127
|
+
INTER_CH_GAP = 5 * TIMEUNIT
|
|
128
|
+
|
|
129
|
+
def before_channel(self, color):
|
|
130
|
+
if color is Color.red:
|
|
131
|
+
yield FREQ_BLACK, self.INTER_CH_GAP
|
|
132
|
+
|
|
133
|
+
def after_channel(self, color):
|
|
134
|
+
yield FREQ_BLACK, self.INTER_CH_GAP
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PasokonP5(PasokonP3):
|
|
138
|
+
TIMEUNIT = 1000/3200. # ms
|
|
139
|
+
VIS_CODE = 0x72
|
|
140
|
+
SYNC = 25 * TIMEUNIT
|
|
141
|
+
SCAN = PasokonP3.WIDTH * TIMEUNIT
|
|
142
|
+
INTER_CH_GAP = 5 * TIMEUNIT
|
|
143
|
+
|
|
144
|
+
class PasokonP7(PasokonP3):
|
|
145
|
+
TIMEUNIT = 1000/2400. # ms
|
|
146
|
+
VIS_CODE = 0xF3
|
|
147
|
+
SYNC = 25 * TIMEUNIT
|
|
148
|
+
SCAN = PasokonP3.WIDTH * TIMEUNIT
|
|
149
|
+
INTER_CH_GAP = 5 * TIMEUNIT
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PD90(ColorSSTV):
|
|
153
|
+
VIS_CODE = 0x63
|
|
154
|
+
WIDTH = 320
|
|
155
|
+
HEIGHT = 256
|
|
156
|
+
SYNC = 20
|
|
157
|
+
PORCH = 2.08
|
|
158
|
+
PIXEL = 0.532
|
|
159
|
+
|
|
160
|
+
def gen_image_tuples(self):
|
|
161
|
+
yuv = self.image.convert('YCbCr').load()
|
|
162
|
+
for line in range(0, self.HEIGHT, 2):
|
|
163
|
+
yield from self.horizontal_sync()
|
|
164
|
+
yield FREQ_BLACK, self.PORCH
|
|
165
|
+
pixels0 = [yuv[col, line] for col in range(self.WIDTH)]
|
|
166
|
+
pixels1 = [yuv[col, line + 1] for col in range(self.WIDTH)]
|
|
167
|
+
for p in pixels0:
|
|
168
|
+
yield byte_to_freq(p[0]), self.PIXEL
|
|
169
|
+
for p0, p1 in zip(pixels0, pixels1):
|
|
170
|
+
yield byte_to_freq((p0[2] + p1[2]) / 2), self.PIXEL
|
|
171
|
+
for p0, p1 in zip(pixels0, pixels1):
|
|
172
|
+
yield byte_to_freq((p0[1] + p1[1]) / 2), self.PIXEL
|
|
173
|
+
for p in pixels1:
|
|
174
|
+
yield byte_to_freq(p[0]), self.PIXEL
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class PD120(PD90):
|
|
178
|
+
VIS_CODE = 0x5f
|
|
179
|
+
WIDTH = 640
|
|
180
|
+
HEIGHT = 496
|
|
181
|
+
PIXEL = 0.19
|
|
182
|
+
|
|
183
|
+
class PD160(PD90):
|
|
184
|
+
VIS_CODE = 0x62
|
|
185
|
+
WIDTH = 512
|
|
186
|
+
HEIGHT = 400
|
|
187
|
+
PIXEL = 0.382
|
|
188
|
+
|
|
189
|
+
class PD180(PD120):
|
|
190
|
+
VIS_CODE = 0x60
|
|
191
|
+
PIXEL = 0.286
|
|
192
|
+
|
|
193
|
+
class PD240(PD120):
|
|
194
|
+
VIS_CODE = 0x61
|
|
195
|
+
PIXEL = 0.382
|
|
196
|
+
|
|
197
|
+
class PD290(PD240):
|
|
198
|
+
VIS_CODE = 0x5e
|
|
199
|
+
WIDTH = 800
|
|
200
|
+
HEIGHT = 616
|
|
201
|
+
PIXEL = 0.286
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
MODES = (MartinM1, MartinM2, ScottieS1, ScottieS2, Robot36,
|
|
205
|
+
PasokonP3, PasokonP5, PasokonP7, PD90, PD120, PD160, PD180, PD240, PD290)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
class Image(object):
|
|
4
|
+
def __init__(self, content):
|
|
5
|
+
self.content = content
|
|
6
|
+
|
|
7
|
+
def load(self):
|
|
8
|
+
return self
|
|
9
|
+
|
|
10
|
+
def __getitem__(self, item):
|
|
11
|
+
if isinstance(item, tuple):
|
|
12
|
+
x, y = item
|
|
13
|
+
return Image('{0}[(ROW({1}) + COL({2})) * 3'.format(self.content, y, x))
|
|
14
|
+
elif isinstance(item, int):
|
|
15
|
+
return Image('{0} + RGB({1})]'.format(self.content, item))
|
|
16
|
+
else:
|
|
17
|
+
raise NotImplementedError()
|
|
18
|
+
|
|
19
|
+
def __rmul__(self, n):
|
|
20
|
+
return Image('({1} * {0})'.format(self.content, float(n)))
|
|
21
|
+
|
|
22
|
+
def __mul__(self, n):
|
|
23
|
+
return Image('({0} * {1})'.format(self.content, float(n)))
|
|
24
|
+
|
|
25
|
+
def __rtruediv__(self, n):
|
|
26
|
+
return Image('({1} / {0})'.format(self.content, n))
|
|
27
|
+
|
|
28
|
+
def __truediv__(self, n):
|
|
29
|
+
return Image('({0} / {1})'.format(self.content, n))
|
|
30
|
+
|
|
31
|
+
def __radd__(self, n):
|
|
32
|
+
return Image('({1} + {0})'.format(self.content, n))
|
|
33
|
+
|
|
34
|
+
def __add__(self, n):
|
|
35
|
+
return Image('({0} + {1})'.format(self.content, n))
|
|
36
|
+
|
|
37
|
+
def __str__(self):
|
|
38
|
+
return self.content
|
|
39
|
+
|
|
40
|
+
from pysstv.color import MartinM1, MartinM2, PasokonP3, PasokonP5, PasokonP7
|
|
41
|
+
import re
|
|
42
|
+
|
|
43
|
+
supported = [MartinM1, MartinM2, PasokonP3, PasokonP5, PasokonP7]
|
|
44
|
+
ROW_RE = re.compile(r'ROW\(\d+\)')
|
|
45
|
+
|
|
46
|
+
def main(sstv_class=None):
|
|
47
|
+
if sstv_class is None:
|
|
48
|
+
sstv_class = MartinM1
|
|
49
|
+
elif sstv_class not in supported:
|
|
50
|
+
raise NotImplementedError()
|
|
51
|
+
sstv = sstv_class(Image('img'), 44100, 16)
|
|
52
|
+
n = 0
|
|
53
|
+
yield '#define ROW(x) x'
|
|
54
|
+
yield '#define COL(x) x'
|
|
55
|
+
yield '#define RGB(x) x'
|
|
56
|
+
yield 'void convert(unsigned char *img, float *freqs, float *msecs, const int width) {\nint frq = 0;'
|
|
57
|
+
history = []
|
|
58
|
+
lut = {}
|
|
59
|
+
same_as = {}
|
|
60
|
+
for freq, msec in sstv.gen_freq_bits():
|
|
61
|
+
printed = 'freqs[frq] = {1}; msecs[frq++] = {2};'.format(n, freq, msec)
|
|
62
|
+
key = ROW_RE.sub('row', printed)
|
|
63
|
+
old = lut.get(key)
|
|
64
|
+
if old is not None:
|
|
65
|
+
same_as[n] = old
|
|
66
|
+
else:
|
|
67
|
+
lut[key] = n
|
|
68
|
+
history.append((printed, key))
|
|
69
|
+
n += 1
|
|
70
|
+
del lut
|
|
71
|
+
m_start, m_len = gen_matches(same_as, history, n)
|
|
72
|
+
for i in range(same_as[m_start]):
|
|
73
|
+
yield history[i][0]
|
|
74
|
+
yield 'for (int row = 0; row < width * {0}; row += width) {{'.format(sstv.HEIGHT)
|
|
75
|
+
for i in range(same_as[m_start], same_as[m_start] + m_len - 1):
|
|
76
|
+
yield ' ' + history[i][1]
|
|
77
|
+
yield '}'
|
|
78
|
+
yield '}}\n\n#define FREQ_COUNT {0}'.format(n)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def gen_matches(same_as, history, n):
|
|
83
|
+
cur_start = None
|
|
84
|
+
cur_len = None
|
|
85
|
+
cur_end = None
|
|
86
|
+
for i in range(n):
|
|
87
|
+
if cur_start is None:
|
|
88
|
+
tmp = same_as.get(i)
|
|
89
|
+
if tmp is not None:
|
|
90
|
+
cur_len = 1
|
|
91
|
+
cur_start = i
|
|
92
|
+
cur_end = tmp
|
|
93
|
+
else:
|
|
94
|
+
tmp = same_as.get(i)
|
|
95
|
+
if tmp is not None and history[tmp][1] == history[cur_end + 1][1] and cur_start > cur_end:
|
|
96
|
+
cur_len += 1
|
|
97
|
+
cur_end += 1
|
|
98
|
+
else:
|
|
99
|
+
if tmp is not None and history[tmp][1] == history[cur_end + 1][1]:
|
|
100
|
+
return cur_start, cur_len
|
|
101
|
+
tmp = same_as.get(i)
|
|
102
|
+
if tmp is None:
|
|
103
|
+
cur_start = None
|
|
104
|
+
else:
|
|
105
|
+
cur_len = 1
|
|
106
|
+
cur_start = i
|
|
107
|
+
cur_end = tmp
|
|
108
|
+
|
|
109
|
+
def test(img_file):
|
|
110
|
+
from subprocess import Popen, PIPE, check_output
|
|
111
|
+
from os import remove, path
|
|
112
|
+
from PIL import Image
|
|
113
|
+
from datetime import datetime
|
|
114
|
+
import struct
|
|
115
|
+
exe = './codegen-test-executable'
|
|
116
|
+
if not path.exists('stb_image.h'):
|
|
117
|
+
from urllib.request import urlretrieve
|
|
118
|
+
urlretrieve('https://raw.githubusercontent.com/nothings/stb/master/stb_image.h', 'stb_image.h')
|
|
119
|
+
try:
|
|
120
|
+
for sstv_class in supported:
|
|
121
|
+
print('Testing', sstv_class)
|
|
122
|
+
gcc = Popen(['gcc', '-xc', '-lm', '-o', exe, '-'], stdin=PIPE)
|
|
123
|
+
start = datetime.now()
|
|
124
|
+
with open(path.join(path.dirname(__file__), 'codeman.c')) as cm:
|
|
125
|
+
c_src = cm.read().replace('#include "codegen.c"', '\n'.join(main(sstv_class)))
|
|
126
|
+
gcc.communicate(c_src)
|
|
127
|
+
gen_elapsed = datetime.now() - start
|
|
128
|
+
print(' - gengcc took', gen_elapsed)
|
|
129
|
+
start = datetime.now()
|
|
130
|
+
gen = check_output([exe, img_file])
|
|
131
|
+
native_elapsed = datetime.now() - start
|
|
132
|
+
print(' - native took', native_elapsed)
|
|
133
|
+
img = Image.open(img_file)
|
|
134
|
+
sstv = sstv_class(img, 44100, 16)
|
|
135
|
+
start = datetime.now()
|
|
136
|
+
try:
|
|
137
|
+
for n, (freq, msec) in enumerate(sstv.gen_freq_bits()):
|
|
138
|
+
assert gen[n * 8:(n + 1) * 8] == struct.pack('ff', freq, msec)
|
|
139
|
+
except AssertionError:
|
|
140
|
+
mode_name = sstv_class.__name__
|
|
141
|
+
with open('/tmp/{0}-c.bin'.format(mode_name), 'wb') as f:
|
|
142
|
+
f.write(gen)
|
|
143
|
+
with open('/tmp/{0}-py.bin'.format(mode_name), 'wb') as f:
|
|
144
|
+
for n, (freq, msec) in enumerate(sstv.gen_freq_bits()):
|
|
145
|
+
f.write(struct.pack('ff', freq, msec))
|
|
146
|
+
with open('/tmp/{0}.c'.format(mode_name), 'w') as f:
|
|
147
|
+
f.write(c_src)
|
|
148
|
+
print((" ! Outputs are different, they've been saved to "
|
|
149
|
+
"/tmp/{0}-{{c,py}}.bin, along with the C source code "
|
|
150
|
+
"in /tmp/{0}.c").format(mode_name))
|
|
151
|
+
python_elapsed = datetime.now() - start
|
|
152
|
+
print(' - python took', python_elapsed)
|
|
153
|
+
print(' - speedup:', python_elapsed.total_seconds() / native_elapsed.total_seconds())
|
|
154
|
+
print('OK')
|
|
155
|
+
finally:
|
|
156
|
+
try:
|
|
157
|
+
remove(exe)
|
|
158
|
+
except OSError:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == '__main__':
|
|
163
|
+
from sys import argv
|
|
164
|
+
if len(argv) > 2 and argv[1] == 'test':
|
|
165
|
+
test(argv[2])
|
|
166
|
+
else:
|
|
167
|
+
print('\n'.join(main()))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
This example streams the raw floating point samples to stdout in 4-byte
|
|
5
|
+
single precision format, so that it can be processed outside PySSTV.
|
|
6
|
+
|
|
7
|
+
Usage example: get_floats.py | play -r 44100 -t f32 -c 1 --norm -
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from PIL import Image
|
|
11
|
+
from pysstv.grayscale import Robot8BW
|
|
12
|
+
import struct, sys
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
img = Image.open("160x120bw.png")
|
|
16
|
+
sstv = Robot8BW(img, 44100, 16)
|
|
17
|
+
sstv.vox_enabled = True
|
|
18
|
+
for value in sstv.gen_values():
|
|
19
|
+
sys.stdout.write(struct.pack('f', value))
|
|
20
|
+
|
|
21
|
+
if __name__ == '__main__':
|
|
22
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
This example streams the raw floating point (freq, msec) tuples to stdout
|
|
5
|
+
in 4-byte single precision format (8 bytes per tuple), so that it can be
|
|
6
|
+
processed outside PySSTV.
|
|
7
|
+
|
|
8
|
+
Usage example using unixsstv/gen_values:
|
|
9
|
+
get_freq_bits.py | gen_values 44100 | play -r 44100 -t f32 -c 1 --norm -
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from PIL import Image
|
|
13
|
+
from pysstv.color import MartinM1
|
|
14
|
+
import struct, sys
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
img = Image.open("320x256rgb.png")
|
|
18
|
+
sstv = MartinM1(img, 44100, 16)
|
|
19
|
+
for freq, msec in sstv.gen_freq_bits():
|
|
20
|
+
sys.stdout.write(struct.pack('ff', freq, msec))
|
|
21
|
+
|
|
22
|
+
if __name__ == '__main__':
|
|
23
|
+
main()
|