diff --git a/scripts/MusicRenderer.gd b/scripts/MusicRenderer.gd index 2e0963b..82bc254 100644 --- a/scripts/MusicRenderer.gd +++ b/scripts/MusicRenderer.gd @@ -34,16 +34,18 @@ class TrackCurve: # built-in Curve class is too restrictive for this self.entries.append(entry) else: # Find the first entry bigger than pulse, and insert before for i in l: - if self.entries[i].x > pulse: + if self.entries[i].x == pulse: + self.entries[i] = entry # Replace existing, this makes sense for the VOLUME -> VOLUME_SLIDE pattern + elif self.entries[i].x > pulse: self.entries.insert(i, entry) break var last_pulse_block_get: int = -1 # Cache previous position for sequential lookups func get_pulse(pulse: float) -> float: var l := len(self.entries) - if l == 0 or pulse < self.entries[-1].x: + if l == 0 or pulse < self.entries[0].x: return self.default - if pulse > self.entries[-1].x: + if pulse >= self.entries[-1].x: return self.entries[-1].y for i in l-2: # Find first entry beyond @@ -94,7 +96,25 @@ class TrackCurve: # built-in Curve class is too restrictive for this return integral return 0.0 +static func get_gcd(a: int, b: int) -> int: # Greatest Common Divisor of two numbers + # Euclidian reduction + var c + while b: + c = b + b = a % b + a = c + return a +static func get_lcm(a: int, b: int) -> int: # Least Common Multiple of two numbers + return (a * b) / get_gcd(a, b) + +static func get_lcm_n(values: Array) -> int: # Least Common Multiple of an array of numbers + var lcm: int = values.pop_back() + for value in values: + lcm = get_lcm(lcm, value) + return lcm + +const LOOP_OVERSHOOT := 768 static func render_channels(tracks: Array, inst_map: Array, _debug_name := 'none') -> Array: # [data: PoolByteArray, target_time_length: float in seconds] # Since some channels contain global events (tempo and global volume for now), # the strategy will be to preprocess each channel in a global-state-agnostic way, @@ -104,13 +124,13 @@ static func render_channels(tracks: Array, inst_map: Array, _debug_name := 'none var sample_default_adsrs = RomLoader.snes_data.sfx_adsrs + RomLoader.snes_data.bgm_instrument_adsrs # TODO: UNHARDCODE THIS var all_note_events = [] - var curve_master_volume := TrackCurve.new(100.0/255.0) # [0.0, 1.0] for now + var curve_master_volume := TrackCurve.new(1.0) # [0.0, 1.0] for now var curve_master_tempo := TrackCurve.new(120.0) # bpm is too big, need pulses per second var curve_channel_pans := [] for channel in NUM_TRACKS: - var curve_velocity := TrackCurve.new(100.0/255.0) # [0.0, 1.0] for now + var curve_velocity := TrackCurve.new(1.0) # [0.0, 1.0] for now var curve_pan := TrackCurve.new() # [-1.0, 1.0] for now # Stored and unused for now var curve_fine_tuning := TrackCurve.new() # [0.0, 1.0] for now @@ -145,7 +165,7 @@ static func render_channels(tracks: Array, inst_map: Array, _debug_name := 'none var current_instrument := 0 var current_octave := 5 var current_transpose := 0 - # var current_velocity := 100 + # var current_velocity := 255 var current_adsr_attack_rate := 0 var current_adsr_decay_rate := 0 var current_adsr_sustain_level := 0 @@ -342,14 +362,17 @@ static func render_channels(tracks: Array, inst_map: Array, _debug_name := 'none # Integrate tempo so we can get a pulse->time mapping curve_master_tempo.bake_integrals() # Find the longest channel - var channel_loop_p_returns = PoolIntArray() - var channel_loop_p_lengths = PoolIntArray() + var channel_loop_p_returns := PoolIntArray() + var channel_loop_p_lengths := PoolIntArray() + var channel_p_ends := PoolIntArray() var longest_channel_idx = 0 var longest_channel_p_end = 0 var highest_channel_p_return = -1 for channel in NUM_TRACKS: if all_note_events[channel].empty(): channel_loop_p_returns.append(-1) + channel_loop_p_lengths.append(0) + channel_p_ends.append(0) continue var note_event: NoteEvent = all_note_events[channel][-1] var p_end = note_event.p_end @@ -364,13 +387,28 @@ static func render_channels(tracks: Array, inst_map: Array, _debug_name := 'none channel_loop_p_returns.append(-1) channel_loop_p_lengths.append(0) + channel_p_ends.append(p_end) if p_end > longest_channel_p_end: longest_channel_p_end = p_end longest_channel_idx = channel - var target_pulse_length = longest_channel_p_end + 200 + var target_pulse_length = longest_channel_p_end + LOOP_OVERSHOOT var target_time_length = curve_master_tempo.get_integral(target_pulse_length) + # # DEBUG: calculate LCM of the loop lengths to determine required length for a true loop point + # var unique_loop_lengths := [] + # # TODO: find common loop pulses + # for loop_length in channel_loop_p_lengths: + # if loop_length > 0 and not (loop_length in unique_loop_lengths): + # unique_loop_lengths.append(loop_length) + # if unique_loop_lengths: + # var loop_length_lcm = get_lcm_n(unique_loop_lengths) + # var p_overall_loop_start = highest_channel_p_return #+ LOOP_OVERSHOOT + # var p_overall_loop_end = p_overall_loop_start + loop_length_lcm + # print('%s has lcm loop length %d pulses from loop lengths %s, from %d to %d.' % [_debug_name, loop_length_lcm, channel_loop_p_lengths, p_overall_loop_start, p_overall_loop_end]) + # else: + # print('%s does not loop' % _debug_name) + # Second pass - encode the notes with the now-known global tempo and volume curves var data := PoolByteArray() for channel in NUM_TRACKS: diff --git a/scripts/loaders/snes/music.gd b/scripts/loaders/snes/music.gd index 8d3f8a1..b217f66 100644 --- a/scripts/loaders/snes/music.gd +++ b/scripts/loaders/snes/music.gd @@ -309,7 +309,7 @@ func unroll_track(buffer: StreamPeerBuffer, bgm_start_pos: int, track_start_pos: print_info('Infinite GOTO found in track %s at 0x%06X:0x%06X:0x%06X: 0x%06X to 0x%06X (event number %d)' % [bgm_id, bgm_start_pos, track_start_pos, bgm_end_pos, pos, target_pos, event_num]) events.append([EventType.GOTO, rom_address_to_event_index[target_pos]]) break - else: + else: # The GOTO address isn't in our index yet, proceed to it print_debug('forward GOTO found in track %s at 0x%06X:0x%06X:0x%06X: 0x%06X to 0x%06X' % [bgm_id, bgm_start_pos, track_start_pos, bgm_end_pos, pos, target_pos]) buffer.seek(target_pos) _: @@ -318,25 +318,32 @@ func unroll_track(buffer: StreamPeerBuffer, bgm_start_pos: int, track_start_pos: # DEBUG: Report if track doesn't end all the contained loops if loop_level >= 0: print_info('track %s ended on loop_level %d (0x%06X)' % [bgm_id, loop_level, loop_positions[loop_level]]) - # DEBUG: Report total note duration and repeat note duration - if events: - print_info('%d events' % len(events)) - var total_note_dur := 0 - var total_notes := 0 - var total_rests := 0 - for e in events: - if e[0] == EventType.NOTE: - total_note_dur += e[2] - if e[1] >= 0: - total_notes += 1 - else: - total_rests += 1 - if events[-1][0] == EventType.GOTO: - var repeat_note_dur := 0 - for e in events.slice(events[-1][1], -1): - if e[0] == EventType.NOTE: - repeat_note_dur += e[2] - print_info('track %s has duration %d and repeat duration %d (intro %d) - %d notes %d rests' % [bgm_id, total_note_dur, repeat_note_dur, total_note_dur-repeat_note_dur, total_notes, total_rests]) - else: - print_info('track %s has duration %d - %d notes %d rests' % [bgm_id, total_note_dur, total_notes, total_rests]) + # # DEBUG: Report total note duration and repeat note duration + # if events: + # print_info('%d events' % len(events)) + # var total_note_dur := 0 + # var total_notes := 0 + # var total_rests := 0 + # for e in events: + # if e[0] == EventType.NOTE: + # total_note_dur += e[2] + # if e[1] >= 0: + # total_notes += 1 + # else: + # total_rests += 1 + # if events[-1][0] == EventType.GOTO: + # var repeat_note_dur := 0 + # for e in events.slice(events[-1][1], -1): + # if e[0] == EventType.NOTE: + # repeat_note_dur += e[2] + # print_info('track %s has duration %d and repeat duration %d (intro %d) - %d notes %d rests' % [bgm_id, total_note_dur, repeat_note_dur, total_note_dur-repeat_note_dur, total_notes, total_rests]) + # events[-1].append_array([total_note_dur-repeat_note_dur, total_note_dur]) + # else: + # print_info('track %s has duration %d - %d notes %d rests' % [bgm_id, total_note_dur, total_notes, total_rests]) return events + +func unroll_bgm(buffer: StreamPeerBuffer, bgm_start_address: int, track_start_addresses: PoolIntArray, bgm_end_address: int, bgm_id='N/A') -> Array: + var tracks := [] + for track_start_address in track_start_addresses: + tracks.append(self.unroll_track(buffer, bgm_start_address, track_start_address, bgm_end_address, bgm_id)) + return tracks diff --git a/test/audio_system.gd b/test/audio_system.gd index e319215..7be3138 100644 --- a/test/audio_system.gd +++ b/test/audio_system.gd @@ -85,13 +85,7 @@ func evaluate_bgm(id: int): var end_ptr := buffer.get_u16() + bank_offset if end_ptr < bgm_song_ptr: end_ptr += 0x010000 # next bank - var tracks := [] - for i in NUM_CHANNELS: - var track_ptr := track_ptrs[i] - # var channel: MusicChannel = self.channels[i] - # print('Unrolling BGM track %02d:%02d at 0x%06X:0x%06X:0x%06X' % [id, i, bgm_song_ptr, track_ptr, end_ptr]) - tracks.append(MusicLoader.unroll_track(buffer.duplicate(), bgm_song_ptr, track_ptr, end_ptr, '%02d:%02d'%[id, i])) - bgm_tracksets[id] = tracks + bgm_tracksets[id] = MusicLoader.unroll_bgm(buffer.duplicate(), bgm_song_ptr, track_ptrs, end_ptr, '%02d'%id) func _play_bgm_jaot(key: String) -> void: self.queued_bgm_playback = '' @@ -294,6 +288,7 @@ func _on_render_initial_ready(key: String): rendered_audio.loop_begin = int(round(prerendered_bgm_start_and_end_loops[key][0])) rendered_audio.loop_end = int(round(prerendered_bgm_start_and_end_loops[key][1])) rendered_audio.loop_mode = AudioStreamSample.LOOP_FORWARD + print('Should loop from %.2fs to %.2fs' % [rendered_audio.loop_begin/32000.0, rendered_audio.loop_end/32000.0]) self.prerendered_bgms[key] = rendered_audio if self.queued_bgm_playback == key: @@ -311,6 +306,7 @@ func _on_render_complete(key: String): print_debug('render_completed signal for incomplete render! %s has %d remaining samples, should be 0'%[key, remaining_samples_and_data[0]]) # Assume _on_render_initial_ready was already called and AudioStreamSample has already been created self.prerendered_bgms[key].data = data + print('Should loop from %.2fs to %.2fs' % [self.prerendered_bgms[key].loop_begin/32000.0, self.prerendered_bgms[key].loop_end/32000.0]) if save_prerendered_audio: var error = self.prerendered_bgms[key].save_to_wav('output/rendered_%s.wav'%key) print('@%dms - Saved render of %s (error code %s)' % [get_ms(), key, globals.ERROR_CODE_STRINGS[error]])