Files
2026-04-11 20:18:08 +02:00

1064 lines
38 KiB
PHP
Executable File

<?php
/**
* Convert MOD files to NinjaTracker+ format.
*
* by Jesse Blue / Ninjaforce
*
*
* July 29, 2018 - Initial release
**/
/* the converter class, can be used on its own */
class ntpconverter
{
private $path_source_mod; /* path to the MOD file that should be converted */
private $path_output_ntp; /* path to the resulting ninjatracker+ (.ntp) file */
private $rotate_loops; /* true if loops that do not have ideal size should be rotated */
private $mod_data; /* the binary file data of the MOD */
private $max_number_instruments; /* how many instruments this MOD has at max */
private $no_of_tracks; /* how many tracks this MOD uses */
private $max_pat_cnt; /* number of patterns (blocks) that the MOD has */
private $ptr_instnumb; /* pointer to where instruments are in a MOD */
private $mod_headlen; /* length of the MOD header */
private $ptr_songp_start; /* pointer to where the Songinfo is, which block should be played after another */
private $ptr_songp_end; /* pointer to where the Songinfo ends */
private $length_of_one_pattern; /* how many bytes form a pattern in this MOD */
private $length_of_all_patterns; /* how many bytes form all patterns in this MOD, combined */
private $pattern_order; /* array of pattern numbers that defines the order in which to play the patterns */
private $instrument_information; /* array of instrument information from the MOD */
private $instrument_data; /* array of MOD instrument data */
private $instrument_data_gs; /* array of converted sample data */
private $instrument_map; /* array of which MOD instrument maps to which NT instrument */
private $instrument_types; /* array with type of gs instrument: 0 = not looped, 1 = looped, 2 = the loop itself */
private $instrument_volume_gs; /* array with volume of gs instrument */
private $instrument_finetune_gs; /* array with finetune values of gs instrument */
private $instrument_is_splitted; /* array of booleans about which MOD instrument is split into two */
private $doc_ptrs; /* array of pointers to where the GS instrument are placed in the DOC */
private $doc_sizes; /* array of size values that can be used by the DOC to play the instrument */
private $ntp_notes; /* array of ntp pattern data bytes */
private $instrument_errors; /* array of errors when converting instruments */
private $pattern_errors; /* array of errors when converting patterns */
private static $ptr_first_instrument = 20; /* pointer to where the MOD has its first instrument */
private static $length_instrument_info = 30; /* length of an instrument information block in the MOD */
private static $offset_instrument_length = 22; /* offset in instrument information block to the length of the instrument */
private static $offset_instrument_finetune = 24; /* offset in instrument information block to the finetune value */
private static $offset_instrument_volume = 25; /* offset in instrument information block to the volume */
private static $offset_instrument_rep_ptr = 26; /* offset in instrument information block to the repeat pointer within the instrument */
private static $offset_instrument_rep_len = 28; /* offset in instrument information block to the length of the repeat */
private static $ptr_mod_type = 1080; /* pointer to where the MOD identifier code is */
private static $ptr_by_type = [
'old' => [ /* old trackers have up to 15 instruments */
'max_number_instruments' => 15,
'ptr_instnumb' => 950-480,
'mod_headlen' => 1080-480,
'ptr_songp_start' => 952-480,
'ptr_songp_end' => 1080-480,
],
'normal' => [ /* normal trackers have up to 31 instruments */
'max_number_instruments' => 31,
'ptr_instnumb' => 950,
'mod_headlen' => 1084,
'ptr_songp_start' => 952,
'ptr_songp_end' => 1080,
],
];
private static $length_of_note = 4; /* 4 bytes represent a note in a mod */
private static $number_of_lines_in_pattern = 64; /* there are always 64 lines in a pattern */
private static $max_ntp_instruments = 255; /* there may be only a limited number of instruments in an ntp */
private static $number_of_stopper_bytes = 8; /* at the end of every instrument in DOC ram, add some 0 bytes to make the ensoniq stop playing */
private static $optimal_small_loop_length = 512; /* a comfortable length if the instrument is a small loop */
private static $mod_types = [ /* mapping for ID in MOD => number of tracks */
'M.K.' => 4,
'2CHN' => 2,
'5CHN' => 5,
'6CHN' => 6,
'7CHN' => 7,
'8CHN' => 8,
'9CHN' => 9,
'01CH' => 1,
'02CH' => 2,
'03CH' => 3,
'04CH' => 4,
'05CH' => 5,
'06CH' => 6,
'07CH' => 7,
'08CH' => 8,
'09CH' => 9,
'10CH' => 10,
'11CH' => 11,
'12CH' => 12,
'13CH' => 13,
'14CH' => 14,
'15CH' => 14,
'01CN' => 1,
'02CN' => 2,
'03CN' => 3,
'04CN' => 4,
'05CN' => 5,
'06CN' => 6,
'07CN' => 7,
'08CN' => 8,
'09CN' => 9,
'10CN' => 10,
'11CN' => 11,
'12CN' => 12,
'13CN' => 13,
'14CN' => 14,
'15CN' => 14,
'FLT4' => 4,
'FLT8' => 8,
];
private static $doc_page_boundaries = [ /* instruments in the DOC need to be aligned to this number of pages */
1,
2,
4,
8,
16,
32,
64,
128,
];
private static $doc_register_sizes = [ /* the doc wavetable size register is set according to the wave size */
0b0,
0b001001,
0b010010,
0b011011,
0b100100,
0b101101,
0b110110,
0b111111,
];
private static $optimal_instrument_lengths = [
256,
512,
1024,
2048,
4096,
8192,
16384,
32768,
];
private static $mod_notes = [ /* music notes that are possible in the MOD */
1712,1616,1525,1440,1357,1281,1209,1141,1077,1017, 961, 907, /* c2-b2 */
856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453, /* c3-b3 */
428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226, /* c4-b4 */
214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113, /* c5-b5 */
107, 101, 95, 90, 85, 80, 76, 71, 67, 64, 60, 57, /* c6-b6 */
53, 50, 47, 45, 42, 40, 37, 35, 33, 31, 30, 28, /* c7-b7 */
];
private static $doc_stereo_defaults = [
1, 0,
0, 1,
1, 0,
0, 1,
1, 0,
0, 1,
1, 0,
0, 1,
];
/* ---- helper methods ---- */
private function get_mod_file_byte($position)
{
return ord(substr($this->mod_data, $position, 1));
}
private function get_amiga_word($position)
{
return 256 * $this->get_mod_file_byte($position) + $this->get_mod_file_byte($position + 1);
}
private function word_to_gs_str($word)
{
$hi_byte = ($word & 0xff00) >> 8;
$lo_byte = $word & 0x00ff;
return chr($lo_byte) . chr($hi_byte);
}
private function get_smaller_length($length)
{
foreach(array_reverse(self::$doc_page_boundaries) as $page_boundary)
{
$byte_length = $page_boundary * 256;
if($byte_length < $length)
{
return $byte_length;
}
}
return $length;
}
/* ---- main program code ---- */
public function __construct($path_source_mod, $path_output_ntp)
{
$this->path_source_mod = $path_source_mod;
$this->path_output_ntp = $path_output_ntp;
$this->instrument_errors = [];
$this->pattern_errors = [];
}
private function detect_mod_and_number_of_tracks()
{
/* find out if old or normal and how many tracks there are */
$str_type = substr($this->mod_data, self::$ptr_mod_type, 4);
if(isset(self::$mod_types[$str_type]))
{
$old_or_normal = 'normal';
$this->no_of_tracks = self::$mod_types[$str_type];
}
else
{
$old_or_normal = 'old';
$this->no_of_tracks = 4;
}
/* set values, depending on old or normal */
$this->max_number_instruments = self::$ptr_by_type[$old_or_normal]['max_number_instruments'];
$this->ptr_instnumb = self::$ptr_by_type[$old_or_normal]['ptr_instnumb'];
$this->mod_headlen = self::$ptr_by_type[$old_or_normal]['mod_headlen'];
$this->ptr_songp_start = self::$ptr_by_type[$old_or_normal]['ptr_songp_start'];
$this->ptr_songp_end = self::$ptr_by_type[$old_or_normal]['ptr_songp_end'];
}
private function detect_number_of_patterns()
{
$max_pat_cnt = 0;
for($pos = $this->ptr_songp_start; $pos < $this->ptr_songp_end; $pos++)
{
$byte = $this->get_mod_file_byte($pos);
$max_pat_cnt = max($byte, $max_pat_cnt);
}
$max_pat_cnt++; /* add 1 because pattern 0 is also a pattern */
$this->max_pat_cnt = $max_pat_cnt;
}
private function calc_pattern_length()
{
$this->length_of_one_pattern = $this->no_of_tracks * self::$length_of_note * self::$number_of_lines_in_pattern;
$this->length_of_all_patterns = $this->length_of_one_pattern * $this->max_pat_cnt;
}
private function get_pattern_order()
{
$pattern_order_length = $this->get_mod_file_byte($this->ptr_instnumb); /* length of pattern order */
$this->pattern_order = [];
for($i = 0; $i < $pattern_order_length; $i++)
{
$ptr_mod = $this->ptr_instnumb + 2 + $i;
$byte = $this->get_mod_file_byte($ptr_mod);
$this->pattern_order[] = $byte;
}
}
private function extract_instrument_information($instrument_number)
{
/* instrument numbers run from 1 to 15 or 1 to 31 */
$ptr = self::$ptr_first_instrument + ($instrument_number - 1) * self::$length_instrument_info;
$instrument_name = '';
for($i = 0; $i < self::$length_instrument_info; $i++)
{
$byte = $this->get_mod_file_byte($ptr + $i);
if($byte === 0)
{
break;
}
$instrument_name .= chr($byte);
}
$instrument_length = $this->get_amiga_word($ptr + self::$offset_instrument_length) * 2; /* given in words, transform to bytes */
$instrument_volume = $this->get_mod_file_byte($ptr + self::$offset_instrument_volume);
$instrument_finetune = (15 & $this->get_mod_file_byte($ptr + self::$offset_instrument_finetune)); /* lower nibble only */
$instrument_rep_ptr = $this->get_amiga_word($ptr + self::$offset_instrument_rep_ptr) * 2; /* given in words, transform to bytes */
$instrument_rep_len = $this->get_amiga_word($ptr + self::$offset_instrument_rep_len) * 2; /* given in words, transform to bytes */
return [
'name' => $instrument_name,
'length' => $instrument_length,
'finetune' => $instrument_finetune,
'volume' => $instrument_volume,
'rep_ptr' => $instrument_rep_ptr,
'rep_len' => $instrument_rep_len,
];
}
private function extract_all_instrument_information()
{
$this->instrument_information = [];
for($i = 1; $i <= $this->max_number_instruments; $i++)
{
$this->instrument_information[$i] = $this->extract_instrument_information($i);
}
}
private function extract_instrument_data($instrument_number)
{
/* calculcate the pointer in the MOD where the instrument begins */
$ptr = $this->mod_headlen + $this->length_of_all_patterns;
for($i = 1; $i < $instrument_number; $i++)
{
$ptr += $this->instrument_information[$i]['length'];
}
/* get instrument data */
$instrument_data = [];
for($i = $ptr; $i < $ptr + $this->instrument_information[$instrument_number]['length']; $i++)
{
$instrument_data[] = $this->get_mod_file_byte($i);
}
return $instrument_data;
}
private function extract_all_instrument_data()
{
$this->instrument_data = [];
for($i = 1; $i <= $this->max_number_instruments; $i++)
{
if($this->instrument_information[$i]['length'] > 0)
{
$this->instrument_data[$i] = $this->extract_instrument_data($i);
}
}
}
private function convert_instrument_bytes($mod_instrument_number, $offset, $length)
{
$instrument_data_gs = [];
for($i = $offset; $i < $offset + $length; $i++)
{
$byte = $this->instrument_data[$mod_instrument_number][$i];
/* shift waveform, the GS has 128 for silence, the Amiga has 0 */
$ensoniq_byte = 255 & ($byte + 128);
/* make sure we do not have a 0 in the wave, which would stop the ensoniq from playing */
if($ensoniq_byte === 0)
{
$ensoniq_byte = 1;
}
$instrument_data_gs[] = $ensoniq_byte;
}
return $instrument_data_gs;
}
private function add_stopper_bytes_if_needed($instrument_data)
{
$instrument_has_optimal_size = in_array(count($instrument_data), self::$optimal_instrument_lengths);
if(!$instrument_has_optimal_size)
{
/* add stopper bytes */
for($i = 0; $i < self::$number_of_stopper_bytes; $i++)
{
$instrument_data[] = 0;
}
}
return [$instrument_has_optimal_size, $instrument_data];
}
private function optimize_loop($instrument_data_gs)
{
$instrument_size_gs = count($instrument_data_gs);
$instrument_has_optimal_size = in_array($instrument_size_gs, self::$optimal_instrument_lengths);
/* if the loop has not an ideal length, and is very short, copy the loop to improve sound quality */
if(!$instrument_has_optimal_size && $instrument_size_gs < self::$optimal_small_loop_length)
{
$number_of_stopper_bytes = self::$number_of_stopper_bytes;
/* edge case: if the loop fits exactly n times into the optimal length */
$factor = self::$optimal_small_loop_length / $instrument_size_gs;
if($factor == floor($factor))
{
$instrument_has_optimal_size = true;
$number_of_stopper_bytes = 0;
}
$loop_data_gs = $instrument_data_gs;
while(count($instrument_data_gs) + count($loop_data_gs) <= self::$optimal_small_loop_length - $number_of_stopper_bytes)
{
$instrument_data_gs = array_merge($instrument_data_gs, $loop_data_gs);
}
}
return $instrument_data_gs;
}
private function convert_instrument_data($mod_instrument_number, $offset, $length, $loop = false)
{
$instrument_data_gs = $this->convert_instrument_bytes($mod_instrument_number, $offset, $length);
if($loop)
{
$instrument_data_gs = $this->optimize_loop($instrument_data_gs);
}
return $this->add_stopper_bytes_if_needed($instrument_data_gs);
}
private function add_gs_instrument($mod_instrument_number, $instrument_data_gs, $instrument_type, $optimal_instrument_size)
{
$index = 1 + count($this->instrument_data_gs);
$this->instrument_data_gs[$index] = $instrument_data_gs;
/* Add mapping.
Because we maybe split looped instruments into two for the GS version,
we need to map which MOD instrument is which NT instrument.
But for the mapping, we only need the first instrument.
Because if the instrument consists of a head and a loop, the head
is always first and loop is second.
*/
if(!isset($this->instrument_map[$mod_instrument_number]))
{
$this->instrument_map[$mod_instrument_number] = $index;
}
$this->instrument_types[$index] = $instrument_type + ($optimal_instrument_size ? 4 : 0);
$this->instrument_volume_gs[$index] = $this->instrument_information[$mod_instrument_number]['volume'];
$this->instrument_finetune_gs[$index] = $this->instrument_information[$mod_instrument_number]['finetune'];
}
private function find_closest_zero_crossing($instrument_data_gs_loop)
{
$ptr = false;
$min_diff = 1000;
foreach($instrument_data_gs_loop as $ptr => $byte)
{
if($min_diff > abs($byte - 128))
{
$min_diff = abs($byte - 128);
$min_ptr = $ptr;
}
}
return $min_ptr;
}
private function rotate_loop($i, $rep_ptr, $rep_len)
{
$instrument_data_gs_head = $this->convert_instrument_bytes($i, 0, $rep_ptr);
$instrument_data_gs_loop = $this->convert_instrument_bytes($i, $rep_ptr, $rep_len);
$position = $this->find_closest_zero_crossing($instrument_data_gs_loop);
$instrument_data_gs_head_new = array_merge(
$instrument_data_gs_head, /* keep head */
array_slice($instrument_data_gs_loop, 0, $position) /* add beginning of loop */
);
$instrument_data_gs_loop_new = array_merge(
array_slice($instrument_data_gs_loop, $position), /* start loop from position to the end */
array_slice($instrument_data_gs_loop, 0, $position) /* add start of loop from beginning to position */
);
$instrument_data_gs_loop_new = $this->optimize_loop($instrument_data_gs_loop_new);
list($head_has_optimal_size, $instrument_head) = $this->add_stopper_bytes_if_needed($instrument_data_gs_head_new);
list($loop_has_optimal_size, $instrument_loop) = $this->add_stopper_bytes_if_needed($instrument_data_gs_loop_new);
return [$head_has_optimal_size, $instrument_head, $loop_has_optimal_size, $instrument_loop];
}
private function convert_instruments()
{
$this->instrument_data_gs = [];
$this->instrument_map = [];
$this->instrument_errors = [];
for($i = 1; $i <= $this->max_number_instruments; $i++)
{
$length = $this->instrument_information[$i]['length'];
if($length === 0)
{
/* No instrument, skip */
continue;
}
$rep_ptr = $this->instrument_information[$i]['rep_ptr'];
$rep_len = $this->instrument_information[$i]['rep_len'];
$instrument_is_splitted = false;
if($rep_len < 3)
{
/* instrument is not looped */
list($instrument_has_optimal_size, $instrument_data_gs_sample) = $this->convert_instrument_data($i, 0, $length);
$this->add_gs_instrument($i, $instrument_data_gs_sample, 0, $instrument_has_optimal_size);
}
else
{
/* instrument is looped */
if($rep_ptr > 2)
{
/* instrument consists of a head and a loop, save each as individual instrument */
list($head_has_optimal_size, $instrument_data_gs_head) = $this->convert_instrument_data($i, 0, $rep_ptr, false);
list($loop_has_optimal_size, $instrument_data_gs_loop) = $this->convert_instrument_data($i, $rep_ptr, $rep_len, true);
if(!$loop_has_optimal_size && $this->rotate_loop)
{
/* if the loop has not an optimal size, the player has to use swap mode */
/* but in order to minimize the swap mode bug (see Apple IIGS Technote #11), rotate loop */
list(
$head_has_optimal_size, $instrument_data_gs_head,
$loop_has_optimal_size, $instrument_data_gs_loop
) = $this->rotate_loop($i, $rep_ptr, $rep_len);
}
$this->add_gs_instrument($i, $instrument_data_gs_head, 2, $head_has_optimal_size);
$this->add_gs_instrument($i, $instrument_data_gs_loop, 3, $loop_has_optimal_size);
$instrument_is_splitted = true;
}
else
{
/* instrument consists only of a loop */
list($instrument_has_optimal_size, $instrument_data_gs_loop) = $this->convert_instrument_data($i, $rep_ptr, $rep_len, true);
$this->add_gs_instrument($i, $instrument_data_gs_loop, 1, $instrument_has_optimal_size);
}
}
$this->instrument_is_splitted[$i] = $instrument_is_splitted;
}
}
private function check_interrupt_instrument()
{
/* Rare edge case:
*
* Make sure that the first 256 of the DOC ram are filled with non-zeros.
* Otherwise, the interrupt timer cannot work.
*
* Since the largest instrument will be placed to this address, this is
* extremely unlikely to happen.
*
* But if it does, we add a dummy 256 byte silent instrument.
*/
$max_gs_instrument_length = 0;
foreach($this->instrument_data_gs as $index => $instrument_data_gs)
{
$max_gs_instrument_length = max($max_gs_instrument_length, count($instrument_data_gs));
}
if($max_gs_instrument_length < 256)
{
$mod_instrument_number = 10000;
$instrument_data_gs = array_fill(0, 256, 128); /* array with 256 elements with value of 128 (silence) */
$instrument_type = 1;
$optimal_instrument_size = true;
$this->add_gs_instrument($mod_instrument_number, $instrument_data_gs, $instrument_type, $optimal_instrument_size);
}
}
private function check_instruments()
{
if(count($this->instrument_data_gs) > self::$max_ntp_instruments)
{
$this->instrument_errors[] = 'Too many instruments:';
$this->instrument_errors[] = sprintf('- Ninjatracker only allows up to %s instruments.', self::$max_ntp_instruments);
$number_of_instruments = count($this->instrument_data);
if($number_of_instruments > self::$max_ntp_instruments)
{
$this->instrument_errors[] = sprintf('- The MOD already has %s instruments.', $number_of_instruments);
}
else
{
$this->instrument_errors[] = sprintf('- The MOD has %s instruments.', $number_of_instruments);
}
$number_splitted = 0;
foreach($this->instrument_is_splitted as $index => $is_splitted)
{
$number_splitted += $is_splitted ? 1 : 0;
}
if($number_splitted > 0)
{
$this->instrument_errors[] = sprintf('- Because of looped instruments, the converter creates %s additional instrument%s.',
$number_splitted,
($number_splitted != 1) ? 's' : ''
);
}
$this->instrument_errors[] = '- Consider getting rid of instruments or try removing instrument loop headers.';
}
}
private function sort_by_size($a, $b)
{
list($length_a, $page_size_a, $align_boundary_a) = $a;
list($length_b, $page_size_b, $align_boundary_b) = $b;
if($page_size_a === $page_size_b)
{
return 0;
}
return ($page_size_a < $page_size_b) ? 1 : -1;
}
private function sort_instruments_by_size()
{
/* sort instruments by size, descending */
$sizes = [];
foreach($this->instrument_data_gs as $instrument_number_gs => $instrument_data_gs)
{
$length = count($instrument_data_gs);
$page_size = ceil($length / 256); /* how many pages does our instrument need? */
$align_boundary = 0;
foreach(self::$doc_page_boundaries as $page_limit)
{
if($page_size <= $page_limit)
{
$align_boundary = $page_limit;
break;
}
}
$sizes[$instrument_number_gs] = [$length, $page_size, $align_boundary];
}
uasort($sizes, [$this, 'sort_by_size']);
return $sizes;
}
private function get_instrument_error($instrument_number_gs, $error)
{
/* from the GS instrument number, find the MOD instrument number */
$instrument_map_reverse = array_flip($this->instrument_map);
if(isset($instrument_map_reverse[$instrument_number_gs]))
{
$instrument_number_mod = $instrument_map_reverse[$instrument_number_gs];
}
else
{
$instrument_number_mod = $instrument_map_reverse[$instrument_number_gs - 1];
}
/* Give a hint about making the instrument smaller */
$sizehint = '';
switch($this->instrument_types[$instrument_number_gs])
{
default:
case 0:
case 4:
$length = $this->instrument_information[$instrument_number_mod]['length'];
$sizehint = 'consider reducing the size from %s to %s bytes';
break;
case 2:
case 6:
$length = count($this->instrument_data_gs[$instrument_number_gs]);
$sizehint = 'consider reducing the loop header from %s to %s bytes or removing it';
break;
case 1:
case 3:
case 5:
case 7:
$length = count($this->instrument_data_gs[$instrument_number_gs]);
$sizehint = 'consider reducing the loop from %s to %s bytes';
break;
}
$smaller_length = $this->get_smaller_length($length);
if($smaller_length < $length)
{
$sizehint = ' - ' . sprintf($sizehint, $length, $smaller_length);
}
return sprintf('Instrument %s: %s%s.', str_pad($instrument_number_mod, 2, '0', STR_PAD_LEFT), $error, $sizehint);
}
private function put_instruments_in_docwave()
{
/* for each page (a 256 byte block), we mark if it is used or not; init with 0 */
$doc_pages = array_fill(0, 256, 0);
/* loop over instruments */
$sorted_sizes = $this->sort_instruments_by_size();
foreach($sorted_sizes as $instrument_number_gs => $arr_size)
{
list($length, $page_size, $my_align_boundary) = $arr_size;
/* Depending on the size of an instrument, find the matching alignment.
For example, an instrument with length of 423 takes up 2 pages (=512 bytes) and therefore needs to be aligned on 512 bytes.
*/
$align_boundary = 0;
foreach(self::$doc_page_boundaries as $i => $page_limit)
{
if($page_size <= $page_limit)
{
$align_boundary = $page_limit;
$doc_register_size = self::$doc_register_sizes[$i];
break;
}
}
/* If we don't find a matching alignment, it means that the instrument is larger than 32k and the DOC can't play that */
if($align_boundary === 0)
{
$error = 'Too large (must be < 32k)';
$this->instrument_errors[] = $this->get_instrument_error($instrument_number_gs, $error);
continue;
}
/* find a free slot in the DOC ram */
$step_size = $align_boundary;
$page_start = -1;
for($i = 0; $i < 256; $i += $step_size)
{
if($doc_pages[$i] === 0)
{
/* we found a free slot */
$page_start = $i;
break;
}
}
if($page_start === -1)
{
$error = 'Too large';
$this->instrument_errors[] = $this->get_instrument_error($instrument_number_gs, $error);
continue;
}
/* record where we place the instrument */
$this->doc_ptrs[$instrument_number_gs] = $page_start;
/* record the size of the instrument as the doc needs it */
$this->doc_sizes[$instrument_number_gs] = $doc_register_size;
/* mark DOC ram as occupied */
for($i = $page_start; $i < $page_start + $page_size; $i++)
{
$doc_pages[$i] = 1;
}
}
}
private function optimize_instruments()
{
/* try to convert instruments using rotated loops */
$this->rotate_loop = false;
$this->convert_instruments();
$this->check_interrupt_instrument();
$this->check_instruments();
$this->put_instruments_in_docwave();
if(count($this->instrument_errors) === 0)
{
return;
}
/* try to convert instruments without using rotated loops */
$this->rotate_loop = false;
$this->convert_instruments();
$this->check_interrupt_instrument();
$this->check_instruments();
$this->put_instruments_in_docwave();
}
private function convert_period($period)
{
if($period == 0)
{
return 0; /* no note */
}
$key_mod_note = array_search($period, self::$mod_notes);
if($key_mod_note === false)
{
throw new Exception(sprintf('Unable to convert note period value of %s.', $period));
}
return $key_mod_note + 1;
}
private function convert_instrument_number($mod_instrument_number)
{
if($mod_instrument_number === 0)
{
return 0; /* no instrument */
}
if(isset($this->instrument_map[$mod_instrument_number]))
{
return $this->instrument_map[$mod_instrument_number];
}
throw new Exception(sprintf('Instrument %s not found!', $mod_instrument_number));
}
private function convert_mod_note($ptr_to_note)
{
/* 4 bytes are one note/effect */
$word0 = $this->get_amiga_word($ptr_to_note);
$word1 = $this->get_amiga_word($ptr_to_note + 2);
/* extract mod data */
$mod_period = $word0 & 0x0fff;
$mod_instrument_number = (($word0 & 0xf000) >> 8) + (($word1 & 0xf000) >> 12);
$mod_effect_number = ($word1 & 0x0f00) >> 8;
$mod_effect_value = ($word1 & 0x00ff);
/* convert to gs */
$gs_note_index = $this->convert_period($mod_period);
$gs_instrument_number = $this->convert_instrument_number($mod_instrument_number);
$gs_effect_number = $mod_effect_number;
$gs_effect_value = $mod_effect_value;
return [$gs_note_index, $gs_instrument_number, $gs_effect_number, $gs_effect_value];
}
private function add_note_to_ntp(array $note_data)
{
list($gs_note_index, $gs_instrument_number, $gs_effect_number, $gs_effect_value) = $note_data;
/* sanity checks */
if($gs_note_index > 255)
{
throw new Exception('NinjaTracker+ does not allow a note value > 255!');
}
if($gs_instrument_number > self::$max_ntp_instruments)
{
throw new Exception(sprintf('NinjaTracker+ does not allow more than %s instruments!', self::$max_ntp_instruments));
}
if($gs_effect_number > 15)
{
throw new Exception('NinjaTracker+ does not allow effect numbers > 15!');
}
if($gs_effect_value > 255)
{
throw new Exception('NinjaTracker+ does not allow effect values > 255!');
}
$this->ntp_notes[] = $gs_note_index;
$this->ntp_notes[] = $gs_instrument_number;
$this->ntp_notes[] = $gs_effect_number;
$this->ntp_notes[] = $gs_effect_value;
}
private function convert_pattern($pattern_number)
{
$pattern_len = $this->no_of_tracks * self::$number_of_lines_in_pattern * self::$length_of_note;
$pattern_offset = $this->mod_headlen + $pattern_len * $pattern_number;
try {
for($line = 0; $line < self::$number_of_lines_in_pattern; $line++)
{
$line_offset = $line * $this->no_of_tracks * self::$length_of_note;
for($track = 0; $track < $this->no_of_tracks; $track++)
{
$track_offset = $track * self::$length_of_note;
$ptr = $pattern_offset + $line_offset + $track_offset;
$ntp_note_data = $this->convert_mod_note($ptr);
$this->add_note_to_ntp($ntp_note_data);
}
}
} catch(Exception $ex)
{
$this->pattern_errors[] = sprintf('Error in pattern %s, line %s, track %s: %s',
str_pad($pattern_number, 2, '0', STR_PAD_LEFT),
str_pad($line, 2, '0', STR_PAD_LEFT),
str_pad($track + 1, 2, '0', STR_PAD_LEFT),
$ex->getMessage()
);
}
}
private function convert_patterns()
{
for($pattern_number = 0; $pattern_number < $this->max_pat_cnt; $pattern_number++)
{
$this->convert_pattern($pattern_number);
}
}
private function save_ntp()
{
/* identifier */
$str = 'nfc!';
/* version */
$str .= chr(0);
/* number of tracks */
$str .= chr($this->no_of_tracks);
/* number of instruments */
$str .= chr(count($this->instrument_data_gs));
/* number of patterns */
$str .= chr($this->max_pat_cnt);
/* number of pattern order */
$str .= chr(count($this->pattern_order));
/* n bytes: output channel (0-7) for every track */
for($track_number = 1; $track_number <= $this->no_of_tracks; $track_number++)
{
$str .= chr(self::$doc_stereo_defaults[$track_number - 1]);
}
/* instrument data */
for($instrument_number_gs = 1; $instrument_number_gs <= count($this->instrument_data_gs); $instrument_number_gs++)
{
$str .= chr($this->instrument_types[$instrument_number_gs]);
$str .= $this->word_to_gs_str(count($this->instrument_data_gs[$instrument_number_gs]));
$str .= chr($this->instrument_volume_gs[$instrument_number_gs]);
$str .= chr($this->instrument_finetune_gs[$instrument_number_gs]);
$str .= chr($this->doc_ptrs[$instrument_number_gs]);
$str .= chr($this->doc_sizes[$instrument_number_gs]);
/*echo "num ".$instrument_number_gs;
echo ", type=".$this->instrument_types[$instrument_number_gs];
echo ", len=".count($this->instrument_data_gs[$instrument_number_gs]);
echo ", vol=".$this->instrument_volume_gs[$instrument_number_gs];
echo ", finetune=".$this->instrument_finetune_gs[$instrument_number_gs];
echo ", docptr=$".dechex($this->doc_ptrs[$instrument_number_gs]);
echo ", docsiz=%".decbin($this->doc_sizes[$instrument_number_gs]);
echo "\n";*/
}
/* add pattern order */
foreach($this->pattern_order as $byte)
{
$str .= chr($byte);
}
/* add pattern data */
foreach($this->ntp_notes as $byte)
{
$str .= chr($byte);
}
/* add instrument data */
foreach($this->instrument_data_gs as $instrument_data_gs)
{
foreach($instrument_data_gs as $byte)
{
$str .= chr($byte);
}
}
file_put_contents($this->path_output_ntp, $str);
}
private function load_mod()
{
$this->mod_data = file_get_contents($this->path_source_mod);
if($this->mod_data === false)
{
throw new Exception(sprintf('Unable to load MOD %s', $this->path_source_mod), self::FATAL_ERROR);
}
}
private function check_no_errors_occurred()
{
$no_errors = empty($this->instrument_errors) && empty($this->pattern_errors);
if($no_errors)
{
return true;
}
$show_max_pattern_errors = 10;
$number_of_pattern_errors = count($this->pattern_errors);
$all_errors = $this->instrument_errors;
$all_errors = array_merge($all_errors, array_slice($this->pattern_errors, 0, $show_max_pattern_errors));
if($number_of_pattern_errors > $show_max_pattern_errors)
{
$all_errors[] = sprintf('... (%s more pattern errors)', $number_of_pattern_errors - $show_max_pattern_errors);
}
throw new Exception(implode($all_errors, PHP_EOL));
}
private function list_instruments()
{
$instrument_map_reverse = array_flip($this->instrument_map);
foreach($this->instrument_data_gs as $index => $data)
{
echo $index.(isset($instrument_map_reverse[$index]) ? ' ('.$instrument_map_reverse[$index].')' : '')."\n";
echo "len=".count($data)."\n";
echo "typ=".$this->instrument_types[$index]."\n";
echo "\n";
}
}
public function convert()
{
try {
/* load mod */
$this->load_mod();
/* read basic information from mod */
$this->detect_mod_and_number_of_tracks();
$this->detect_number_of_patterns();
$this->calc_pattern_length();
$this->get_pattern_order();
/* convert instruments */
$this->extract_all_instrument_information();
$this->extract_all_instrument_data();
$this->optimize_instruments();
/* convert patterns */
$this->convert_patterns();
/* save, if there are no errors */
if($this->check_no_errors_occurred())
{
$this->save_ntp();
}
$result = ['success' => true];
}
catch(Exception $ex)
{
$result = ['success' => false, 'message' => $ex->getMessage(). PHP_EOL];
}
return $result;
}
}