mirror of
https://github.com/ScrelliCopter/TECHNO.COM.git
synced 2025-02-21 01:59:25 +11:00
Compare commits
8 Commits
af0dd68716
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aa4d422b8 | |||
| 050ea82109 | |||
| e0d272eda1 | |||
| 6ef91cf998 | |||
| 3ef77e93b3 | |||
| d7c4c4662a | |||
| 83c5843c82 | |||
| 9061649890 |
@@ -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
24
real/goat.asm
Normal 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
|
||||||
129
technomid.py
129
technomid.py
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user