Add music player source
This commit is contained in:
parent
3f4765029b
commit
c006e1a0a2
|
|
@ -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)
|
||||
|
|
|
|||
8
loader/samples/minexample/furC64/.gitignore
vendored
Normal file
8
loader/samples/minexample/furC64/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
__pycache__
|
||||
asm/song.asm
|
||||
asm/song.bin
|
||||
*.o
|
||||
*.lbl
|
||||
*.lst
|
||||
*.map
|
||||
*.prg
|
||||
42
loader/samples/minexample/furC64/README.md
Normal file
42
loader/samples/minexample/furC64/README.md
Normal 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
|
||||
|
||||
16
loader/samples/minexample/furC64/asm/bin.cfg
Normal file
16
loader/samples/minexample/furC64/asm/bin.cfg
Normal 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;
|
||||
}
|
||||
22
loader/samples/minexample/furC64/asm/exe.cfg
Normal file
22
loader/samples/minexample/furC64/asm/exe.cfg
Normal 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;
|
||||
}
|
||||
2181
loader/samples/minexample/furC64/asm/furC64.asm
Normal file
2181
loader/samples/minexample/furC64/asm/furC64.asm
Normal file
File diff suppressed because it is too large
Load diff
3
loader/samples/minexample/furC64/asm/note_hi.bin
Normal file
3
loader/samples/minexample/furC64/asm/note_hi.bin
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
"$')+.147:>AEINRW\bhnu|<7C><><EFBFBD><EFBFBD>ク皇俑褻<E4BF91>
|
||||
1
loader/samples/minexample/furC64/asm/note_lo.bin
Normal file
1
loader/samples/minexample/furC64/asm/note_lo.bin
Normal file
|
|
@ -0,0 +1 @@
|
|||
&8K^s‰¡¹Ôð
,Mq–½çBs¨àY›â,{Î'„çQÀ6³7ÄYö<59>N Ï¢<C38F>mgoˆ²í:œŸDÚÎßdÚu8&?‰´œ¿"È´ëqLh8E<7F>hÖã˜ÿ$ÿ
|
||||
18
loader/samples/minexample/furC64/chipchune/__init__.py
Normal file
18
loader/samples/minexample/furC64/chipchune/__init__.py
Normal 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"
|
||||
98
loader/samples/minexample/furC64/chipchune/_util.py
Normal file
98
loader/samples/minexample/furC64/chipchune/_util.py
Normal 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')
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
soon!
|
||||
"""
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
soon!
|
||||
"""
|
||||
|
|
@ -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)
|
||||
"""
|
||||
706
loader/samples/minexample/furC64/chipchune/furnace/data_types.py
Normal file
706
loader/samples/minexample/furC64/chipchune/furnace/data_types.py
Normal 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
|
||||
648
loader/samples/minexample/furC64/chipchune/furnace/enums.py
Normal file
648
loader/samples/minexample/furC64/chipchune/furnace/enums.py
Normal 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
|
||||
1572
loader/samples/minexample/furC64/chipchune/furnace/instrument.py
Normal file
1572
loader/samples/minexample/furC64/chipchune/furnace/instrument.py
Normal file
File diff suppressed because it is too large
Load diff
1111
loader/samples/minexample/furC64/chipchune/furnace/module.py
Normal file
1111
loader/samples/minexample/furC64/chipchune/furnace/module.py
Normal file
File diff suppressed because it is too large
Load diff
47
loader/samples/minexample/furC64/chipchune/furnace/sample.py
Normal file
47
loader/samples/minexample/furC64/chipchune/furnace/sample.py
Normal 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)
|
||||
101
loader/samples/minexample/furC64/chipchune/furnace/wavetable.py
Normal file
101
loader/samples/minexample/furC64/chipchune/furnace/wavetable.py
Normal 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)]
|
||||
|
|
@ -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.
|
||||
|
||||
"""
|
||||
|
|
@ -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()
|
||||
|
|
@ -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.__
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
"""
|
||||
Utilities for manipulating, converting, etc. tracker data.
|
||||
|
||||
- :mod:`chipchune.utils.conversion`: Conversion tools.
|
||||
"""
|
||||
|
|
@ -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
|
||||
14
loader/samples/minexample/furC64/convert.bat
Normal file
14
loader/samples/minexample/furC64/convert.bat
Normal 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
|
||||
15
loader/samples/minexample/furC64/convert.sh
Executable file
15
loader/samples/minexample/furC64/convert.sh
Executable 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
|
||||
780
loader/samples/minexample/furC64/convert_to_asm.py
Normal file
780
loader/samples/minexample/furC64/convert_to_asm.py
Normal 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()
|
||||
BIN
loader/samples/minexample/guy_amb.fur
Normal file
BIN
loader/samples/minexample/guy_amb.fur
Normal file
Binary file not shown.
BIN
loader/samples/minexample/guy_appear.fur
Normal file
BIN
loader/samples/minexample/guy_appear.fur
Normal file
Binary file not shown.
|
|
@ -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.
BIN
loader/samples/minexample/ys2_port_legato.fur
Normal file
BIN
loader/samples/minexample/ys2_port_legato.fur
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue