Compare commits

...

8 Commits

3 changed files with 111 additions and 50 deletions

View File

@@ -3,6 +3,8 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
insert_final_newline = true insert_final_newline = true
end_of_line = lf
tab_width = 4
[*.asm] [*.asm]
indent_style = tab indent_style = tab
@@ -11,12 +13,10 @@ trim_trailing_whitespace = true
[*.{masm.asm,nasm.asm}] [*.{masm.asm,nasm.asm}]
charset = latin1 charset = latin1
end_of_line = crlf end_of_line = crlf
tab_width = 4
[*.gas.asm] [*.gas.asm]
end_of_line = lf
tab_width = 8 tab_width = 8
[*.py] [*.py]
end_of_line = lf indent_style = tab
tab_width = 4 trim_trailing_whitespace = true

24
real/goat.asm Normal file
View File

@@ -0,0 +1,24 @@
; 100-byte COM sacrificial goat executable (1993, author unknown)
; Binary MD5: 195307045CC39D6B284B60442ECFD202
; SHA256: D1F60FCA64F1903F8D405109C5AA55A3F3B6DDE622BCFBA15CD95001CAE1DEE2
;
; Assemble with FASM: fasm goat.asm goat.com
; Assemble with NASM or YASM: nasm -fbin goat.asm -o goat.com
use16
org 100h
start:
jmp short print
nop
hello_str db 'Hello - This is a 100 COM test file, 1993', 0Ah, 0Dh, '$' ; Hello followed by '\n\r'
db 1Ah ; Pad with substitute
times 41 db 'A' ; and 'A' * 41
print:
mov ah, 9 ; AH: Print string
mov dx, hello_str ; DS:DX: String = "Hello - This is a 100 COM test file, 1993"
int 21h
int 20h ; Return to DOS

View File

@@ -3,11 +3,12 @@
# Home page: https://github.com/ScrelliCopter/TECHNO.COM # Home page: https://github.com/ScrelliCopter/TECHNO.COM
# SPDX-License-Identifier: Zlib (https://opensource.org/license/Zlib) # SPDX-License-Identifier: Zlib (https://opensource.org/license/Zlib)
import os
import struct import struct
import math import math
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from typing import BinaryIO, List, Tuple from typing import BinaryIO, Iterable, Tuple
from enum import IntEnum from enum import IntEnum
@@ -54,7 +55,7 @@ class MIDIProgramChange(MIDIEvent):
if channel < 0 or channel > 0xF: if channel < 0 or channel > 0xF:
raise ValueError("MIDI Channel out of range") raise ValueError("MIDI Channel out of range")
if patch < 0 or patch >= 0x80: if patch < 0 or patch >= 0x80:
raise ValueError("Program out of range") raise ValueError("MIDI Program out of range")
self._channel = channel self._channel = channel
self._patch = patch self._patch = patch
@@ -75,6 +76,11 @@ class MIDIPitchWheel(MIDIEvent):
return struct.pack(">BBB", 0xE0 | self._channel, self._value & 0x7F, self._value >> 7) return struct.pack(">BBB", 0xE0 | self._channel, self._value & 0x7F, self._value >> 7)
class MIDIMetaTrackEnd(MIDIEvent):
def serialise(self) -> bytes:
return struct.pack(">BBB", 0xFF, 0x2F, 0x00)
class MIDIMetaTempo(MIDIEvent): class MIDIMetaTempo(MIDIEvent):
def __init__(self, quarter_us: int): def __init__(self, quarter_us: int):
if quarter_us < 0 or quarter_us >= 0x1000000: if quarter_us < 0 or quarter_us >= 0x1000000:
@@ -89,80 +95,111 @@ class MIDIWriter:
def __init__(self, file: BinaryIO): def __init__(self, file: BinaryIO):
self._file = file self._file = file
Format = IntEnum("Format", ["SINGLE", "MULTI", "SEQUENCE"]) class Format(IntEnum):
SINGLE = 0
MULTI = 1
SEQUENCE = 2
def write_header(self, division: int, fmt: Format = Format.SINGLE, track_count: int = 1): def write_header(self, division: int, fmt: Format = Format.SINGLE, track_count: int = 1):
self._file.write(b"MThd") self._file.write(b"MThd")
self._file.write(struct.pack(">IHHH", 6, fmt, track_count, division)) self._file.write(struct.pack(">IHHH", 6, fmt, track_count, division))
def write_track(self, events: List[Tuple[int, MIDIEvent]]): def write_track(self, events: Iterable[Tuple[int, MIDIEvent]]):
payload = b""
for event in events:
payload += self.encode_varint(event[0])
payload += event[1].serialise()
self._file.write(b"MTrk") self._file.write(b"MTrk")
self._file.write(len(payload).to_bytes(4, byteorder="big")) ofs = self._file.tell()
self._file.write(payload) self._file.write(b"\0\0\0\0") # Blank length field to write later
# Serialise and write out events
payload_len = 0
for event in events:
data = event[1].serialise()
length = self.encode_varint(event[0])
self._file.writelines([length, data])
payload_len += len(length) + len(data)
# Fill in track length field
self._file.seek(ofs)
self._file.write(payload_len.to_bytes(4, byteorder="big"))
self._file.seek(0, os.SEEK_END)
# Variable integer, used by event deltas.
# Up to 4 bytes can encode 7 bits each by setting the continuation bit (bit 8)
def encode_varint(self, value: int) -> bytes: def encode_varint(self, value: int) -> bytes:
if value < 0:
raise ValueError("Variable integer must be positive")
if value >= 0x10000000:
raise ValueError("Variable integer is larger than 0FFFFFFF")
if value < 0x80: if value < 0x80:
return value.to_bytes() return value.to_bytes(1, byteorder="big")
if value < 0x4000:
return bytes([0x80 | (value >> 7), value & 0x7F])
if value < 0x200000:
return bytes([0x80 | (value >> 14), 0x80 | (value >> 7) & 0x7F, value & 0x7F])
if value < 0x10000000:
return bytes([0x80 | (value >> 21), 0x80 | (value >> 14) & 0x7F, 0x80 | (value >> 7) & 0x7F, value & 0x7F])
else: else:
a = (value & 0xFE00000) >> 21 raise ValueError("Variable integer out of range")
b = (value & 0x01FC000) >> 14
c = (value & 0x0003F80) >> 7
d = (value & 0x000007F)
if a != 0:
return bytes([a | 0x80, b | 0x80, c | 0x80, d])
elif b != 0:
return bytes([b | 0x80, c | 0x80, d])
elif c != 0:
return bytes([c | 0x80, d])
def generate(outpath: Path): def generate(f: BinaryIO):
timer = round((1000000 * 14.31818) / 12) def note_from_period(period: int, reference: int) -> (int, int):
frequency = reference / max(1, period) # Convert period to hz
fnote = 69 + 12 * math.log2(frequency / 440) # Convert pitch to MIDI note
note = int(round(fnote)) # Snap to nearest semitone
bend = int(round((fnote - note) * 0x1000)) # Error is encoded as pitch bend
return min(0x7F, note), min(0x1FFF, bend)
def note_from_period(period: int) -> (int, int): def parse_midi_note(note: str) -> int:
frequency = timer / max(1, period) flat_sharp = 0
fnote = 69 + 12 * math.log2(frequency / 440) octave = 3
note = int(round(fnote)) if len(note) > 1:
bend = min(0x1FFF, int(round((fnote - note) * 0x1000))) if note[1] == '-' or note[1].isnumeric():
return min(0x7F, note), bend octave = int(note[1:])
else:
flat_sharp = { "b": -1, "#": 1 }[note[1]]
if len(note) > 2:
octave = int(note[2:])
def techno(): natural = { "C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11 }
return natural[note[0]] + flat_sharp + (1 + octave) * 12
def techno(length: int):
timer = int(round((1000000 * 1260 / 88) / 12)) # Intel 8253 (PIC) clock in Mhz
# Music tables from disassembly
phrase = [2, *[1, 0, 0] * 3] * 3 + [2, 3] + [0, 3] * 3 phrase = [2, *[1, 0, 0] * 3] * 3 + [2, 3] + [0, 3] * 3
freq_tbl = [5424, 2712, 2416, 2280] note_tbl = ["A3", "A4", "B4", "C5"]
mangler = 0x0404 mangler = 0x0404
mangler_inc = 76 # len(phrase) * 2
# Convert note table to period
freq_from_note = lambda m: 440.0 * 2.0 ** ((m - 69) / 12.0)
period_from_freq = lambda f: int(round(timer / f))
period_from_note = lambda s: period_from_freq(freq_from_note(parse_midi_note(s)))
freq_tbl = [period_from_note(note) for note in note_tbl]
yield 0, MIDIMetaTempo(int(round((1000000 * 0x80000) / timer))) # 16th note every two PIC ticks
yield 0, MIDIProgramChange(0, 80) # Set GM patch to #81 "Lead 1 (Square)"
i = 0 i = 0
bend = 0 bend = 0
while True: while True:
for sixteenth in phrase: for sixteenth in phrase:
note, new_bend = note_from_period(freq_tbl[sixteenth]) note, new_bend = note_from_period(freq_tbl[sixteenth], timer)
if new_bend != bend: if new_bend != bend:
yield 0, MIDIPitchWheel(0, new_bend) yield 0, MIDIPitchWheel(0, new_bend)
bend = new_bend bend = new_bend
yield 0, MIDINoteOn(0, note) yield 0, MIDINoteOn(0, note)
yield 32, MIDINoteOff(0, note) yield 32, MIDINoteOff(0, note)
i += 2 i += 2
if i >= 80 * 25: if i >= length:
yield 0, MIDIMetaTrackEnd()
return return
mangler = (mangler + len(phrase) * 2) & 0xFFFF # Scramble pitch table at the end of each measure
mangler = (mangler + mangler_inc) & 0xFFFF
freq_tbl = [freq ^ mangler for freq in freq_tbl] freq_tbl = [freq ^ mangler for freq in freq_tbl]
with outpath.open("wb") as f: mid = MIDIWriter(f)
mid = MIDIWriter(f) mid.write_header(128)
mid.write_header(128) mid.write_track(techno(80 * 25))
mid.write_track([
(0, MIDIProgramChange(0, 80)),
(0, MIDIMetaTempo(int(round((1000000 * 0x80000) / timer))))
] + list(techno()))
if __name__ == "__main__": if __name__ == "__main__":
generate(Path("techno.mid")) with Path("techno.mid").open("wb") as f:
generate(f)