-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Massive Refactoring; added Axx support; sample-mapping via instrument…
…, not octave; Volume is now mapped from 0-64 values, non-set-volume doesn't map to None anymore
- Loading branch information
kleeder
committed
Feb 7, 2022
1 parent
1cef1aa
commit 97daff6
Showing
7 changed files
with
493 additions
and
274 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,53 @@ | ||
# it2fss3 | ||
# it2fss | ||
|
||
Improved version of: https://gist.github.com/jangler/9565970 | ||
Converter which takes single-channel ImpulseTracker modules as input and outputs a fSound .fss file, for use with fsound.exe. | ||
fSound is available [here](https://kleeder.de/files/botbFiles.php). This converter aims for the version called "fsound.zip". | ||
The [Pytrax/impulsetracker-parser library](https://github.com/ramen/pytrax) is used and | ||
slightly modified to work with Python 3. | ||
|
||
Improved version of: https://gist.github.com/jangler/9565970 | ||
|
||
------- | ||
|
||
## Features | ||
- Squarewaves of any volume and pitch are supported. | ||
- White Noise of any volume is supported. | ||
- Tempo Changes are supported. (Txx with values bigger <= 20 and Axx). | ||
- Kick and Snare Samples are supported. | ||
- Only use an instrument setting, volume or effect along with a note, otherwise it will throw an error. | ||
|
||
|
||
------- | ||
|
||
## Installation | ||
- download/clone repo | ||
- make sure Python 3 is installed | ||
- write your song using the test.it (follow the limits listed below) | ||
- convert your song with | ||
```bash | ||
python it2fss.py test.it | ||
``` | ||
|
||
------- | ||
|
||
## Limitations | ||
- Square Waves can be produced between C-2 and B-8 (C-1 to B-7 in fSound), using Instrument 1. | ||
- White Noise is always the same pitch, using Instrument 2. | ||
- Kick uses Instrument 3 and Snare uses Instrument 4. | ||
- Make sure to set an instrument for every note you put in. | ||
|
||
|
||
- Square and Noise have volume control. v00 is lowest, v64 is highest. | ||
- The values get mapped to the 16 available sound values in fSound (0-F). | ||
- Non-set volume gets mapped to f (loudest). | ||
------- | ||
## Version history | ||
|
||
* 0.5: Massive Refactoring; added Axx support; sample-mapping via instrument, not octave; | ||
Volume is now mapped from 0-64 values, non-set-volume doesn't map to None anymore | ||
* 0.4.1: Fixed a bug where consecutive drum notes would be combined into one | ||
* 0.4: Added Tempo Change Support | ||
* 0.3: Added Sample and Volume Support | ||
* 0.2: Fixed a bug that occurred when translating a single IT note into | ||
multiple FSS notes. | ||
* 0.1: Initial creation of program. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
#!/usr/bin/env python | ||
|
||
# it2fss.py, version 0.5 | ||
|
||
from sys import argv, stderr, version_info | ||
from math import floor, log2 | ||
from pytrax import impulsetracker | ||
|
||
|
||
def die(msg): | ||
if isinstance(msg, BaseException): | ||
msg = str(msg) | ||
stderr.write(str(msg) + '\n') | ||
exit(1) | ||
|
||
|
||
if version_info.major < 3: | ||
die('python3 only!') | ||
|
||
|
||
if len(argv) == 2: | ||
MODULE = argv[1] | ||
else: | ||
die('Usage: {} MODULE'.format(argv[0])) | ||
|
||
|
||
NOTE_NAMES = ['a', 'A', 'b', 'c', 'C', 'd', 'D', 'e', 'f', 'F', 'g', 'G'] | ||
VALUE_NAMES = ['f', '8', '4', '2', '1'] | ||
|
||
|
||
# Calculates and returns the fSound Speed based on the tempo and speed of an ImpulseTracker Module. | ||
def get_fsound_tempo(tempo: int, speed: int) -> int: | ||
return 2500 // tempo * speed | ||
|
||
|
||
# Sets current row values. | ||
# input: an .it row | ||
# output: cur_item (absolute note value. if not set, the script stops) | ||
# cur_instr (instrument value. if not set, the script stops (unless its a note cut) | ||
# cur_vol (volume value. if not set, it defaults to 64) | ||
# cur_cmd (command + value. if not set, it defaults to None) | ||
def get_row_info(row): | ||
cur_item = None | ||
cur_instr = None | ||
cur_vol = None | ||
cur_cmd = None | ||
error_msg = "ERROR: There are rows with content but no note in your song." | ||
|
||
try: | ||
cur_item = row[0]['note'] | ||
# check if the current note is a note cut (254) or not | ||
if cur_item != 254: | ||
try: | ||
cur_instr = row[0]['instrument'] | ||
except: | ||
error_msg = "ERROR: There are notes in your song with no instrument assigned." | ||
die(error_msg) | ||
else: | ||
cur_instr = None | ||
try: | ||
cur_vol = row[0]['volpan'] | ||
except: | ||
cur_vol = 64 | ||
try: | ||
cur_cmd = row[0]['command'] | ||
except: | ||
cur_cmd = None | ||
except: | ||
die(error_msg) | ||
|
||
return cur_item, cur_instr, cur_vol, cur_cmd | ||
|
||
|
||
# returns a .fss string with linebreaks for one row of the .it file | ||
# inputs: note (absolute IT note) | ||
# rows (the amount of empty rows until the next note happens (or end of song)) | ||
# vol (the volume of the note) | ||
# instr (the instrument used for the note) | ||
# speed (None if there is no new speed to set, new speed value otherwise) | ||
# output: string with .fss content | ||
def note_format(note, rows, vol, instr, speed): | ||
# 64 values get mapped down to 16 | ||
vol = round(vol/4) | ||
# dec values need to get converted to hex | ||
if vol == 10: | ||
vol = "a" | ||
elif vol == 11: | ||
vol = "b" | ||
elif vol == 12: | ||
vol = "c" | ||
elif vol == 13: | ||
vol = "d" | ||
elif vol == 14: | ||
vol = "e" | ||
elif vol >= 15: | ||
vol = "f" | ||
|
||
lengths = [] | ||
while rows > 0: | ||
power = min(4, floor(log2(rows))) | ||
lengths.append(VALUE_NAMES[power]) | ||
rows -= 2 ** power | ||
|
||
strings = [] | ||
|
||
for length in lengths: | ||
if speed is not None: | ||
if speed > 9: | ||
if int(str(speed)[1]) in [1, 2, 4, 8]: | ||
print("Your song uses the speed value t{}. Keep in mind, that speed values with 1, 2, 4 or 8 at the 2nd position will result in pauses in your song.".format(speed)) | ||
strings.append('t{}'.format(speed)) | ||
if note == 254: | ||
strings.append('r-' + length) | ||
else: | ||
fs_note = note - 9 | ||
octave = fs_note // 12 | ||
if instr == 1: | ||
if not 1 <= octave <= 7: | ||
die("ERROR: Your module file uses octaves below 2 or above 8.") | ||
name = NOTE_NAMES[fs_note % 12] | ||
strings.append(name + str(octave) + length + str(vol)) | ||
elif instr == 2: | ||
strings.append('x-' + length + str(vol)) | ||
# for kick and snare, only trigger them once, then set note to r-x | ||
# speed has to be set to None, to avoid multiple trigger of the same speed value | ||
elif instr == 3: | ||
strings.append('K-' + length) | ||
speed = None | ||
note = 254 | ||
elif instr == 4: | ||
strings.append('S-' + length) | ||
speed = None | ||
note = 254 | ||
else: | ||
die("ERROR: Your module file uses instruments higher than 4.") | ||
|
||
if strings: | ||
return '\n'.join(strings) + '\n' | ||
return '' | ||
|
||
|
||
# calculates a new speed given either a Txx or a Axx effect with value | ||
# returns the tempo and speed too, because those changed values will get reused later on | ||
def calc_new_speed(cur_cmd, tempo: int, speed: int): | ||
new_speed = None | ||
if cur_cmd is not None: | ||
if cur_cmd.startswith("T"): | ||
tempo = int(cur_cmd[1:], 16) | ||
new_speed = get_fsound_tempo(tempo, speed) | ||
elif cur_cmd.startswith("A"): | ||
speed = int(cur_cmd[1:]) | ||
new_speed = get_fsound_tempo(tempo, speed) | ||
return tempo, speed, new_speed | ||
|
||
|
||
# converts an .it module into a .fss file | ||
def convert(module, filename): | ||
outfile = None | ||
try: | ||
outfile = open(filename, 'w') | ||
print(".fss file created.") | ||
except BaseException as ex: | ||
die(ex) | ||
|
||
length = 0 | ||
tempo = module['inittempo'] | ||
speed = module['initspeed'] | ||
|
||
cur_item = None | ||
cur_instr = None | ||
cur_vol = None | ||
cur_cmd = None | ||
|
||
outfile.write('{}\n\n'.format(get_fsound_tempo(tempo, speed))) | ||
outfile.write('> generated by it2fss.py Ver 0.5\n\n') | ||
print("Header written.") | ||
|
||
print("Converting patterns.") | ||
for order in (x for x in module['orders']): | ||
# +++ Patterns | ||
if order == 254: | ||
pass | ||
# --- Pattern (End of Song, no matter if there are other patterns after) | ||
elif order == 255: | ||
tempo, speed, new_speed = calc_new_speed(cur_cmd, tempo, speed) | ||
outfile.write(note_format(cur_item, length, cur_vol, cur_instr, new_speed)) | ||
break | ||
else: | ||
pattern = module['patterns'][order] | ||
pattern_comment_check = True | ||
for row in pattern[0]: | ||
if len(row) > 0: | ||
if length == 0: | ||
cur_item, cur_instr, cur_vol, cur_cmd = get_row_info(row) | ||
else: | ||
tempo, speed, new_speed = calc_new_speed(cur_cmd, tempo, speed) | ||
outfile.write(note_format(cur_item, length, cur_vol, cur_instr, new_speed)) | ||
length = 0 | ||
cur_item, cur_instr, cur_vol, cur_cmd = get_row_info(row) | ||
if pattern_comment_check: | ||
outfile.write('> pattern {}\n'.format(order)) | ||
pattern_comment_check = False | ||
length += 1 | ||
|
||
outfile.close() | ||
print("File sucessfully converted.") | ||
|
||
|
||
module = impulsetracker.parse_file(MODULE, with_patterns=True) | ||
convert(module, MODULE[:-3] + '.fss') |
Oops, something went wrong.