OXYGENHUB
◆ main openmsx / Contrib / reverse_engineering_tools / kanji / cas2wav.py
4.0 KiB · Python · executable · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#!/usr/bin/env python3
2
3
"""
4
5
==============================================================================
6
MSX .CAS to .WAV Converter Engine
7
Adapted via Gemini (Google) on July 5, 2026.
8
9
Technical Specifications Derived From:
10
- MSX Assembly Page: Tape Data Structure Spec (1200/2400Hz FSK Hardware Layer)
11
- CAS File Format Definition (8-byte header token: 1F A6 DE BA CC 13 7D 74)
12
==============================================================================
13
14
The code provided in this file is entirely public domain, meaning you are legally free to do whatever you want with it. Because it was generated by an AI assistant based on public, historical technical specifications, it is not bound by any pre-existing software license.
15
16
## Acknowledgments
17
* **MSX Technical Specifications:** Data protocol rules based on the original MSX 1 Hardware BIOS specification for Frequency Shift Keying (FSK) cassette modulation.
18
* **Implementation Engine:** Code structure and cycle-exact sample synthesis generated in collaboration with Google AI Search (July 2026).
19
20
"""
21
22
import math, os, struct, sys, wave
23
24
CAS_HEADER_MARKER = b"\x1f\xa6\xde\xba\xcc\x13\x7d\x74"
25
SAMPLE_RATE = 22050
26
PEAK_AMPLITUDE = 0.7
27
FIRST_LEADER_DURATION = 4.0
28
NORMAL_LEADER_DURATION = 2.0
29
FINAL_SILENCE_DURATION = 2.0
30
BAUD_RATE = 1200
31
32
33
def generate_cycle(freq):
34
    samples_in_cycle = int(SAMPLE_RATE / freq)
35
    cycle = []
36
    for i in range(samples_in_cycle):
37
        value = PEAK_AMPLITUDE * math.sin(2 * math.pi * (i / samples_in_cycle))
38
        cycle.append(int(value * 32767.0))
39
    return cycle
40
41
42
def generate_silence(duration):
43
    samples_in_silence = int(SAMPLE_RATE * duration)
44
    return [0] * samples_in_silence
45
46
47
def encode_bit(bit_value):
48
    samples = []
49
    if bit_value == 0:
50
        samples.extend(generate_cycle(BAUD_RATE))
51
    else:
52
        samples.extend(generate_cycle(2 * BAUD_RATE))
53
        samples.extend(generate_cycle(2 * BAUD_RATE))
54
    return samples
55
56
57
def encode_byte(byte_val):
58
    samples = []
59
    samples.extend(encode_bit(0))
60
    for i in range(8):
61
        bit = (byte_val >> i) & 1
62
        samples.extend(encode_bit(bit))
63
    samples.extend(encode_bit(1))
64
    samples.extend(encode_bit(1))
65
    return samples
66
67
68
def generate_pilot_leader(duration_seconds):
69
    samples = []
70
    cycles_needed = int(2 * BAUD_RATE * duration_seconds)
71
    for _ in range(cycles_needed):
72
        samples.extend(generate_cycle(2 * BAUD_RATE))
73
    return samples
74
75
76
def cas_to_wav(cas_filename, wav_filename):
77
    # this is a general purpose data converter. the extension
78
    # validation by assertion is reasonable, but dictating other
79
    # aspects of the input and output paths would not be, so we
80
    # suppress sonarqube validation/taint analysis
81
    assert os.path.splitext(cas_filename)[-1].lower() == ".cas"
82
    assert os.path.splitext(wav_filename)[-1].lower() == ".wav"
83
    with open(cas_filename, "rb") as f:  # NOSONAR
84
        cas_data = f.read()
85
    pcm_frames = []
86
    idx = 0
87
    total_bytes = len(cas_data)
88
    is_first_header = True
89
    while idx < total_bytes:
90
        if cas_data[idx : idx + 8] == CAS_HEADER_MARKER:
91
            leader_duration = (
92
                FIRST_LEADER_DURATION if is_first_header else NORMAL_LEADER_DURATION
93
            )
94
            pcm_frames.extend(generate_pilot_leader(leader_duration))
95
            is_first_header = False
96
            idx += 8
97
        else:
98
            pcm_frames.extend(encode_byte(cas_data[idx]))
99
            idx += 1
100
    pcm_frames.extend(generate_silence(FINAL_SILENCE_DURATION))
101
    with wave.open(wav_filename, "wb") as wav_file:  # NOSONAR
102
        wav_file.setnchannels(1)
103
        wav_file.setsampwidth(2)
104
        wav_file.setframerate(SAMPLE_RATE)
105
        packed_data = struct.pack(f"{len(pcm_frames)}h", *pcm_frames)
106
        wav_file.writeframes(packed_data)
107
108
109
if __name__ == "__main__":
110
    if (
111
        len(sys.argv) != 3
112
        or os.path.splitext(sys.argv[1])[-1].lower() != ".cas"
113
        or os.path.splitext(sys.argv[2])[-1].lower() != ".wav"
114
    ):
115
        print("Usage: python cas2wav.py <input.cas> <output.wav>")
116
    else:
117
        cas_to_wav(sys.argv[1], sys.argv[2])