Add music player source

This commit is contained in:
Natt Akuma 2025-11-26 19:42:34 +07:00
parent 3f4765029b
commit c006e1a0a2
35 changed files with 7608 additions and 6 deletions

View file

@ -97,6 +97,7 @@ C1541 = c1541
CC1541 = ../../tools/cc1541/cc1541
ZX02 = zx02/build/zx02
ZX02_SRC = zx02
FURC64 = furC64
PYTHON = python3
MKDIR = mkdir -p
@ -130,6 +131,7 @@ DISKIMAGE = $(BUILDDIR)/$(NAME)-$(_PLATFORM_).d64
AS_FLAGS = -Wa -I../../../shared -I ../../include -u __EXEHDR__
ZX0PRGS = \
use_this_sid.zx0.prg \
badguy.zx0.prg \
title_320-prepared.zx0.prg \
tower.zx0.prg \
@ -174,11 +176,11 @@ endif
diskimage: $(DISKIMAGE)
$(DISKIMAGE): $(ASSEMBLE) $(CC1541) $(ZX0PRGS) $(LZPRGS) use_this_sid.bin font.bin
$(DISKIMAGE): $(ASSEMBLE) $(CC1541) $(ZX0PRGS) $(LZPRGS) font.bin
$(RM) $@
$(CC1541) -n "otomata labs" -i " 2025" \
-f "ys2intro" -w $< \
-f "sid" -w use_this_sid.bin \
-f "song0" -w use_this_sid.zx0.prg \
-f "badguy" -w badguy.zx0.prg \
-f "font" -w font.bin \
-f "intrbmp" -w title_320-prepared.zx0.prg \
@ -224,6 +226,10 @@ $(ZX02):
$(PRINTF) "\x00\x90" | cat - $@.tmp > $@
$(RM) $@.tmp
use_this_sid.bin: ys2_port_legato.fur
cd $(FURC64) && ./convert.sh $(abspath $<)
cp $(FURC64)/asm/song.bin $@
clean:
-$(RM) $(ZX0PRGS) $(LZPRGS)
-$(RM) *.o $(ASSEMBLE) $(DISKIMAGE)

View file

@ -0,0 +1,8 @@
__pycache__
asm/song.asm
asm/song.bin
*.o
*.lbl
*.lst
*.map
*.prg

View file

@ -0,0 +1,42 @@
# furC64
a C64/SID sound driver for Furnace
### **THIS SOUND DRIVER IS CURRENTLY A WIP**
A SID driver that's easy to make music with? It's more likely than you think.
* You have to have [Python](https://www.python.org/) and the [CC65 toolchain](https://cc65.github.io/) installed
* You **have** to set the pitch linearity option to "None". You can do this by going to `window -> song -> compatability flags -> Pitch/Playback -> Pitch linearity` and then setting the option to "None".
* The driver only supports **arpeggio, waveform, duty and cutoff** macros in each instrument and it DOESN'T support LFO and ADSR macros nor delay and step length, **although you can use LFO macros in the duty and cutoff macros (as in range-sweeping)**
* The furC64 driver only supports these effects:
* 00xx: arpeggio
* 01xx: pitch slide up
* 02xx: pitch slide down
* 03xx: portamento
* 04xx: vibrato
* 09xx: set speed 1
* 0Bxx: jump to pattern
* 0Dxx: jump to next pattern
* 0Fxx: set speed 2
* 1Axx: disable/enable envelope reset
* 1Bxx: reset cutoff
* 1Cxx: reset pulse-width
* 4xxx: set filter cutoff
* E1xx: note slide up
* E2xx: note slide down
* E5xx: note fine-pitch
* EAxx: legato
* ECxx: note cut
when you've finished / want to test out this driver:
* open the terminal/command prompt **to the furC64 directory**
* run `convert.sh your_fur_file.fur` or `convert.bat file.fur` (depending on your OS)
* in the `furC64/asm` directory you'll hopefully see a file called **`furC64-test.prg`**
* that's your .prg file that you can run on hardware or on an emulator like VICE!
Hopefully you'll have fun with this driver alongside [furNES](https://github.com/AnnoyedArt1256/furNES) :D
Libraries used: chipchune

View file

@ -0,0 +1,16 @@
FEATURES {
STARTADDRESS: default = $0801;
}
MEMORY {
ZP: file = "", start = $0002, size = $00FE, define = yes;
MAIN: file = "", start = %S, size = $A000 - %S;
PLAYER: file = %O, start = $A000, size = $5FFA;
}
SEGMENTS {
ZEROPAGE: load = ZP, type = zp, optional = yes;
CODE: load = MAIN, type = rw;
RODATA: load = MAIN, type = ro, optional = yes;
DATA: load = MAIN, type = rw, optional = yes;
BSS: load = MAIN, type = bss, optional = yes, define = yes;
PLAYER: load = PLAYER, type = rw, define = yes;
}

View file

@ -0,0 +1,22 @@
FEATURES {
STARTADDRESS: default = $0801;
}
SYMBOLS {
__LOADADDR__: type = import;
}
MEMORY {
ZP: file = "", start = $0002, size = $00FE, define = yes;
LOADADDR: file = %O, start = %S - 2, size = $0002;
MAIN: file = %O, start = %S, size = $A000 - %S;
PLAYER: file = %O, start = $A000, size = $5FFA;
}
SEGMENTS {
ZEROPAGE: load = ZP, type = zp, optional = yes;
LOADADDR: load = LOADADDR, type = ro;
EXEHDR: load = MAIN, type = ro, optional = yes;
CODE: load = MAIN, type = rw;
RODATA: load = MAIN, type = ro, optional = yes;
DATA: load = MAIN, type = rw, optional = yes;
BSS: load = MAIN, type = bss, optional = yes, define = yes;
PLAYER: load = MAIN, run = PLAYER, type = rw, define = yes;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@

 "$')+.147:>AEINRW\bhnu|<7C><><EFBFBD><EFBFBD>ク皇俑褻<E4BF91>

View file

@ -0,0 +1 @@
&8K^s‰¡¹Ôð ,Mq½çBs¨àYâ,{Î'„çQÀ6³7ÄYö<59>N Ï¢<C38F>mgoˆ²í:œŸDÚÎßdÚu8&?‰´œ¿"È´ëqLh8E<7F>hÖã˜ÿ$ÿ

View file

@ -0,0 +1,18 @@
"""
:mod:`chipchune` is a Python library for manipulating several
different kinds of chiptune music files.
Currently supports:
- Furnace (:mod:`chipchune.furnace`) (almost!)
Plans to support:
- DefleMask (:mod:`chipchune.deflemask`)
- FamiTracker (:mod:`chipchune.famitracker`)
### Installation
`pip install git+https://github.com/ZoomTen/chipchune@master`
"""
__version__ = "0.0.1"

View file

@ -0,0 +1,98 @@
import struct
from enum import Enum
from typing import BinaryIO, Any, cast
import io
known_sizes = {
'c': 1,
'b': 1, 'B': 1,
'?': 1,
'h': 2, 'H': 2,
'i': 4, 'I': 4,
'l': 4, 'L': 4,
'q': 8, 'Q': 8,
'e': 2, 'f': 4,
'd': 8
}
class EnumShowNameOnly(Enum):
"""
Just an Enum, except its string repr is
just the enum's name
"""
def __repr__(self) -> str:
return self.name
def __str__(self) -> str:
return self.__repr__()
class EnumValueEquals(Enum):
"""
Enum that can be compared to its raw value.
"""
def __eq__(self, other: Any) -> bool:
return cast(bool, self.value == other)
def truthy_to_boolbyte(value: Any) -> bytes:
"""
If value is truthy, output b'\x01'. Else output b'\x00'.
:param value: anything
"""
if value:
return b'\x01'
else:
return b'\x00'
# these are just to make the typehinter happy
# cast(dolphin, foobar) should've been named trust_me_bro_im_a(dolphin, foobar)
def read_int(file: BinaryIO, signed: bool = False) -> int:
"""
4 bytes
"""
if signed:
return cast(int, struct.unpack('<i', file.read(known_sizes['i']))[0])
return cast(int, struct.unpack('<I', file.read(known_sizes['I']))[0])
def read_short(file: BinaryIO, signed: bool = False) -> int:
"""
2 bytes
"""
if signed:
return cast(int, struct.unpack('<h', file.read(known_sizes['h']))[0])
return cast(int, struct.unpack('<H', file.read(known_sizes['H']))[0])
def read_byte(file: BinaryIO, signed: bool = False) -> int:
"""
1 bytes
"""
if signed:
return cast(int, struct.unpack('<b', file.read(known_sizes['b']))[0])
return cast(int, struct.unpack('<B', file.read(known_sizes['B']))[0])
def read_float(file: BinaryIO) -> float:
"""
4 bytes
"""
return cast(float, struct.unpack('<f', file.read(known_sizes['f']))[0])
def read_str(file: BinaryIO) -> str:
"""
variable string (ends in \\x00)
"""
buffer = bytearray()
char = file.read(1)
while char != b'\x00':
buffer += char
char = file.read(1)
return buffer.decode('utf-8')

View file

@ -0,0 +1,3 @@
"""
soon!
"""

View file

@ -0,0 +1,3 @@
"""
soon!
"""

View file

@ -0,0 +1,24 @@
"""
Tools to manipulate Furnace .fur files.
- :mod:`chipchune.furnace.module`: Tools to inspect and manipulate module files.
- :mod:`chipchune.furnace.instrument`: Tools to inspect and manipulate instrument data from within or without the module.
- :mod:`chipchune.furnace.sample`: Tools to inspect and manipulate sample data (might be merged with inst?)
- :mod:`chipchune.furnace.wavetable`: Tools to inspect and manipulate wavetable data
- :mod:`chipchune.furnace.enums`: Various constants that apply to Furnace.
- :mod:`chipchune.furnace.data_types`: Various data types that apply to Furnace.
### Example
from chipchune.furnace.module import FurnaceModule
module = FurnaceModule("tests/samples/furnace/skate_or_die.143.fur")
pattern = module.get_pattern(0, 0, 0)
print(pattern.as_clipboard())
for row in pattern.data:
print(row)
"""

View file

@ -0,0 +1,706 @@
from dataclasses import dataclass, field
from typing import Tuple, List, TypedDict, Any, Union, Dict
from .enums import (
ChipType, LinearPitch, LoopModality, DelayBehavior, JumpTreatment, InputPortSet, OutputPortSet,
InstrumentType, MacroCode, OpMacroCode, MacroType, MacroItem, GBHwCommand, WaveFX, ESFilterMode,
SNESSusMode, GainMode, Note
)
# modules
@dataclass
class ChipInfo:
"""
Information on a single chip.
"""
type: ChipType
#: shall be a simple dict, no enums needed
flags: Dict[str, Any] = field(default_factory=dict)
panning: float = 0.0
surround: float = 0.0
"""
Chip front/rear balance.
"""
volume: float = 1.0
@dataclass
class ModuleMeta:
"""
Module metadata.
"""
name: str = ''
name_jp: str = ''
author: str = ''
author_jp: str = ''
album: str = ''
"""
Can also be the game name or container name.
"""
album_jp: str = ''
sys_name: str = 'Sega Genesis/Mega Drive'
sys_name_jp: str = ''
comment: str = ''
version: int = 0
tuning: float = 440.0
@dataclass
class TimingInfo:
"""
Timing information for a single subsong.
"""
arp_speed = 1
clock_speed = 60.0
highlight: Tuple[int, int] = (4, 16)
speed: Tuple[int, int] = (0, 0)
timebase = 1
virtual_tempo: Tuple[int, int] = (150, 150)
@dataclass
class ChipList:
"""
Information about chips used in the module.
"""
list: List[ChipInfo] = field(default_factory=list)
master_volume: float = 2.0
@dataclass(repr=False)
class ChannelDisplayInfo:
"""
Relating to channel display in Pattern and Order windows.
"""
name: str = ''
abbreviation: str = ''
collapsed: bool = False
shown: bool = True
def __repr__(self) -> str:
return "ChannelDisplayInfo(name='%s', abbreviation='%s', collapsed=%s, shown=%s)" % (
self.name,
self.abbreviation,
self.collapsed,
self.shown
)
@dataclass
class ModuleCompatFlags:
"""
Module compatibility flags, a.k.a. "The Motherload"
Default values correspond with fileOps.cpp in the furnace src.
"""
# compat 1
limit_slides: bool = False
linear_pitch: LinearPitch = field(default_factory=lambda: LinearPitch.FULL_LINEAR)
loop_modality: LoopModality = field(default_factory=lambda: LoopModality.DO_NOTHING)
proper_noise_layout: bool = True
wave_duty_is_volume: bool = False
reset_macro_on_porta: bool = False
legacy_volume_slides: bool = False
compatible_arpeggio: bool = False
note_off_resets_slides: bool = True
target_resets_slides: bool = True
arpeggio_inhibits_portamento: bool = False
wack_algorithm_macro: bool = False
broken_shortcut_slides: bool = False
ignore_duplicates_slides: bool = False
stop_portamento_on_note_off: bool = False
continuous_vibrato: bool = False
broken_dac_mode: bool = False
one_tick_cut: bool = False
instrument_change_allowed_in_porta: bool = True
reset_note_base_on_arpeggio_stop: bool = True
# compat 2 (>= dev70)
broken_speed_selection: bool = False
no_slides_on_first_tick: bool = False
next_row_reset_arp_pos: bool = False
ignore_jump_at_end: bool = False
buggy_portamento_after_slide: bool = False
gb_ins_affects_env: bool = True
shared_extch_state: bool = True
ignore_outside_dac_mode_change: bool = False
e1e2_takes_priority: bool = False
new_sega_pcm: bool = True
weird_fnum_pitch_slides: bool = False
sn_duty_resets_phase: bool = False
linear_pitch_macro: bool = True
pitch_slide_speed_in_linear: int = 4
old_octave_boundary: bool = False
disable_opn2_dac_volume_control: bool = False
new_volume_scaling: bool = True
volume_macro_lingers: bool = True
broken_out_vol: bool = False
e1e2_stop_on_same_note: bool = False
broken_porta_after_arp: bool = False
sn_no_low_periods: bool = False
cut_delay_effect_policy: DelayBehavior = field(default_factory=lambda: DelayBehavior.LAX)
jump_treatment: JumpTreatment = field(default_factory=lambda: JumpTreatment.ALL_JUMPS)
auto_sys_name: bool = True
disable_sample_macro: bool = False
broken_out_vol_2: bool = False
old_arp_strategy: bool = False
# not-a-compat (>= dev135)
auto_patchbay: bool = True
# compat 3 (>= dev138)
broken_porta_during_legato: bool = False
broken_fm_off: bool = False
pre_note_no_effect: bool = False
old_dpcm: bool = False
reset_arp_phase_on_new_note: bool = False
ceil_volume_scaling: bool = False
old_always_set_volume: bool = False
old_sample_offset: bool = False
@dataclass
class SubSong:
"""
Information on a single subsong.
"""
name: str = ''
comment: str = ''
speed_pattern: List[int] = field(default_factory=lambda: [6])
"""
Maximum 16 entries.
"""
grooves: List[List[int]] = field(default_factory=list)
timing: TimingInfo = field(default_factory=TimingInfo)
pattern_length = 64
order: Dict[int, List[int]] = field(default_factory=lambda: {
0: [0], 1: [0], 2: [0], 3: [0], 4: [0],
5: [0], 6: [0], 7: [0], 8: [0], 9: [0]
})
effect_columns: List[int] = field(default_factory=lambda: [
1 for _ in range(
ChipType.YM2612.channels + ChipType.SMS.channels
)
])
channel_display: List[ChannelDisplayInfo] = field(default_factory=lambda: [
ChannelDisplayInfo() for _ in range(
ChipType.YM2612.channels + ChipType.SMS.channels
)
])
@dataclass
class FurnaceRow:
"""
Represents a single row in a pattern.
"""
note: Note
octave: int
instrument: int
volume: int
effects: List[Tuple[int, int]] = field(default_factory=list)
def as_clipboard(self) -> str:
"""
Renders the selected row in Furnace clipboard format (without header!)
:return: Furnace clipboard data (str)
"""
note_maps = {
Note.Cs: "C#",
Note.D_: "D-",
Note.Ds: "D#",
Note.E_: "E-",
Note.F_: "F-",
Note.Fs: "F#",
Note.G_: "G-",
Note.Gs: "G#",
Note.A_: "A-",
Note.As: "A#",
Note.B_: "B-",
Note.C_: "C-",
}
if self.note == Note.OFF:
note_str = "OFF"
elif self.note == Note.OFF_REL:
note_str = "==="
elif self.note == Note.REL:
note_str = "REL"
elif self.note == Note.__:
note_str = "..."
else:
note_str = "%s%d" % (note_maps[self.note], self.octave)
vol = ".." if self.volume==0xffff else "%02X" % self.volume
ins = ".." if self.instrument==0xffff else "%02X" % self.instrument
rep_str = "%s%s%s"
for fx in self.effects:
cmd, val = fx
cmd_str = ".." if cmd == 0xffff else "%02X" % cmd
val_str = ".." if val == 0xffff else "%02X" % val
rep_str += "%s%s" % (cmd_str, val_str)
return rep_str % (
note_str,
ins, vol
) + "|"
def __str__(self) -> str:
if self.note == Note.OFF:
note_str = "OFF"
elif self.note == Note.OFF_REL:
note_str = "==="
elif self.note == Note.REL:
note_str = "///"
elif self.note == Note.__:
note_str = "---"
else:
note_str = "%s%d" % (self.note, self.octave)
vol = "--" if self.volume==0xffff else "%02x" % self.volume
ins = "--" if self.instrument==0xffff else "%02x" % self.instrument
rep_str = "row data: %s %s %s"
for fx in self.effects:
cmd, val = fx
cmd_str = "--" if cmd == 0xffff else "%02x" % cmd
val_str = "--" if val == 0xffff else "%02x" % val
rep_str += " %s%s" % (cmd_str, val_str)
return "<" + rep_str % (
note_str,
ins, vol
) + ">"
@dataclass
class FurnacePattern:
"""
Represents one pattern in a module.
"""
channel: int = 0
index: int = 0
subsong: int = 0
data: List[FurnaceRow] = field(default_factory=list) # yeah...
name: str = ""
def as_clipboard(self) -> str:
"""
Renders the selected pattern in Furnace clipboard format.
:return: Furnace clipboard data
"""
return "org.tildearrow.furnace - Pattern Data\n0\n" + "\n".join([x.as_clipboard() for x in self.data])
def __str__(self) -> str:
return "<Furnace pattern %s for ch.%02d of subsong %02d>" % (
self.name if len(self.name) > 0 else "%02x" % self.index,
self.channel,
self.subsong
)
class InputPatchBayEntry(TypedDict):
"""
A patch that has an "input" connector.
"""
set: InputPortSet
"""
The set that the patch belongs to.
"""
port: int
"""
Which port to connect to.
"""
class OutputPatchBayEntry(TypedDict):
"""
A patch that has an "output" connector.
"""
set: OutputPortSet
"""
The set that the patch belongs to.
"""
port: int
"""
Which port to connect from.
"""
@dataclass
class PatchBay:
"""
A single patchbay connection.
"""
source: OutputPatchBayEntry
dest: InputPatchBayEntry
# instruments
@dataclass
class InsFeatureAbstract:
"""
Base class for all InsFeature* classes. Not really to be used.
"""
_code: str = field(init=False)
def __post_init__(self) -> None:
if len(self._code) != 2:
raise ValueError('No code defined for this instrument feature')
# def serialize(self) -> bytes:
# raise Exception('Method serialize() has not been overridden...')
@dataclass
class InsFeatureName(InsFeatureAbstract, str):
"""
Instrument's name block. Can be used as a string.
"""
_code = 'NA'
name: str = ''
def __str__(self) -> str:
return self.name
@dataclass
class InsMeta:
version: int = 143
type: InstrumentType = InstrumentType.FM_4OP
@dataclass
class InsFMOperator:
am: bool = False
ar: int = 0
dr: int = 0
mult: int = 0
rr: int = 0
sl: int = 0
tl: int = 0
dt2: int = 0
rs: int = 0
dt: int = 0
d2r: int = 0
ssg_env: int = 0
dam: int = 0
dvb: int = 0
egt: bool = False
ksl: int = 0
sus: bool = False
vib: bool = False
ws: int = 0
ksr: bool = False
enable: bool = True
kvs: int = 2
@dataclass
class InsFeatureFM(InsFeatureAbstract):
_code = 'FM'
alg: int = 0
fb: int = 4
fms: int = 0
ams: int = 0
fms2: int = 0
ams2: int = 0
ops: int = 2
opll_preset: int = 0
op_list: List[InsFMOperator] = field(default_factory=lambda: [
InsFMOperator(
tl=42, ar=31, dr=8,
sl=15, rr=3, mult=5,
dt=5
),
InsFMOperator(
tl=48, ar=31, dr=4,
sl=11, rr=1, mult=1,
dt=5
),
InsFMOperator(
tl=18, ar=31, dr=10,
sl=15, rr=4, mult=1,
dt=0
),
InsFMOperator(
tl=2, ar=31, dr=9,
sl=15, rr=9, mult=1,
dt=0
),
])
@dataclass
class SingleMacro:
kind: Union[MacroCode, OpMacroCode] = field(default_factory=lambda: MacroCode.VOL)
mode: int = 0
type: MacroType = field(default_factory=lambda: MacroType.SEQUENCE)
delay: int = 0
speed: int = 1
open: bool = False
data: List[Union[int, MacroItem]] = field(default_factory=list)
@dataclass
class InsFeatureMacro(InsFeatureAbstract):
_code = 'MA'
macros: List[SingleMacro] = field(default_factory=lambda: [SingleMacro()])
@dataclass
class InsFeatureOpr1Macro(InsFeatureMacro):
_code = 'O1'
@dataclass
class InsFeatureOpr2Macro(InsFeatureMacro):
_code = 'O2'
@dataclass
class InsFeatureOpr3Macro(InsFeatureMacro):
_code = 'O3'
@dataclass
class InsFeatureOpr4Macro(InsFeatureMacro):
_code = 'O4'
@dataclass
class GBHwSeq:
command: GBHwCommand
data: List[int] = field(default_factory=lambda: [0, 0])
@dataclass
class InsFeatureGB(InsFeatureAbstract):
_code = 'GB'
env_vol: int = 15
env_dir: int = 0
env_len: int = 2
sound_len: int = 0
soft_env: bool = False
always_init: bool = False
hw_seq: List[GBHwSeq] = field(default_factory=list)
@dataclass
class GenericADSR:
a: int = 0
d: int = 0
s: int = 0
r: int = 0
@dataclass
class InsFeatureC64(InsFeatureAbstract):
_code = '64'
tri_on: bool = False
saw_on: bool = True
pulse_on: bool = False
noise_on: bool = False
envelope: GenericADSR = field(default_factory=lambda: GenericADSR(a=0, d=8, s=0, r=0))
duty: int = 2048
ring_mod: int = 0
osc_sync: int = 0
to_filter: bool = False
vol_is_cutoff: bool = False
init_filter: bool = False
duty_is_abs: bool = False
filter_is_abs: bool = False
no_test: bool = False
res: int = 0
cut: int = 0
hp: bool = False
lp: bool = False
bp: bool = False
ch3_off: bool = False
@dataclass
class SampleMap:
freq: int = 0
sample_index: int = 0
@dataclass
class DPCMMap:
pitch: int = 0
delta: int = 0
@dataclass
class InsFeatureAmiga(InsFeatureAbstract): # Sample data
_code = 'SM'
init_sample: int = 0
use_note_map: bool = False
use_sample: bool = False
use_wave: bool = False
wave_len: int = 31
sample_map: List[SampleMap] = field(default_factory=lambda: [SampleMap() for _ in range(120)])
@dataclass
class InsFeatureDPCMMap(InsFeatureAbstract): # DPCM sample data
_code = 'NE'
use_map: bool = False
sample_map: List[DPCMMap] = field(default_factory=lambda: [SampleMap() for _ in range(120)])
@dataclass
class InsFeatureX1010(InsFeatureAbstract):
_code = 'X1'
bank_slot: int = 0
@dataclass
class InsFeaturePowerNoise(InsFeatureAbstract):
_code = 'PN'
octave: int = 0
@dataclass
class InsFeatureSID2(InsFeatureAbstract):
_code = 'S2'
noise_mode: int = 0
wave_mix: int = 0
volume: int = 0
@dataclass
class InsFeatureN163(InsFeatureAbstract):
_code = 'N1'
wave: int = -1
wave_pos: int = 0
wave_len: int = 32
wave_mode: int = 3
@dataclass
class InsFeatureFDS(InsFeatureAbstract): # Virtual Boy
_code = 'FD'
mod_speed: int = 0
mod_depth: int = 0
init_table_with_first_wave: bool = False # compat
mod_table: List[int] = field(default_factory=lambda: [0 for i in range(32)])
@dataclass
class InsFeatureMultiPCM(InsFeatureAbstract):
_code = 'MP'
ar: int = 15
d1r: int = 15
dl: int = 0
d2r: int = 0
rr: int = 15
rc: int = 15
lfo: int = 0
vib: int = 0
am: int = 0
@dataclass
class InsFeatureWaveSynth(InsFeatureAbstract):
_code = 'WS'
wave_indices: List[int] = field(default_factory=lambda: [0, 0])
rate_divider: int = 1
effect: WaveFX = WaveFX.NONE
enabled: bool = False
global_effect: bool = False
speed: int = 0
params: List[int] = field(default_factory=lambda: [0, 0, 0, 0])
one_shot: bool = False # not read?
@dataclass
class InsFeatureSoundUnit(InsFeatureAbstract):
_code = 'SU'
switch_roles: bool = False
@dataclass
class InsFeatureES5506(InsFeatureAbstract):
_code = 'ES'
filter_mode: ESFilterMode = ESFilterMode.LPK2_LPK1
k1: int = 0xffff
k2: int = 0xffff
env_count: int = 0
left_volume_ramp: int = 0
right_volume_ramp: int = 0
k1_ramp: int = 0
k2_ramp: int = 0
k1_slow: int = 0
k2_slow: int = 0
@dataclass
class InsFeatureSNES(InsFeatureAbstract):
_code = 'SN'
use_env: bool = True
sus: SNESSusMode = SNESSusMode.DIRECT
gain_mode: GainMode = GainMode.DIRECT
gain: int = 127
d2: int = 0
envelope: GenericADSR = field(default_factory=lambda: GenericADSR(a=15, d=7, s=7, r=0))
@dataclass
class InsFeatureOPLDrums(InsFeatureAbstract):
_code = 'LD'
fixed_drums: bool = False
kick_freq: int = 1312
snare_hat_freq: int = 1360
tom_top_freq: int = 448
@dataclass
class _InsFeaturePointerAbstract(InsFeatureAbstract):
"""
Also not really to be used. Container for all "list" features.
"""
_code = 'LL'
pointers: Dict[int, int] = field(default_factory=dict)
@dataclass
class InsFeatureSampleList(_InsFeaturePointerAbstract):
"""
List of pointers to all samples used by this instrument.
"""
_code = 'SL'
@dataclass
class InsFeatureWaveList(_InsFeaturePointerAbstract):
"""
List of pointers to all wave tables used by this instrument.
"""
_code = 'WL'
@dataclass
class WavetableMeta:
name: str = ''
width: int = 32
height: int = 32
@dataclass
class SampleMeta:
name: str = ''
length: int = 0
sample_rate: int = 0
bitdepth: int = 0
loop_start: int = 0
loop_end: int = 0

View file

@ -0,0 +1,648 @@
from chipchune._util import EnumShowNameOnly, EnumValueEquals
from typing import Tuple
class LinearPitch(EnumShowNameOnly, EnumValueEquals):
"""
Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.linear_pitch`.
"""
NON_LINEAR = 0
ONLY_PITCH_CHANGE = 1
FULL_LINEAR = 2
class LoopModality(EnumShowNameOnly, EnumValueEquals):
"""
Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.loop_modality`.
"""
HARD_RESET_CHANNELS = 0
SOFT_RESET_CHANNELS = 1
DO_NOTHING = 2
class DelayBehavior(EnumShowNameOnly, EnumValueEquals):
"""
Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.cut_delay_effect_policy`.
"""
STRICT = 0
BROKEN = 1
LAX = 2
class JumpTreatment(EnumShowNameOnly, EnumValueEquals):
"""
Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.jump_treatment`.
"""
ALL_JUMPS = 0
FIRST_JUMP_ONLY = 1
ROW_JUMP_PRIORITY = 2
class Note(EnumShowNameOnly):
"""
All notes recognized by Furnace
"""
__ = 0
Cs = 1
D_ = 2
Ds = 3
E_ = 4
F_ = 5
Fs = 6
G_ = 7
Gs = 8
A_ = 9
As = 10
B_ = 11
C_ = 12
OFF = 100
OFF_REL = 101
REL = 102
class MacroItem(EnumShowNameOnly):
"""
Special values used only in this parser, to allow data editing similar to that
of Furnace itself.
"""
LOOP = 0
RELEASE = 1
class MacroCode(EnumShowNameOnly, EnumValueEquals):
"""
Marks what aspect of an instrument does a macro change.
"""
VOL = 0
"""
Also:
- C64 cutoff
"""
ARP = 1
"""
Not applicable to MSM6258 and MSM6295.
"""
DUTY = 2
"""
Also:
- AY noise freq
- POKEY audctl
- Mikey duty/int
- MSM5232 group ctrl
- Beeper/Pokemon Mini pulse width
- T6W28 noise type
- Virtual Boy noise length
- PC Engine/Namco/WonderSwan noise type
- SNES noise freq
- Namco 163 waveform pos.
- ES5506 filter mode
- MSM6258/MSM6295 freq. divider
- ADPCMA global volume
- QSound echo level
"""
WAVE = 3
"""
Also:
- OPLL patch
- OPZ/OPM lfo1 shape
"""
PITCH = 4
EX1 = 5
"""
- OPZ/OPM am depth
- C64 filter mode
- SAA1099 envelope
- X1-010 env. mode
- Namco 163 wave length
- FDS mod depth
- TSU cutoff
- ES5506 filter k1
- MSM6258 clk divider
- QSound echo feedback
- SNES special
- MSM5232 group attack
- AY8930 duty?
"""
EX2 = 6
"""
- C64 resonance
- Namco 163 wave update
- FDS mod speed
- TSU resonance
- ES5506 filter k2
- QSound echo length
- SNES gain
- MSM5232 group decay
- AY3/AY8930 envelope
"""
EX3 = 7
"""
- C64 special
- AY/AY8930 autoenv num
- X1-010 autoenv num
- Namco 163 waveload wave
- FDS mod position
- TSU control
- MSM5232 noise
"""
ALG = 8
"""
Also:
- AY/AY8930 autoenv den
- X1-010 autoenv den
- Namco 163 waveload pos
- ES5506 control
"""
FB = 9
"""
Also:
- AY8930 noise & mask
- Namco 163 waveload len
- ES5506 outputs
"""
FMS = 10
"""
Also:
- AY8930 noise | mask
- Namco 163 waveload trigger
"""
AMS = 11
PAN_L = 12
PAN_R = 13
PHASE_RESET = 14
EX4 = 15
"""
- C64 test/gate
- TSU phase reset timer
- FM/OPM opmask
"""
EX5 = 16
"""
- OPZ am depth 2
"""
EX6 = 17
"""
- OPZ pm depth 2
"""
EX7 = 18
"""
- OPZ lfo2 speed
"""
EX8 = 19
"""
- OPZ lfo2 shape
"""
STOP = 255
"""
Marks end of macro reading.
"""
class OpMacroCode(EnumShowNameOnly, EnumValueEquals):
"""
Controls which FM parameter a macro should change.
"""
AM = 0
AR = 1
DR = 2
MULT = 3
RR = 4
SL = 5
TL = 6
DT2 = 7
RS = 8
DT = 9
D2R = 10
SSG_EG = 11
DAM = 12
DVB = 13
EGT = 14
KSL = 15
SUS = 16
VIB = 17
WS = 18
KSR = 19
class MacroType(EnumShowNameOnly):
"""
Instrument macro type (version 120+).
"""
SEQUENCE = 0
ADSR = 1
LFO = 2
class MacroSize(EnumShowNameOnly):
"""
Type of value stored in the instrument file.
"""
_value_: int
num_bytes: int
signed: bool
UINT8: Tuple[int, int, bool] = (0, 1, False)
INT8: Tuple[int, int, bool] = (1, 1, True)
INT16: Tuple[int, int, bool] = (2, 2, True)
INT32: Tuple[int, int, bool] = (3, 4, True)
def __new__(cls, id: int, num_bytes: int, signed: bool): # type: ignore[no-untyped-def]
member = object.__new__(cls)
member._value_ = id
setattr(member, 'num_bytes', num_bytes)
setattr(member, 'signed', signed)
return member
class GBHwCommand(EnumShowNameOnly):
"""
Game Boy hardware envelope commands.
"""
ENVELOPE = 0
SWEEP = 1
WAIT = 2
WAIT_REL = 3
LOOP = 4
LOOP_REL = 5
class SampleType(EnumShowNameOnly):
"""
Sample types used in Furnace
"""
ZX_DRUM = 0
NES_DPCM = 1
QSOUND_ADPCM = 4
ADPCM_A = 5
ADPCM_B = 6
X68K_ADPCM = 7
PCM_8 = 8
SNES_BRR = 9
VOX = 10
PCM_16 = 16
class InstrumentType(EnumShowNameOnly):
"""
Instrument types currently available as of version 144.
"""
STANDARD = 0
FM_4OP = 1
GB = 2
C64 = 3
AMIGA = 4
PCE = 5
SSG = 6
AY8930 = 7
TIA = 8
SAA1099 = 9
VIC = 10
PET = 11
VRC6 = 12
FM_OPLL = 13
FM_OPL = 14
FDS = 15
VB = 16
N163 = 17
KONAMI_SCC = 18
FM_OPZ = 19
POKEY = 20
PC_BEEPER = 21
WONDERSWAN = 22
LYNX = 23
VERA = 24
X1010 = 25
VRC6_SAW = 26
ES5506 = 27
MULTIPCM = 28
SNES = 29
TSU = 30
NAMCO_WSG = 31
OPL_DRUMS = 32
FM_OPM = 33
NES = 34
MSM6258 = 35
MSM6295 = 36
ADPCM_A = 37
ADPCM_B = 38
SEGAPCM = 39
QSOUND = 40
YMZ280B = 41
RF5C68 = 42
MSM5232 = 43
T6W28 = 44
K007232 = 45
GA20 = 46
POKEMON_MINI = 47
SM8521 = 48
PV1000 = 49
class ChipType(EnumShowNameOnly):
"""
Furnace chip database, either planned or implemented.
Contains console name, chip ID and number of channels.
"""
_value_: int
channels: int
YMU759 = (0x01, 17)
GENESIS = (0x02, 10) # YM2612 + SN76489
SMS = (0x03, 4) # SN76489
GB = (0x04, 4) # LR53902
PCE = (0x05, 6) # HuC6280
NES = (0x06, 5) # RP2A03
C64_8580 = (0x07, 3) # SID r8580
SEGA_ARCADE = (0x08, 13) # YM2151 + SegaPCM
NEO_GEO_CD = (0x09, 13)
GENESIS_EX = (0x42, 13) # YM2612 + SN76489
SMS_JP = (0x43, 13) # SN76489 + YM2413
NES_VRC7 = (0x46, 11) # RP2A03 + YM2413
C64_6581 = (0x47, 3) # SID r6581
NEO_GEO_CD_EX = (0x49, 16)
AY38910 = (0x80, 3)
AMIGA = (0x81, 4) # Paula
YM2151 = (0x82, 8) # YM2151
YM2612 = (0x83, 6) # YM2612
TIA = (0x84, 2)
VIC20 = (0x85, 4)
PET = (0x86, 1)
SNES = (0x87, 8) # SPC700
VRC6 = (0x88, 3)
OPLL = (0x89, 9) # YM2413
FDS = (0x8a, 1)
MMC5 = (0x8b, 3)
N163 = (0x8c, 8)
OPN = (0x8d, 6) # YM2203
PC98 = (0x8e, 16) # YM2608
OPL = (0x8f, 9) # YM3526
OPL2 = (0x90, 9) # YM3812
OPL3 = (0x91, 18) # YMF262
MULTIPCM = (0x92, 24)
PC_SPEAKER = (0x93, 1) # Intel 8253
POKEY = (0x94, 4)
RF5C68 = (0x95, 8)
WONDERSWAN = (0x96, 4)
SAA1099 = (0x97, 6)
OPZ = (0x98, 8)
POKEMON_MINI = (0x99, 1)
AY8930 = (0x9a, 3)
SEGAPCM = (0x9b, 16)
VIRTUAL_BOY = (0x9c, 6)
VRC7 = (0x9d, 6)
YM2610B = (0x9e, 16)
ZX_BEEPER = (0x9f, 6) # tildearrow's engine
YM2612_EX = (0xa0, 9)
SCC = (0xa1, 5)
OPL_DRUMS = (0xa2, 11)
OPL2_DRUMS = (0xa3, 11)
OPL3_DRUMS = (0xa4, 20)
NEO_GEO = (0xa5, 14)
NEO_GEO_EX = (0xa6, 17)
OPLL_DRUMS = (0xa7, 11)
LYNX = (0xa8, 4)
SEGAPCM_DMF = (0xa9, 5)
MSM6295 = (0xaa, 4)
MSM6258 = (0xab, 1)
COMMANDER_X16 = (0xac, 17) # VERA
BUBBLE_SYSTEM_WSG = (0xad, 2)
OPL4 = (0xae, 42)
OPL4_DRUMS = (0xaf, 44)
SETA = (0xb0, 16) # Allumer X1-010
ES5506 = (0xb1, 32)
Y8950 = (0xb2, 10)
Y8950_DRUMS = (0xb3, 12)
SCC_PLUS = (0xb4, 5)
TSU = (0xb5, 8)
YM2203_EX = (0xb6, 9)
YM2608_EX = (0xb7, 19)
YMZ280B = (0xb8, 8)
NAMCO = (0xb9, 3) # Namco WSG
N15XX = (0xba, 8) # Namco 15xx
CUS30 = (0xbb, 8) # Namco CUS30
MSM5232 = (0xbc, 8)
YM2612_PLUS_EX = (0xbd, 11)
YM2612_PLUS = (0xbe, 7)
T6W28 = (0xbf, 4)
PCM_DAC = (0xc0, 1)
YM2612_CSM = (0xc1, 10)
NEO_GEO_CSM = (0xc2, 18) # YM2610 CSM
YM2203_CSM = (0xc3, 10)
YM2608_CSM = (0xc4, 20)
YM2610B_CSM = (0xc5, 20)
K007232 = (0xc6, 2)
GA20 = (0xc7, 4)
SM8521 = (0xc8, 3)
M114S = (0xc9, 16)
ZX_BEEPER_QUADTONE: Tuple[int, int] = (0xca, 5) # Natt Akuma's engine
PV_1000: Tuple[int, int] = (0xcb, 3) # NEC D65010G031
K053260 = (0xcc, 4)
TED = (0xcd, 2)
NAMCO_C140 = (0xce, 24)
NAMCO_C219 = (0xcf, 16)
NAMCO_C352 = (0xd0, 32)
ESFM = (0xd1, 18)
ES5503 = (0xd2, 32)
POWERNOISE = (0xd4, 4)
DAVE = (0xd5, 6)
NDS = (0xd6, 16)
GBA = (0xd7, 2)
GBA_MINMOD = (0xd8, 16)
BIFURCATOR = (0xd9, 4)
YM2610B_EX = (0xde, 19)
QSOUND = (0xe0, 19)
SID2 = (0xf0, 3) # SID2
FIVEE01 = (0xf1, 5) # 5E01
PONG = (0xfc, 1)
DUMMY = (0xfd, 1)
RESERVED_1 = (0xfe, 1)
RESERVED_2 = (0xff, 1)
def __new__(cls, id: int, channels: int): # type: ignore[no-untyped-def]
member = object.__new__(cls)
member._value_ = id
setattr(member, 'channels', channels)
return member
def __repr__(self) -> str:
# repr abuse
# about as stupid as "mapping for the renderer"...
return "%s (0x%02x), %d channel%s" % (
self.name, self._value_, self.channels,
"s" if self.channels != 1 else ""
)
class InputPortSet(EnumShowNameOnly):
"""
Devices which contain an "input" port.
"""
SYSTEM = 0
NULL = 0xFFF
class OutputPortSet(EnumShowNameOnly):
"""
Devices which contain an "output" port.
"""
CHIP_1 = 0
CHIP_2 = 1
CHIP_3 = 2
CHIP_4 = 3
CHIP_5 = 4
CHIP_6 = 5
CHIP_7 = 6
CHIP_8 = 7
CHIP_9 = 8
CHIP_10 = 9
CHIP_11 = 10
CHIP_12 = 11
CHIP_13 = 12
CHIP_14 = 13
CHIP_15 = 14
CHIP_16 = 15
CHIP_17 = 16
CHIP_18 = 17
CHIP_19 = 18
CHIP_20 = 19
CHIP_21 = 20
CHIP_22 = 21
CHIP_23 = 22
CHIP_24 = 23
CHIP_25 = 24
CHIP_26 = 25
CHIP_27 = 26
CHIP_28 = 27
CHIP_29 = 28
CHIP_30 = 29
CHIP_31 = 30
CHIP_32 = 31
PREVIEW = 0xFFD
METRONOME = 0xFFE
NULL = 0xFFF
class WaveFX(EnumShowNameOnly):
"""
Used in :attr:`chipchune.furnace.data_types.InsFeatureWaveSynth.effect`.
"""
NONE = 0
# single waveform
INVERT = 1
ADD = 2
SUBTRACT = 3
AVERAGE = 4
PHASE = 5
CHORUS = 6
# double waveform
NONE_DUAL = 128
WIPE = 129
FADE = 130
PING_PONG = 131
OVERLAY = 132
NEGATIVE_OVERLAY = 133
SLIDE = 134
MIX = 135
PHASE_MOD = 136
class ESFilterMode(EnumShowNameOnly):
"""
Used in :attr:`chipchune.furnace.data_types.InsFeatureES5506.filter_mode`.
"""
HPK2_HPK2 = 0
HPK2_LPK1 = 1
LPK2_LPK2 = 2
LPK2_LPK1 = 3
class GainMode(EnumShowNameOnly):
"""
Used in :attr:`chipchune.furnace.data_types.InsFeatureSNES.gain_mode`.
"""
DIRECT = 0
DEC_LINEAR = 4
DEC_LOG = 5
INC_LINEAR = 6
INC_INVLOG = 7
class SNESSusMode(EnumShowNameOnly):
"""
Used in :attr:`chipchune.furnace.data_types.InsFeatureSNES.sus`.
"""
DIRECT = 0
SUS_WITH_DEC = 1
SUS_WITH_EXP = 2
SUS_WITH_REL = 3
class _FurInsImportType(EnumShowNameOnly, EnumValueEquals):
"""
Also only used in this parser to differentiate between different types of instrument formats.
"""
# Old format
FORMAT_0_FILE = 0
FORMAT_0_EMBED = 1
# Dev127 format
FORMAT_1_FILE = 2
FORMAT_1_EMBED = 3
class _FurWavetableImportType(EnumShowNameOnly, EnumValueEquals):
"""
Also only used in this parser to differentiate between different types of wavetable formats.
"""
FILE = 0
EMBED = 1
class _FurSampleType(EnumShowNameOnly, EnumValueEquals):
"""
Also only used in this parser to differentiate between different types of sample formats.
"""
PCM_1_BIT = 0
DPCM = 1
YMZ = 3
QSOUND = 4
ADPCM_A = 5
ADPCM_B = 6
K05_ADPCM = 7
PCM_8_BIT = 8
BRR = 9
VOX = 10
ULAW = 11
C219 = 12
IMA = 13
PCM_16_BIT = 16

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
from io import BytesIO
from typing import Optional, Union, BinaryIO, List
from chipchune._util import read_short, read_int, read_str
from .data_types import SampleMeta
from .enums import _FurSampleType
FILE_MAGIC_STR = b'SMP2'
class FurnaceSample:
def __init__(self) -> None:
self.meta: SampleMeta = SampleMeta()
"""
Sample metadata.
"""
self.data: bytearray = b''
"""
Sample data.
"""
def load_from_stream(self, stream: BinaryIO) -> None:
"""
Load a sample from an **uncompressed** stream.
:param stream: File-like object containing the uncompressed wavetable.
"""
if stream.read(len(FILE_MAGIC_STR)) != FILE_MAGIC_STR:
raise ValueError('Bad magic value for a wavetable file')
blk_size = read_int(stream)
if blk_size > 0:
smp_data = BytesIO(stream.read(blk_size))
else:
smp_data = stream
self.meta.name = read_str(smp_data)
self.meta.length = read_int(smp_data)
read_int(smp_data) # compatablity rate
self.meta.sample_rate = read_int(smp_data)
self.meta.depth = int(smp_data.read(1)[0])
smp_data.read(1) # loop direction
smp_data.read(1) # flags
smp_data.read(1) # flags 2
self.meta.loop_start = read_int(smp_data)
self.meta.loop_end = read_int(smp_data)
smp_data.read(16) # sample presence bitfields
self.data = smp_data.read(self.meta.length)

View file

@ -0,0 +1,101 @@
from io import BytesIO
from typing import Optional, Union, BinaryIO, List
from chipchune._util import read_short, read_int, read_str
from .data_types import WavetableMeta
from .enums import _FurWavetableImportType
FILE_MAGIC_STR = b'-Furnace waveta-'
EMBED_MAGIC_STR = b'WAVE'
class FurnaceWavetable:
def __init__(self, file_name: Optional[str] = None) -> None:
"""
Creates or opens a new Furnace wavetable as a Python object.
:param file_name: (Optional)
If specified, then it will parse a file as a FurnaceWavetable. If file name (str) is
given, it will load that file.
Defaults to None.
"""
self.file_name: Optional[str] = None
"""
Original file name, if the object was initialized with one.
"""
self.meta: WavetableMeta = WavetableMeta()
"""
Wavetable metadata.
"""
self.data: List[int] = []
"""
Wavetable data.
"""
if isinstance(file_name, str):
self.load_from_file(file_name)
def load_from_file(self, file_name: Optional[str] = None) -> None:
if isinstance(file_name, str):
self.file_name = file_name
if self.file_name is None:
raise RuntimeError('No file name set, either set self.file_name or pass file_name to the function')
# since we're loading from an uncompressed file, we can just check the file magic number
with open(self.file_name, 'rb') as f:
detect_magic = f.peek(len(FILE_MAGIC_STR))[:len(FILE_MAGIC_STR)]
if detect_magic == FILE_MAGIC_STR:
return self.load_from_stream(f, _FurWavetableImportType.FILE)
else: # uncompressed for sure
raise ValueError('No recognized file type magic')
def load_from_bytes(self, data: bytes, import_as: Union[int, _FurWavetableImportType]) -> None:
"""
Load a wavetable from a series of bytes.
:param data: Bytes
"""
return self.load_from_stream(
BytesIO(data),
import_as
)
def load_from_stream(self, stream: BinaryIO, import_as: Union[int, _FurWavetableImportType]) -> None:
"""
Load a wavetable from an **uncompressed** stream.
:param stream: File-like object containing the uncompressed wavetable.
:param import_as: int
- 0 = wavetable file
- 1 = wavetable embedded in module
"""
if import_as == _FurWavetableImportType.FILE:
if stream.read(len(FILE_MAGIC_STR)) != FILE_MAGIC_STR:
raise ValueError('Bad magic value for a wavetable file')
version = read_short(stream)
read_short(stream) # reserved
self.__load_embed(stream)
elif import_as == _FurWavetableImportType.EMBED:
return self.__load_embed(stream)
else:
raise ValueError('Invalid import type')
def __load_embed(self, stream: BinaryIO) -> None:
if stream.read(len(EMBED_MAGIC_STR)) != EMBED_MAGIC_STR:
raise RuntimeError('Bad magic value for a wavetable embed')
blk_size = read_int(stream)
if blk_size > 0:
wt_data = BytesIO(stream.read(blk_size))
else:
wt_data = stream
self.meta.name = read_str(wt_data)
self.meta.width = read_int(wt_data)
read_int(wt_data) # reserved
self.meta.height = read_int(wt_data) + 1 # serialized height is 1 lower than actual value
self.data = [read_int(wt_data) for _ in range(self.meta.width)]

View file

@ -0,0 +1,7 @@
"""
Generic format for manipulating to, from, and between tracker formats.
- :mod:`chipchune.interchange.enums`: Various constants.
- :mod:`chipchune.interchange.furnace`: Adapters for Furnace.
"""

View file

@ -0,0 +1,24 @@
import enum
from chipchune._util import EnumShowNameOnly
class InterNote(EnumShowNameOnly):
"""
Common note interchange format.
"""
__ = enum.auto() # Signifies a blank space in tracker
C_ = enum.auto()
Cs = enum.auto()
D_ = enum.auto()
Ds = enum.auto()
E_ = enum.auto()
F_ = enum.auto()
Fs = enum.auto()
G_ = enum.auto()
Gs = enum.auto()
A_ = enum.auto()
As = enum.auto()
B_ = enum.auto()
Off = enum.auto()
OffRel = enum.auto()
Rel = enum.auto()
Echo = enum.auto()

View file

@ -0,0 +1,52 @@
from chipchune.furnace.enums import Note as FurnaceNote
from chipchune.interchange.enums import InterNote
def furnace_note_to_internote(note: FurnaceNote) -> InterNote:
"""
Convert a Furnace note into an InterNote.
Raises:
- Exception: If the supplied note is out of range.
"""
if note == FurnaceNote.__: return InterNote.__
elif note == FurnaceNote.C_: return InterNote.C_
elif note == FurnaceNote.Cs: return InterNote.Cs
elif note == FurnaceNote.D_: return InterNote.D_
elif note == FurnaceNote.Ds: return InterNote.Ds
elif note == FurnaceNote.E_: return InterNote.E_
elif note == FurnaceNote.F_: return InterNote.F_
elif note == FurnaceNote.Fs: return InterNote.Fs
elif note == FurnaceNote.G_: return InterNote.G_
elif note == FurnaceNote.Gs: return InterNote.Gs
elif note == FurnaceNote.A_: return InterNote.A_
elif note == FurnaceNote.As: return InterNote.As
elif note == FurnaceNote.B_: return InterNote.B_
elif note == FurnaceNote.OFF: return InterNote.Off
elif note == FurnaceNote.OFF_REL: return InterNote.OffRel
elif note == FurnaceNote.REL: return InterNote.Rel
else:
raise Exception("Invalid note value %s" % note)
def internote_to_furnace_note(note: InterNote) -> FurnaceNote:
"""
Convert an InterNote into a Furnace note. If the equivalent
value is unable to be determined, a blank note `__` is returned.
"""
if note == InterNote.__: return FurnaceNote.__
elif note == InterNote.C_: return FurnaceNote.C_
elif note == InterNote.Cs: return FurnaceNote.Cs
elif note == InterNote.D_: return FurnaceNote.D_
elif note == InterNote.Ds: return FurnaceNote.Ds
elif note == InterNote.E_: return FurnaceNote.E_
elif note == InterNote.F_: return FurnaceNote.F_
elif note == InterNote.Fs: return FurnaceNote.Fs
elif note == InterNote.G_: return FurnaceNote.G_
elif note == InterNote.Gs: return FurnaceNote.Gs
elif note == InterNote.A_: return FurnaceNote.A_
elif note == InterNote.As: return FurnaceNote.As
elif note == InterNote.B_: return FurnaceNote.B_
elif note == InterNote.Off: return FurnaceNote.OFF
elif note == InterNote.OffRel: return FurnaceNote.OFF_REL
elif note == InterNote.Rel: return FurnaceNote.REL
else:
return FurnaceNote.__

View file

@ -0,0 +1,5 @@
"""
Utilities for manipulating, converting, etc. tracker data.
- :mod:`chipchune.utils.conversion`: Conversion tools.
"""

View file

@ -0,0 +1,94 @@
from chipchune.furnace.module import FurnacePattern
from chipchune.interchange.enums import InterNote
from chipchune.interchange.furnace import furnace_note_to_internote
from typing import Union, List, Tuple
from dataclasses import dataclass, field
@dataclass
class SequenceEntry:
"""
A representation of a row in note-length format. Such a format is commonly
used across different sound engines and sequenced data.
A pattern can be turned into a list of SequenceEntries, which should be easier
to convert into a format of your choice.
"""
note: InterNote
length: int
volume: int
"""
Tracker-defined volume; although this should be -1 for undefined values.
"""
octave: int
"""
Tracker-defined octave; although this should be -1 for undefined values.
"""
instrument: int
"""
Tracker-defined instrument number; although this should be -1 for undefined values.
"""
effects: List[Tuple[int, int]] = field(default_factory=list)
"""
Tracker-defined effects list; if undefined, this should be empty.
"""
def pattern_to_sequence(pattern: Union[FurnacePattern, None]) -> List[SequenceEntry]:
"""
Interface to convert a pattern from tracker rows to a "sequence", which is
really a list of SequenceEntries.
:param pattern:
A pattern object. Supported types at the moment: `FurnacePattern`.
Anything outside of the supported types will throw a `TypeError`.
"""
if isinstance(pattern, FurnacePattern):
return furnace_pattern_to_sequence(pattern)
else:
raise TypeError("Invalid pattern type; must be one of: FurnacePattern")
def furnace_pattern_to_sequence(pattern: FurnacePattern) -> List[SequenceEntry]:
converted: List[SequenceEntry] = []
last_volume = -1
for i in pattern.data:
note = furnace_note_to_internote(i.note)
effects = i.effects
volume = i.volume
instrument = i.instrument
if effects == [(65535, 65535)]:
effects = []
if volume == 65535:
volume = last_volume
else:
last_volume = volume
if instrument == 65535:
instrument = -1
if note == InterNote.__:
if len(converted) == 0:
converted.append(
SequenceEntry(
note=InterNote.__,
length=1,
volume=volume,
octave=i.octave,
instrument=instrument,
effects=effects,
)
)
else:
converted[-1].length += 1
else:
converted.append(
SequenceEntry(
note=note,
length=1,
volume=volume,
octave=i.octave,
instrument=instrument,
effects=effects,
)
)
return converted

View file

@ -0,0 +1,14 @@
@echo off
if [%1]==[] goto usage
python3 convert_to_asm.py %1
echo converted .fur file to .asm!
cd asm
cl65 -d -vm -l furC64.lst -g -u __EXEHDR__ -t c64 -C .\c64-asm.cfg -m furC64.map -Ln furC64.lbl -o furC64-test.prg furC64.asm
@echo compiled .prg file at asm/furC64-test.prg
cd ..
goto :eof
:usage
@echo No arguments supplied
@echo Make sure to run this command with an argument
@echo example: convert.bat test_file.fur
exit /B 1

View file

@ -0,0 +1,15 @@
#!/bin/bash
if [ $# -eq 0 ]
then
echo "No arguments supplied"
echo "Make sure to run this command with an argument"
echo "example: convert.sh test_file.fur"
else
python3 convert_to_asm.py $1
echo "converted .fur file to .asm!"
cd asm
cl65 -d -vm -l furC64.lst -g -u __EXEHDR__ -t c64 -C ./exe.cfg -m furC64.map -Ln furC64.lbl -o furC64-test.prg furC64.asm
cl65 -d -C ./bin.cfg -o song.bin furC64.asm
echo "compiled .prg file at asm/furC64-test.prg"
cd ..
fi

View file

@ -0,0 +1,780 @@
from chipchune.furnace.module import FurnaceModule
from chipchune.furnace.data_types import InsFeatureMacro, InsFeatureC64, InsFeatureAmiga
from chipchune.furnace.enums import MacroCode, MacroItem, MacroType
from chipchune.furnace.enums import InstrumentType
import sys
subsong = 0
note_transpose = 0
dups = {}
print(sys.argv)
module = FurnaceModule(sys.argv[1])
chnum = module.get_num_channels()
speed_type = len(module.subsongs[subsong].speed_pattern)
notes = ["C_","Cs","D_","Ds","E_","F_","Fs","G_","Gs","A_","As","B_"]
def comp(pat):
i = 0
o = []
n = 0
while i < len(pat):
j = i
k = 0
if pat[i] >= 0x40 and pat[i] < 128: i += 1
elif pat[i] == 0xFB: i += 2
elif pat[i] == 0xFC: i += 2
elif pat[i] == 0xE0: i += 2
elif pat[i] == 0xE1: i += 2
elif pat[i] == 0xE2: i += 2
elif pat[i] == 0xE3: i += 2
elif pat[i] == 0xE4: i += 2
elif pat[i] == 0xE5: i += 3
elif pat[i] == 0xE6: i += 2
elif pat[i] == 0xE7: i += 2
elif pat[i] == 0xE8: i += 2
elif pat[i] == 0xE9: i += 3
elif pat[i] == 0xEA: i += 3
elif pat[i] == 0xEB: i += 2
elif pat[i] == 0xEC: i += 2
elif pat[i] == 0xED: i += 2
elif pat[i] == 0xEE: i += 2
elif pat[i] == 0xEF: i += 1
elif pat[i] == 0xF0: i += 1
elif pat[i] == 0xF1: i += 2
elif pat[i] == 0xFF: i += 2
elif pat[i] == 0xFD:
i += 1
n = 2
elif pat[i] == 0xFE:
i += 1
n = 2
elif pat[i] >= 128:
i += 1
n = 2
else:
k = 1
if n == 0:
o.append(pat[i])
elif pat[i] > 1:
o.append(pat[i]-1)
#print(i,pat[i])
i += 1
n = max(n-1,0)
if k == 0:
o.extend(pat[j:i])
#print(pat,"\n",o,"\n")
return o
def conv_pattern(pattern):
out = [0]
oldtemp = [0,0]
r = 0
bitind = 0
oldins = -1
for row in pattern.data:
has03xx = 0
for l in row.effects:
k = list(l)
if k[0] == 0x03 and k[1] > 0:
has03xx = 1
break
temp = []
notnote = 0
new_byte = 0
if row.instrument != 65535 and oldins != row.instrument:
new_byte = 1
if row.instrument < 0x40:
temp.append(row.instrument+0x40)
else:
temp.append(0xFB)
temp.append(row.instrument)
oldins = row.instrument
if row.volume != 65535:
new_byte = 1
temp.append(0xFC)
temp.append(row.volume)
hasEffect = [-1,-1]
has0Dxx = -1
for l in row.effects:
k = list(l)
if k[1] == 65535:
k[1] = 0
if k[0] == 0xD:
new_byte = 1
has0Dxx = k[1]
continue
if k[0] == 0x0B:
new_byte = 1
temp.extend([0xED, k[1]])
has0Dxx = 0
continue
if (k[0] == 0x09 or k[0] == 0x0F) and (speed_type == 1):
new_byte = 1
temp.extend([0xE1, k[1]])
temp.extend([0xE0, k[1]])
continue
if k[0] == 0x0F and (speed_type == 2):
new_byte = 1
temp.extend([0xE1, k[1]])
continue
if k[0] == 0x09 and (speed_type == 2):
new_byte = 1
temp.extend([0xE0, k[1]])
continue
if k[0] == 0x00:
new_byte = 1
temp.extend([0xE2, k[1]])
continue
if k[0] == 0x01:
new_byte = 1
temp.extend([0xE3, k[1]])
continue
if k[0] == 0x02:
new_byte = 1
temp.extend([0xE4, k[1]])
continue
if k[0] == 0x03 and k[1] == 0:
new_byte = 1
temp.extend([0xE4, 0])
continue
if k[0] == 0x03 and k[1] > 0:
new_byte = 1
temp.extend([0xE5, k[1], max(min(notes.index(str(row.note))+(row.octave*12)+note_transpose,95),0)])
continue
if k[0] == 0x04:
new_byte = 1
temp.extend([0xE6, k[1]])
continue
if k[0] == 0x1B:
new_byte = 1
temp.extend([0xE7, k[1]])
continue
if k[0] == 0x1C:
new_byte = 1
temp.extend([0xE8, k[1]])
continue
if k[0] == 0xE1:
new_byte = 1
temp.extend([0xE9, k[1]>>4, k[1]&15])
continue
if k[0] == 0xE2:
new_byte = 1
temp.extend([0xEA, k[1]>>4, k[1]&15])
continue
if k[0] == 0xE5:
new_byte = 1
temp.extend([0xEB, k[1]])
continue
if k[0] == 0xEC:
new_byte = 1
temp.extend([0xEC, k[1]])
continue
if (k[0]>>4) == 4:
new_byte = 1
temp.extend([0xEE, (k[1]|(k[0]&0xf)<<8)>>3])
continue
if k[0] == 0xEA:
new_byte = 1
if k[1] == 0:
temp.extend([0xEF])
else:
temp.extend([0xF0])
continue
if k[0] == 0x1A:
new_byte = 1
if k[1] > 0:
temp.extend([0xF1, 0x00])
else:
temp.extend([0xF1, 0xFF])
continue
if str(row.note) == "OFF_REL":
notnote = 1
new_byte = 1
temp.append(0xFD)
elif str(row.note) == "REL":
notnote = 1
new_byte = 1
temp.append(0xFD)
elif str(row.note) == "OFF":
notnote = 1
new_byte = 1
temp.append(0xFE)
elif str(row.note) == "__" or (has03xx == 1):
if has03xx == 0:
notnote = 1
#temp.append(0x80)
else:
new_byte = 1
temp.append(max(min(notes.index(str(row.note))+(row.octave*12)+note_transpose,95),0)+0x80)
if new_byte == 1:
temp.append(0)
out.extend(temp)
durpass = False
if out[-1] >= 63:
out.append(0)
if has0Dxx > -1:
out[-1] += 1
if out[0] == 0: out = out[1:]
out.extend([0xFF, has0Dxx])
return out
out[-1] += 1
r += 1
out.extend([0xFF, 0])
if out[0] == 0: out = out[1:]
return out
f = open("asm/song.asm","w")
relW = []
relA = []
relD = []
relC = []
f.write("ticks_init:")
f.write(".byte ")
if speed_type == 1:
f.write(str(module.subsongs[subsong].speed_pattern[0])+", ")
f.write(str(module.subsongs[subsong].speed_pattern[0])+"\n")
elif speed_type == 2:
f.write(str(module.subsongs[subsong].speed_pattern[0])+", ")
f.write(str(module.subsongs[subsong].speed_pattern[1])+"\n")
f.write("insFL:\n")
f.write(".lobytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"F")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
f.write("insFH:\n")
f.write(".hibytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"F")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
f.write("insAL:\n")
f.write(".lobytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"A")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
f.write("insAH:\n")
f.write(".hibytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"A")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
f.write("insDL:\n")
f.write(".lobytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"D")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
f.write("insDH:\n")
f.write(".hibytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"D")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
f.write("insWL:\n")
f.write(".lobytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"W")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
f.write("insWH:\n")
f.write(".hibytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"W")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
f.write("insCL:\n")
f.write(".lobytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"C")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
f.write("insCH:\n")
f.write(".hibytes ")
for i in range(len(module.instruments)):
f.write("ins"+str(i)+"C")
if i == len(module.instruments)-1:
f.write("\n")
else:
f.write(", ")
for i in range(len(module.instruments)):
features = module.instruments[i].features
a = filter(
lambda x: (
type(x) == InsFeatureMacro
), features
)
macros = []
for j in a:
macros = j.macros
hasWaveMacro = 0
hasCutMacro = 0
for j in macros:
kind = j.kind
if kind == MacroCode.WAVE:
hasWaveMacro = 1
continue
if kind == MacroCode.ALG:
hasCutMacro = 1
continue
hasAbsFilter = False
hasAbsDuty = False
written_ins = False
a = filter(
lambda x: (
type(x) == InsFeatureMacro
), features
)
macros = []
for j in a:
macros = j.macros
for j in macros:
kind = j.kind
if kind == MacroCode.DUTY and j.type == MacroType.LFO:
hasAbsDuty = True
a = filter(
lambda x: (
type(x) == InsFeatureC64
), features
)
for j in a:
wave = j.tri_on
wave |= j.saw_on<<1
wave |= j.pulse_on<<2
wave |= j.noise_on<<3
wave <<= 4
wave |= j.ring_mod<<2
wave |= j.osc_sync<<1
f.write("ins"+str(i)+"F:\n")
f.write(".byte ")
f.write(str(wave)+", ")
ad = (j.envelope.a<<4)|j.envelope.d
sr = (j.envelope.s<<4)|j.envelope.r
f.write(str(ad)+", ")
f.write(str(sr)+", ")
f.write(str(j.duty&0xff)+", ")
f.write(str(j.duty>>8)+", ")
flags = 0
if j.duty_is_abs:
flags |= 1
if hasWaveMacro:
flags |= 2
if j.to_filter:
flags |= 4
if j.init_filter:
flags |= 8
if j.filter_is_abs:
flags |= 16
if hasCutMacro:
flags |= 32
if not j.no_test:
flags |= 64
f.write(str(flags)+", ")
fil = 0
fil = j.lp<<4
fil |= j.bp<<5
fil |= j.hp<<6
fil |= j.ch3_off<<7
f.write(str(j.res<<4)+", ")
f.write(str(fil)+", ")
f.write(str(j.cut&0xff)+", ")
f.write(str((j.cut>>8)&15)+"\n")
f.write("\n")
written_ins = True
hasAbsDuty |= j.duty_is_abs
hasAbsFilter = j.filter_is_abs
if written_ins == False:
f.write("ins"+str(i)+"F:\n")
f.write(".byte 32, 8, 0, 255, 7, 0\n")
a = filter(
lambda x: (
type(x) == InsFeatureMacro
), features
)
arp = [128,0xFF,0xFF]
duty = [0xFF,0xFF]
wave = [0xFF,0xFF]
cutoff = [0xFF,0xFF]
macros = []
for j in a:
macros = j.macros
hasRelTotal = [0,0,0,0]
for j in macros:
kind = j.kind
if kind == MacroCode.ARP:
s = j.speed
arp = []
loop = 0xff
hasRel = 0
oldlen = 0
if j.data[-1] == MacroItem.LOOP:
arr = [MacroItem.LOOP, j.data[-2]]
j.data = j.data[:-2] + arr
for k in j.data:
if k == MacroItem.LOOP:
loop = oldlen
elif k == MacroItem.RELEASE:
arp.append(0xFF)
arp.append(loop)
relA.append(len(arp))
oldlen = max(len(arp),0)
hasRel = 1
elif (k>>30) > 0:
arp.append(0xFE)
k = abs(k^(1<<30))
k = max(min(k,95),0)
arp.append(k%120)
oldlen = max(len(arp),0)
else:
if k < 0:
arp.append((k%120)-120+128)
else:
arp.append((k%120)+128)
oldlen = max(len(arp),0)
if hasRel == 0:
relA.append(len(arp))
hasRelTotal[1] = 1
len_temp = len(arp)
arp.append(0xFF)
arp.append(loop if loop!=len_temp else 0xff)
if kind == MacroCode.DUTY and j.type == MacroType.LFO:
while type(j.data[0]) is not int:
j.data = j.data[1:]
s = j.speed
duty = []
hasRel = 0
lfo = j.data[13]
while lfo <= 1023:
lfo += j.data[11]
if lfo > 1023: break
k = lfo
if k & 512:
k = 1023-lfo
k >>= 1
k = j.data[0]+((k+(j.data[1]-j.data[0])*k)>>8)
if (k&0xff) == 255:
duty.append(254)
else:
duty.append(k&0xff)
duty.append(k>>8)
if hasRel == 0:
relD.append(0)
hasRelTotal[2] = 1
duty.append(0xFF)
duty.append(0)
elif kind == MacroCode.DUTY:
s = j.speed
duty = []
loop = 0xff
loop2 = 0
hasRel = 0
for k in j.data:
if k == MacroItem.LOOP:
loop = loop2
elif k == MacroItem.RELEASE:
duty.append(0xFF)
duty.append(loop)
relD.append(len(duty))
hasRel = 1
else:
loop2 = len(duty)+2
if hasAbsDuty:
if (k&0xff) == 255:
duty.append(254)
else:
duty.append(k&0xff)
duty.append(k>>8)
else:
k = 32768-(k*4) #4)
duty.append(k&0xff)
duty.append(k>>8)
if hasRel == 0:
relD.append(len(duty))
hasRelTotal[2] = 1
len_temp = len(duty)
duty.append(0xFF)
duty.append(loop if loop!=len_temp else 0xff)
if kind == MacroCode.ALG and j.type == MacroType.LFO:
while type(j.data[0]) is not int:
j.data = j.data[1:]
s = j.speed
cutoff = []
hasRel = 0
lfo = j.data[13]
while lfo <= 1023:
lfo += j.data[11]
k = lfo
if k & 512:
k = 1023-lfo
k >>= 1
k = j.data[0]+((k+(j.data[1]-j.data[0])*k)>>8)
if (k&0xff) == 255:
cutoff.append(254)
else:
cutoff.append(k&0xff)
cutoff.append(k>>8)
if hasRel == 0:
relC.append(len(cutoff))
hasRelTotal[3] = 1
cutoff.append(0xFF)
cutoff.append(0)
elif kind == MacroCode.ALG:
s = j.speed
cutoff = []
loop = 0xff
loop2 = 0
hasRel = 0
for k in j.data:
if k == MacroItem.LOOP:
loop = loop2
elif k == MacroItem.RELEASE:
cutoff.append(0xFF)
cutoff.append(loop)
relC.append(len(cutoff))
hasRel = 1
else:
loop2 = len(cutoff)+1
if hasAbsFilter:
if (k&0xff) == 255:
cutoff.append(254)
else:
cutoff.append(k&0xff)
cutoff.append(k>>8)
else:
k = 32768+k*7
cutoff.append(k&0xff)
cutoff.append(k>>8)
if hasRel == 0:
relC.append(len(cutoff))
hasRelTotal[3] = 1
len_temp = len(cutoff)
cutoff.append(0xFF)
cutoff.append(loop if loop!=len_temp else 0xff)
if kind == MacroCode.WAVE:
s = j.speed
wave = []
loop = 0xff
loop2 = 0
hasRel = 0
for k in j.data:
if k == MacroItem.LOOP:
loop = len(wave)
elif k == MacroItem.RELEASE:
wave.append(0xFF)
wave.append(loop)
relW.append(len(wave))
hasRel = 1
else:
wave.append(k<<4)
if hasRel == 0:
relW.append(len(wave))
hasRelTotal[0] = 1
len_temp = len(wave)
wave.append(0xFF)
wave.append(loop if loop!=len_temp else 0xff)
if hasRelTotal[0] == 0:
relW.append(0)
if hasRelTotal[1] == 0:
relA.append(0)
if hasRelTotal[2] == 0:
relD.append(0)
if hasRelTotal[3] == 0:
relC.append(0)
wave = str(wave)[1:-1]
duty = str(duty)[1:-1]
arp = str(arp)[1:-1]
cutoff = str(cutoff)[1:-1]
if arp in dups:
f.write("ins"+str(i)+"A = "+dups[arp]+"\n")
else:
f.write("ins"+str(i)+"A:\n")
f.write(".byte "+arp+"\n")
dups[arp] = "ins"+str(i)+"A"
if duty in dups:
f.write("ins"+str(i)+"D = "+dups[duty]+"\n")
else:
f.write("ins"+str(i)+"D:\n")
f.write(".byte "+duty+"\n")
dups[duty] = "ins"+str(i)+"D"
if wave in dups:
f.write("ins"+str(i)+"W = "+dups[wave]+"\n")
else:
f.write("ins"+str(i)+"W:\n")
f.write(".byte "+wave+"\n")
dups[wave] = "ins"+str(i)+"W"
if cutoff in dups:
f.write("ins"+str(i)+"C = "+dups[cutoff]+"\n")
else:
f.write("ins"+str(i)+"C:\n")
f.write(".byte "+cutoff+"\n")
dups[cutoff] = "ins"+str(i)+"C"
relW = str(relW)[1:-1]
relD = str(relD)[1:-1]
relA = str(relA)[1:-1]
f.write("insArel:\n")
f.write(".byte "+relA+"\n")
f.write("insDrel:\n")
f.write(".byte "+relD+"\n")
f.write("insWrel:\n")
f.write(".byte "+relW+"\n")
for i in range(chnum):
order = module.subsongs[subsong].order[i]
f.write("order"+str(i)+"len = "+str(len(order))+"\n")
f.write("order"+str(i)+"L:\n")
f.write(".byte ")
for o in range(len(order)):
f.write("<(patCH"+str(i)+"N"+str(order[o])+"-1)")
if o == len(order)-1:
f.write("\n")
else:
f.write(", ")
f.write("order"+str(i)+"H:\n")
f.write(".byte ")
for o in range(len(order)):
f.write(">(patCH"+str(i)+"N"+str(order[o])+"-1)")
if o == len(order)-1:
f.write("\n")
else:
f.write(", ")
for i in range(chnum):
order = module.subsongs[subsong].order[i]
avail_patterns = filter(
lambda x: (
x.channel == i and
x.subsong == subsong
),
module.patterns
)
for p in avail_patterns:
patnum = p.index
#print(patnum,i)
g = str(comp(conv_pattern(p)))[1:-1]
f.write("patCH"+str(i)+"N"+str(patnum)+":\n")
f.write(".byte "+g+"\n")
if chnum == 4:
f.write("sampleHS:\n.hibytes ")
for i in range(len(module.samples)):
f.write("PCM"+str(i))
if i == (len(module.samples)-1):
f.write("\n")
else:
f.write(", ")
f.write("sampleHE:\n.hibytes ")
for i in range(len(module.samples)):
f.write("PCMe"+str(i))
if i == (len(module.samples)-1):
f.write("\n")
else:
f.write(", ")
total_maps = []
f.write("insSI:\n.byte ")
for i in range(len(module.instruments)):
features = module.instruments[i].features
a = filter(
lambda x: (
type(x) == InsFeatureAmiga
), features
)
init_sample = 0
for j in a:
if j.init_sample > 0 and j.init_sample != 65535:
init_sample = j.init_sample
break
f.write(str(init_sample))
if i == (len(module.instruments)-1):
f.write("\n")
else:
f.write(", ")
f.write(".res 256-(*&$ff), 0\n")
for i in range(len(module.samples)):
sample = []
rate = max(min(module.samples[i].meta.sample_rate,384000),100)
smp = list(module.samples[i].data)
k = 0
while k < len(smp):
j = smp[int(k)]
s = float(((int(j)+128)&0xff))
s = ((s-128)/1.65)+128
#s = ((s-128)/1.6)+128
s = int(s/16)
#s = (s>>1)+8
sample.append(s)
k += rate/7812
s = sample[-1]
if (len(sample)%256) == 0:
sample.extend([s]*256)
else:
while (len(sample)%256) != 0:
sample.append(s)
f.write("PCM"+str(i)+":\n.byte "+str(sample)[1:-1]+"\n")
f.write("PCMe"+str(i)+":\n")
f.close()
# frequency calculation code taken from
# https://codebase64.org/doku.php?id=base:how_to_calculate_your_own_sid_frequency_table
tuning = module.meta.tuning
f = open("asm/note_lo.bin","wb")
for i in range(96):
hz = tuning * (2**(float(i-57)/12.0))
cnst = (256**3)/985248.0 # PAL frequency
freq = min(max(hz*cnst,0),0xffff)
f.write(bytearray([int(freq)&0xff]))
f.close()
f = open("asm/note_hi.bin","wb")
for i in range(96):
hz = tuning * (2**(float(i-57)/12.0))
cnst = (256**3)/985248.0 # PAL frequency
freq = min(max(hz*cnst,0),0xffff)
f.write(bytearray([(int(freq)>>8)&0xff]))
f.close()

Binary file not shown.

Binary file not shown.

View file

@ -165,9 +165,10 @@ code_start:
lda #$10
jsr load_8000_zx02
ldx #<sidname
ldy #>sidname
jsr loadraw
ldx #<song0name
ldy #>song0name
lda #$a0
jsr load_8000_zx02
ldx #<towername
ldy #>towername
@ -1246,7 +1247,7 @@ irq_badguy:
badguy: .byte "badguy",0
fontname: .byte "font",0
sidname: .byte "sid", 0
song0name: .byte "song0", 0
introname: .byte "intrbmp", 0
towername: .byte "tower", 0
towerbeamname: .byte "towerbm", 0

Binary file not shown.

Binary file not shown.