-
Notifications
You must be signed in to change notification settings - Fork 0
/
uef_file_builder.py
151 lines (120 loc) · 4.49 KB
/
uef_file_builder.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
# -*- coding: utf-8 -*-
# uef_file_builder.py
#
# The Python script in this file produces UEF representations of Acorn tapes.
#
# Copyright (C) 2022-2024 Dominic Ford <https://dcford.org.uk/>
#
# This code is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# You should have received a copy of the GNU General Public License along with
# this file; if not, write to the Free Software Foundation, Inc., 51 Franklin
# Street, Fifth Floor, Boston, MA 02110-1301, USA
# ----------------------------------------------------------------------------
"""
Compile and write UEF files used by Acorn emulators such as BeebEm and JSBeeb.
"""
import gzip
class UefFileBuilder:
"""
Class to compile and write UEF files used by Acorn emulators such as BeebEm and JSBeeb.
"""
def __init__(self):
"""
Class to compile and write UEF files used by Acorn emulators such as BeebEm and JSBeeb.
"""
# First compile output into a buffer
self.output = bytearray()
# Baud rate (used to convert time intervals into baud units)
self.baud_rate = 1200
# Write file header
self.output.extend("UEF File!\0".encode("ascii")) # ID header
self.output.append(5) # UEF format major version
self.output.append(0) # UEF format minor version
# Write origin block
origin_string = "acorn-tape-reader <https://github.com/dcf21/acorn-tape-reader>\0"
self._write_chunk_header(chunk_type=0, length=len(origin_string))
self.output.extend(origin_string.encode("ascii"))
def write_to_file(self, filename: str) -> None:
"""
Write UEF file to disk.
:param:
Filename for output UEF file.
:return:
None
"""
# Write binary file
with gzip.open(filename, "wb") as f_out:
f_out.write(self.output)
def _write_chunk_header(self, chunk_type: int, length: int) -> None:
"""
Write the header at the start of a new data chunk.
:param chunk_type:
Numerical chunk type
:param length:
Length of chunk, excluding header [bytes]
:return:
None
"""
# Chunk ID
self.output.append(chunk_type & 0xFF)
self.output.append((chunk_type >> 8) & 0xFF)
# Chunk length
self.output.append(length & 0xFF)
self.output.append((length >> 8) & 0xFF)
self.output.append((length >> 16) & 0xFF)
self.output.append((length >> 24) & 0xFF)
def add_silence(self, duration: float) -> None:
"""
Add a period of silence to the output.
:param duration:
Duration of silence [sec]
:return:
None
"""
# Convert duration into units of baud
duration_cycles = int(duration * (self.baud_rate * 2))
if duration_cycles < 1:
duration_cycles = 1
if duration_cycles > 0xFFFF:
duration_cycles = 0xFFFF
# Chunk type &0112
self._write_chunk_header(chunk_type=0x112, length=2)
# Length of silence
self.output.append(duration_cycles & 0xFF)
self.output.append((duration_cycles >> 8) & 0xFF)
def add_header_tone(self, duration: float) -> None:
"""
Add a period of header tone to the output.
:param duration:
Duration of header tone [sec]
:return:
None
"""
# Convert duration into units of baud
duration_cycles = int(duration * (self.baud_rate * 2))
if duration_cycles < 1:
duration_cycles = 1
if duration_cycles > 0xFFFF:
duration_cycles = 0xFFFF
# Chunk type &0112
self._write_chunk_header(chunk_type=0x110, length=2)
# Length of header tone
self.output.append(duration_cycles & 0xFF)
self.output.append((duration_cycles >> 8) & 0xFF)
def add_data_chunk(self, byte_list: list) -> None:
"""
Add a data block to the output.
:param byte_list:
List of bytes that comprise the data block
:return:
None
"""
# Chunk type &0112
self._write_chunk_header(chunk_type=0x100, length=len(byte_list))
# Add each byte in turn
for item in byte_list:
self.output.append(item['byte'])