midi generator

This commit is contained in:
2024-03-01 15:47:52 +11:00
parent 1e81b64365
commit af0dd68716
3 changed files with 174 additions and 1 deletions

View File

@@ -2,10 +2,10 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
insert_final_newline = true
[*.asm] [*.asm]
indent_style = tab indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{masm.asm,nasm.asm}] [*.{masm.asm,nasm.asm}]
@@ -16,3 +16,7 @@ tab_width = 4
[*.gas.asm] [*.gas.asm]
end_of_line = lf end_of_line = lf
tab_width = 8 tab_width = 8
[*.py]
end_of_line = lf
tab_width = 4

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*.com *.com
*.mid
.vs/ .vs/
.idea/ .idea/

168
technomid.py Normal file
View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# technomid.py - Generate TECHNO.COM procedural melody as MIDI - (c) 2024 a dinosaur
# Home page: https://github.com/ScrelliCopter/TECHNO.COM
# SPDX-License-Identifier: Zlib (https://opensource.org/license/Zlib)
import struct
import math
from abc import ABC, abstractmethod
from pathlib import Path
from typing import BinaryIO, List, Tuple
from enum import IntEnum
class MIDIEvent(ABC):
@abstractmethod
def serialise(self) -> bytes:
pass
class MIDINoteOff(MIDIEvent):
def __init__(self, channel: int, note: int, velocity: int = 64):
if channel < 0 or channel > 0xF:
raise ValueError("MIDI Channel out of range")
if note < 0 or note > 0x7F:
raise ValueError("MIDI Note out of range")
if velocity < 0 or velocity > 0x7F:
raise ValueError("MIDI Velocity out of range")
self._channel = channel
self._note = note
self._velocity = velocity
def serialise(self) -> bytes:
return struct.pack(">BBB", 0x80 | self._channel, self._note, self._velocity)
class MIDINoteOn(MIDIEvent):
def __init__(self, channel: int, note: int, velocity: int = 127):
if channel < 0 or channel > 0xF:
raise ValueError("MIDI Channel out of range")
if note < 0 or note > 0x7F:
raise ValueError("MIDI Note out of range")
if velocity < 0 or velocity > 0x7F:
raise ValueError("MIDI Velocity out of range")
self._channel = channel
self._note = note
self._velocity = velocity
def serialise(self) -> bytes:
return struct.pack(">BBB", 0x90 | self._channel, self._note, self._velocity)
class MIDIProgramChange(MIDIEvent):
def __init__(self, channel: int, patch: int):
if channel < 0 or channel > 0xF:
raise ValueError("MIDI Channel out of range")
if patch < 0 or patch >= 0x80:
raise ValueError("Program out of range")
self._channel = channel
self._patch = patch
def serialise(self) -> bytes:
return struct.pack(">BB", 0xC0 | self._channel, self._patch)
class MIDIPitchWheel(MIDIEvent):
def __init__(self, channel: int, value: int = 0):
if channel < 0 or channel > 0xF:
raise ValueError("MIDI Channel out of range")
if value < -8192 or value > 8191:
raise ValueError("MIDI Pitch bend value out of range")
self._channel = channel
self._value = value + 0x2000
def serialise(self) -> bytes:
return struct.pack(">BBB", 0xE0 | self._channel, self._value & 0x7F, self._value >> 7)
class MIDIMetaTempo(MIDIEvent):
def __init__(self, quarter_us: int):
if quarter_us < 0 or quarter_us >= 0x1000000:
raise ValueError("Quarter note microseconds out of range")
self._quarter_us = quarter_us
def serialise(self) -> bytes:
return b"\xFF\x51\x03" + self._quarter_us.to_bytes(3, byteorder="big")
class MIDIWriter:
def __init__(self, file: BinaryIO):
self._file = file
Format = IntEnum("Format", ["SINGLE", "MULTI", "SEQUENCE"])
def write_header(self, division: int, fmt: Format = Format.SINGLE, track_count: int = 1):
self._file.write(b"MThd")
self._file.write(struct.pack(">IHHH", 6, fmt, track_count, division))
def write_track(self, events: List[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(len(payload).to_bytes(4, byteorder="big"))
self._file.write(payload)
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:
return value.to_bytes()
else:
a = (value & 0xFE00000) >> 21
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):
timer = round((1000000 * 14.31818) / 12)
def note_from_period(period: int) -> (int, int):
frequency = timer / max(1, period)
fnote = 69 + 12 * math.log2(frequency / 440)
note = int(round(fnote))
bend = min(0x1FFF, int(round((fnote - note) * 0x1000)))
return min(0x7F, note), bend
def techno():
phrase = [2, *[1, 0, 0] * 3] * 3 + [2, 3] + [0, 3] * 3
freq_tbl = [5424, 2712, 2416, 2280]
mangler = 0x0404
i = 0
bend = 0
while True:
for sixteenth in phrase:
note, new_bend = note_from_period(freq_tbl[sixteenth])
if new_bend != bend:
yield 0, MIDIPitchWheel(0, new_bend)
bend = new_bend
yield 0, MIDINoteOn(0, note)
yield 32, MIDINoteOff(0, note)
i += 2
if i >= 80 * 25:
return
mangler = (mangler + len(phrase) * 2) & 0xFFFF
freq_tbl = [freq ^ mangler for freq in freq_tbl]
with outpath.open("wb") as f:
mid = MIDIWriter(f)
mid.write_header(128)
mid.write_track([
(0, MIDIProgramChange(0, 80)),
(0, MIDIMetaTempo(int(round((1000000 * 0x80000) / timer))))
] + list(techno()))
if __name__ == "__main__":
generate(Path("techno.mid"))