diff --git a/data/5/addresses_SNES_PSX.tsv b/data/5/addresses_SNES_PSX.tsv index 992af1b..bfe7133 100644 --- a/data/5/addresses_SNES_PSX.tsv +++ b/data/5/addresses_SNES_PSX.tsv @@ -14,14 +14,14 @@ bytelength_sfx_brr_data 0x041E3F u16 Used by the memcpy routine that copies th sfx_brr_data 0x041E41 Use the below SPC pointers bytelength_sfx_brr_pointers 0x041F4F u16 Used by the memcpy routine that copies the below data to the SPC (0x0020 = 32 bytes) sfx_brr_pointers 0x041F51 8 of 2 of u16 SPC memory addresses not ROM. Start address followed by loop address. -sfx_adsrs 0x041F71 8 of u16 +sfx_adsrs 0x041F71 8 of 4 of u4 sfx_samplerates 0x041F83 8 of u16 sfx_data 0x041F95 Contains SPC pointers and tracks bgm_song_pointers 0x043B97 72 of u24 bgm_instrument_brr_pointers 0x043C6F 35 of u24 bgm_instrument_loop_starts 0x043CD8 35 of u16 bgm_instrument_samplerates 0x043D1E 35 of u16 -bgm_instrument_adsrs 0x043D64 35 of u16 +bgm_instrument_adsrs 0x043D64 35 of 4 of u4 bgm_instrument_indices 0x043DAA 72 of 16 of u16 length 0x900 worldmap_compressed_tilesets 0x070000 tilesets 0 up to 0x434 diff --git a/data/5/bgm_titles.txt b/data/5/bgm_titles.txt new file mode 100644 index 0000000..863106e --- /dev/null +++ b/data/5/bgm_titles.txt @@ -0,0 +1,70 @@ +Ahead on our way +The Fierce Battle +A Presentiment +Go Go Boko! +Pirates Ahoy +Tenderness in the Air +Fate in Haze +Moogle theme +Prelude/Crystal Room +The Last Battle +Requiem +Nostalgia +Cursed Earths +Lenna's Theme +Victory's Fanfare +Deception +The Day Will Come +[nothing] +ExDeath's Castle +My Home, Sweet Home +Waltz Suomi +Sealed Away +The Four Warriors of Dawn +Danger +The Fire Powered Ship +As I Feel, You Feel +Mambo de Chocobo! +Music Box +Intension of the Earth +The Dragon Spreads its Wings +Beyond the Deep Blue Sea +The Prelude of Empty Skies +Searching the Light +Harvest +Battle with Gilgamesh +Four Valiant Hearts +The Book of Sealings +What? +Hurry! Hurry! +Unknown Lands +The Airship +Fanfare 1 +Fanfare 2 +The Battle +Walking the Snowy Mountains +The Evil Lord Exdeath +The Castle of Dawn +I'm a Dancer +Reminiscence +Run! +The Ancient Library +Royal Palace +Good Night! +Piano Lesson 1 +Piano Lesson 2 +Piano Lesson 3 +Piano Lesson 4 +Piano Lesson 5 +Piano Lesson 6 +Piano Lesson 7 +Piano Lesson 8 +Musica Machina +[meteor falling?] +The Land Unknown +The Decisive Battle +The Silent Beyond +Dear Friends +Final Fantasy +A New Origin +[chirping sound] diff --git a/globals.gd b/globals.gd index cdd043b..4b56043 100644 --- a/globals.gd +++ b/globals.gd @@ -42,6 +42,8 @@ const POST_ROM_MENUS = [ Menu.DEBUG_WORLD_MAP_BLOCKS, ] +const ERROR_CODE_STRINGS = PoolStringArray(['OK', 'FAILED', 'ERR_UNAVAILABLE', 'ERR_UNCONFIGURED', 'ERR_UNAUTHORIZED', 'ERR_PARAMETER_RANGE_ERROR', 'ERR_OUT_OF_MEMORY', 'ERR_FILE_NOT_FOUND', 'ERR_FILE_BAD_DRIVE', 'ERR_FILE_BAD_PATH', 'ERR_FILE_NO_PERMISSION', 'ERR_FILE_ALREADY_IN_USE', 'ERR_FILE_CANT_OPEN', 'ERR_FILE_CANT_WRITE', 'ERR_FILE_CANT_READ', 'ERR_FILE_UNRECOGNIZED', 'ERR_FILE_CORRUPT', 'ERR_FILE_MISSING_DEPENDENCIES', 'ERR_FILE_EOF', 'ERR_CANT_OPEN', 'ERR_CANT_CREATE', 'ERR_QUERY_FAILED', 'ERR_ALREADY_IN_USE', 'ERR_LOCKED', 'ERR_TIMEOUT', 'ERR_CANT_CONNECT', 'ERR_CANT_RESOLVE', 'ERR_CONNECTION_ERROR', 'ERR_CANT_ACQUIRE_RESOURCE', 'ERR_CANT_FORK', 'ERR_INVALID_DATA', 'ERR_INVALID_PARAMETER', 'ERR_ALREADY_EXISTS', 'ERR_DOES_NOT_EXIST', 'ERR_DATABASE_CANT_READ', 'ERR_DATABASE_CANT_WRITE', 'ERR_COMPILATION_FAILED', 'ERR_METHOD_NOT_FOUND', 'ERR_LINK_FAILED', 'ERR_SCRIPT_FAILED', 'ERR_CYCLIC_LINK', 'ERR_INVALID_DECLARATION', 'ERR_DUPLICATE_SYMBOL', 'ERR_PARSE_ERROR', 'ERR_BUSY', 'ERR_SKIP', 'ERR_HELP', 'ERR_BUG', 'ERR_PRINTER_ON_FIRE']) + const FOLDER_ICON := preload('res://theme/icons/file_folder.tres') const ALLOWED_EXTS := PoolStringArray(['bin', 'iso', 'sfc', 'smc', 'srm', 'gba']) const CD_EXTS := PoolStringArray(['bin', 'iso']) # If you have a weird disc image format, you can mount it yourself, leave me out of it diff --git a/scripts/MusicPlayer.gd b/scripts/MusicPlayer.gd index eb5fb38..28f3d3c 100644 --- a/scripts/MusicPlayer.gd +++ b/scripts/MusicPlayer.gd @@ -233,3 +233,381 @@ func _process(delta: float) -> void: self.is_playing = play_pulse(bgm_timestamp) if not self.is_playing: print('BGM finished playing') + + +# TODO: need to interleave channels for tempo and master volume! +const MAX_NOTE_EVENTS := 2048 +class NoteEvent: + var p_start: int # In pulse space + var p_end: int + var instrument: int + var pitch: int + var velocity: float + var adsr_attack: int + var adsr_decay: int + var adsr_sustain: int + var adsr_release: int + + +class TrackCurve: # built-in Curve class is too restrictive for this + var default: float + var entries: PoolVector3Array + var baked_integrals: PoolRealArray + func _init(default: float = 0.0): + self.default = default + self.entries = PoolVector3Array() + self.baked_integrals = PoolRealArray() + + func add_point(pulse: int, value: float, ramp_to_next: bool) -> void: + var l := len(self.entries) + var entry := Vector3(float(pulse), value, float(ramp_to_next)) + if l == 0 or self.entries[-1].x < pulse: + 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: + 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: + return self.default + if pulse > self.entries[-1].x: + return self.entries[-1].y + for i in l-2: + # Find first entry beyond + if pulse < self.entries[i+1].x: + if self.entries[i].z > 0: # ramp_to_next + return range_lerp(pulse, self.entries[i].x, self.entries[i+1].x, self.entries[i].y, self.entries[i+1].y) + else: + return self.entries[i].y + return self.default # Should be unreachable + + func bake_integrals(): + # Store the starting integrated value (i.e. time for the tempo curve) of each pulse value + self.baked_integrals.clear() + var last_pulse := 0.0 + var last_value := self.default + var last_integral := 0.0 + var last_ramp := false + for entry in self.entries: + var step_pulse = entry.x - last_pulse + var integral := last_integral + if last_ramp: + # Treat it as a rectangle where the height is the average of the slanted top. + integral += step_pulse * (last_value + entry.y)/2.0 + else: + integral += step_pulse * last_value + self.baked_integrals.append(integral) + last_pulse = entry.x + last_value = entry.y + last_integral = integral + last_ramp = entry.z > 0 + + var last_integral_block_get: int = -1 # Cache previous position for sequential lookups + func get_integral(pulse: float) -> float: + # This is for tempo -> time. Need to bake it to have any hope of efficiency. + if self.baked_integrals.empty(): + self.bake_integrals() + # Find first entry earlier than the pulse + for i in range(len(self.entries)-1, -1, -1): + var entry = self.entries[i] + if pulse > entry.x: + var integral = self.baked_integrals[i] + var step_pulse = pulse - entry.x + if entry.z: # Ramp to next + # Treat it as a rectangle where the height is the average of the slanted top. + integral += step_pulse * (entry.y + entries[i+1].y)/2.0 # If last entry somehow has ramp-to-next (it shouldn't), this will out-of-range error + else: + integral += step_pulse * entry.y + return integral + return 0.0 + + +func render_channels(_t_start: float, _t_end: float, inst_map: Array) -> 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, + # then once all the global tracks are known, as well as the longest unlooped length, + # do a second pass to generate the final events + var instrument_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_tempo := TrackCurve.new(120.0) # bpm is too big, need pulses per second + + var curve_channel_pans := [] + + for channel in self.num_tracks: + var curve_velocity := TrackCurve.new(100.0/255.0) # [0.0, 1.0] for now + var curve_pan := TrackCurve.new() # [-1.0, 1.0] for now + var channel_note_events = [] + var track: Array = self.tracks[channel] + var l := len(track) + var p := 0 # current pulse + + if l == 0: # Empty channel, move on + all_note_events.append(channel_note_events) + curve_channel_pans.append(curve_pan) + continue + + # var num_notes: int = 0 + var current_instrument := 0 + var current_octave := 5 + var current_transpose := 0 + # var current_velocity := 100 + var current_adsr_attack := 0 + var current_adsr_decay := 0 + var current_adsr_sustain := 0 + var current_adsr_release := 0 + + # First, check if it ends in a GOTO, then store the program counter of the destination + var infinite_loop_target_program_counter = -1 + var infinite_loop_target_pulse = -1 + if track[-1][0] == EventType.GOTO: + infinite_loop_target_program_counter = track[-1][1] + + var program_counter := 0 + while true: #num_notes < MAX_NOTE_EVENTS: + if program_counter >= l: + break + if program_counter == infinite_loop_target_program_counter: + infinite_loop_target_pulse = p + var event = track[program_counter] + program_counter += 1 + match event[0]: # Control codes + EventType.GOTO: # This is a preprocessed event list, so GOTO is a final infinite loop marker + var note_event = NoteEvent.new() + note_event.p_start = p + note_event.p_end = infinite_loop_target_pulse # Fake final note event using p_start > p_end to encode the infinite jump back loop. + # Note that event[1] points to an Event, not a NoteEvent, not a Pulse, so we looked it up earlier + channel_note_events.append(note_event) + break + EventType.MASTER_VOLUME: + curve_master_volume.add_point(p, event[1]/255.0, false) + EventType.TEMPO: + var new_tempo = music.tempo_to_seconds_per_pulse(event[1]) + curve_master_tempo.add_point(p, new_tempo, false) + EventType.TEMPO_SLIDE: + var old_tempo = curve_master_tempo.get_pulse(p) + var new_tempo = music.tempo_to_seconds_per_pulse(event[2]) + var slide_duration: int = event[1] # TODO: work out how this is scaled + curve_master_tempo.add_point(p, old_tempo, true) + curve_master_tempo.add_point(p + slide_duration, new_tempo, false) + EventType.NOTE: + var note = event[1] + var duration = event[2] + if note >= 0: # Don't shift or play rests + note += (12 * current_octave) + current_transpose + var note_event = NoteEvent.new() + note_event.p_start = p + note_event.p_end = p + duration + note_event.instrument = current_instrument + note_event.pitch = note # pitch_idx #* self.channel_fine_tuning[channel] + note_event.velocity = curve_velocity.get_pulse(p) # current_velocity + note_event.adsr_attack = current_adsr_attack + note_event.adsr_decay = current_adsr_decay + note_event.adsr_sustain = current_adsr_sustain + note_event.adsr_release = current_adsr_release + channel_note_events.append(note_event) + # num_notes += 1 + p += duration + EventType.VOLUME: + var new_velocity: float = event[1]/255.0 + curve_velocity.add_point(p, new_velocity, false) + EventType.VOLUME_SLIDE: # TODO: implement slides + var old_velocity = curve_velocity.get_pulse(p) + var slide_duration: int = event[1] + var new_velocity: float = event[2]/255.0 + curve_velocity.add_point(p, old_velocity, true) + curve_velocity.add_point(p + slide_duration, new_velocity, false) + EventType.PAN: + var new_pan = 1.0 - event[1]/127.5 + curve_pan.add_point(p, new_pan, false) + EventType.PAN_SLIDE: # TODO: implement slides + var old_pan = curve_pan.get_pulse(p) + var new_pan = 1.0 - event[2]/127.5 + var slide_duration: int = event[1] # TODO: work out how slides are scaled + curve_pan.add_point(p, old_pan, true) + curve_pan.add_point(p + slide_duration, new_pan, false) + EventType.PITCH_SLIDE: # TODO: implement slides + var slide_duration: int = event[1] + var target_pitch: int = event[2] # Signed + EventType.OCTAVE: + current_octave = event[1] + EventType.OCTAVE_UP: + current_octave += 1 + EventType.OCTAVE_DOWN: + current_octave -= 1 + EventType.TRANSPOSE_ABS: + current_transpose = event[1] + EventType.TRANSPOSE_REL: + current_transpose += event[1] + EventType.TUNING: + var fine_tune: int = event[1] + var scale: float + if fine_tune < 0x80: + scale = 1.0 + fine_tune/255.0 + else: + scale = fine_tune/255.0 + self.channel_fine_tuning[channel] = scale + EventType.PROGCHANGE: + var event_idx = event[1]-0x20 + if event_idx >= 0: + current_instrument = inst_map[event_idx] - 1 + if current_instrument < len(instrument_adsrs) and current_instrument > 0: + var adsr = instrument_adsrs[current_instrument] + current_adsr_attack = adsr[2] + current_adsr_decay = adsr[3] + current_adsr_sustain = adsr[0] + current_adsr_release = adsr[1] + EventType.ADSR_DEFAULT: # TODO - Investigate actual scaling and order + if current_instrument < len(instrument_adsrs) and current_instrument > 0: + var adsr = instrument_adsrs[current_instrument] + current_adsr_attack = adsr[2] + current_adsr_decay = adsr[3] + current_adsr_sustain = adsr[0] + current_adsr_release = adsr[1] + EventType.ADSR_ATTACK: + current_adsr_attack = event[1] + EventType.ADSR_DECAY: + current_adsr_decay = event[1] + EventType.ADSR_SUSTAIN: + current_adsr_sustain = event[1] + EventType.ADSR_RELEASE: + current_adsr_release = event[1] + EventType.VIBRATO_ON: + self.channel_vibrato_delay[channel] = event[1] + self.channel_vibrato_rate[channel] = event[2] + self.channel_vibrato_depth[channel] = event[3] + self.channel_vibrato_on[channel] = 1 + EventType.VIBRATO_OFF: + self.channel_vibrato_on[channel] = 0 + EventType.TREMOLO_ON: + self.channel_tremolo_delay[channel] = event[1] + self.channel_tremolo_rate[channel] = event[2] + self.channel_tremolo_depth[channel] = event[3] + self.channel_tremolo_on[channel] = 1 + EventType.TREMOLO_OFF: + self.channel_tremolo_on[channel] = 0 + EventType.PAN_LFO_ON: + self.channel_pan_lfo_depth[channel] = event[1] + self.channel_pan_lfo_rate[channel] = event[2] + self.channel_pan_lfo_on[channel] = 1 + EventType.PAN_LFO_OFF: + self.channel_pan_lfo_on[channel] = 0 + EventType.NOISE_FREQ: + self.channel_noise_freq[channel] = event[1] + EventType.NOISE_ON: + self.channel_noise_on[channel] = 1 + EventType.NOISE_OFF: + self.channel_noise_on[channel] = 0 + EventType.PITCHMOD_ON: + self.channel_pitchmod_on[channel] = 1 + EventType.PITCHMOD_OFF: + self.channel_pitchmod_on[channel] = 0 + EventType.ECHO_ON: + self.channel_echo_on[channel] = 1 + EventType.ECHO_OFF: + self.channel_echo_on[channel] = 0 + EventType.ECHO_VOLUME: + self.channel_echo_volume[channel] = event[1] + EventType.ECHO_VOLUME_SLIDE: # TODO: implement slides + self.channel_echo_volume[channel] = event[2] + var slide_duration: int = event[1] + EventType.ECHO_FEEDBACK_FIR: # TODO + var feedback: int = event[1] + var filterIndex: int = event[2] + EventType.END: + break + _: + break + # End of track + all_note_events.append(channel_note_events) + curve_channel_pans.append(curve_pan) + + # 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 longest_channel_idx = 0 + var longest_channel_p_end = 0 + var highest_channel_p_return = -1 + for channel in self.num_tracks: + if all_note_events[channel].empty(): + channel_loop_p_returns.append(-1) + continue + var note_event: NoteEvent = all_note_events[channel][-1] + var p_end = note_event.p_end + if p_end < note_event.p_start: + # Ends on infinite loop + channel_loop_p_returns.append(p_end) + channel_loop_p_lengths.append(note_event.p_start - p_end) + if p_end > highest_channel_p_return: + highest_channel_p_return = p_end + p_end = note_event.p_start + else: + channel_loop_p_returns.append(-1) + + 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_time_length = curve_master_tempo.get_integral(target_pulse_length) + + # Second pass - encode the notes with the now-known global tempo and volume curves + var data := PoolByteArray() + for channel in self.num_tracks: + var events = all_note_events[channel] + var loop_return_note_event_idx = -1 + var loop_return_p = channel_loop_p_returns[channel] + var curve_pan: TrackCurve = curve_channel_pans[channel] + + var midi_events_bytes_t_start := StreamPeerBuffer.new() + var midi_events_bytes_t_end := StreamPeerBuffer.new() + var midi_events_bytes3 := StreamPeerBuffer.new() + var midi_events_bytes_adsr := StreamPeerBuffer.new() + + var num_notes: int = 0 + var event_ptr := 0 + var l_events := len(events) + var loop_p_offset := 0 + for i in MAX_NOTE_EVENTS: + if event_ptr >= l_events: + break + if (loop_return_p >= 0) and event_ptr == l_events-1: + event_ptr = loop_return_note_event_idx + loop_p_offset += channel_loop_p_lengths[channel] + var event: NoteEvent = events[event_ptr] + var p = event.p_start + if loop_return_note_event_idx < 0 and p >= loop_return_p: + loop_return_note_event_idx = event_ptr + midi_events_bytes_t_start.put_32(int(curve_master_tempo.get_integral(p + loop_p_offset) * 32000)) + midi_events_bytes_t_end.put_32(int(curve_master_tempo.get_integral(event.p_end + loop_p_offset) * 32000)) # t_end + midi_events_bytes3.put_u8(event.instrument) + midi_events_bytes3.put_u8(event.pitch) + midi_events_bytes3.put_u8(int(event.velocity * curve_master_volume.get_pulse(p) * 255.0)) # velocity + midi_events_bytes3.put_u8(int((curve_pan.get_pulse(p)+1.0) * 127.5)) # pan + midi_events_bytes_adsr.put_u8(event.adsr_attack) + midi_events_bytes_adsr.put_u8(event.adsr_decay) + midi_events_bytes_adsr.put_u8(event.adsr_sustain) + midi_events_bytes_adsr.put_u8(event.adsr_release) + + event_ptr += 1 + num_notes += 1 + # Fill up end of notes array with dummies + for i in range(num_notes, MAX_NOTE_EVENTS): + midi_events_bytes_t_start.put_32(0x0FFFFFFF) + midi_events_bytes_t_end.put_32(0x0FFFFFFF) + midi_events_bytes3.put_32(0) + midi_events_bytes_adsr.put_32(0) + data += midi_events_bytes_t_start.data_array + midi_events_bytes_t_end.data_array + midi_events_bytes3.data_array + midi_events_bytes_adsr.data_array + var smp_loop_start = -1 + var smp_loop_end = -1 + if highest_channel_p_return > 0: + smp_loop_start = curve_master_tempo.get_integral(highest_channel_p_return + 100) * 32000 + smp_loop_end = curve_master_tempo.get_integral(longest_channel_p_end + 100) * 32000 + return [data, target_time_length, [smp_loop_start, smp_loop_end]] diff --git a/scripts/loaders/SoundLoader.gd b/scripts/loaders/SoundLoader.gd index afe8f37..596d688 100644 --- a/scripts/loaders/SoundLoader.gd +++ b/scripts/loaders/SoundLoader.gd @@ -18,16 +18,25 @@ const BYTES_PER_SAMPLE := 2 # 16bit samples # !!! Adding a few ms to the loops removes harshness. !!! const HACK_EXTEND_LOOP_SAMPLE_EXTRA_MS := 2 # !!! func HACK_EXTEND_LOOP_SAMPLE(audio: AudioStreamSample) -> AudioStreamSample: # !!! - if audio.loop_begin >= audio.loop_end: # !!! - return audio # !!! + var output: AudioStreamSample = audio.duplicate(true) # !!! + # Prepend silence # !!! + var silent_samples := (audio.mix_rate * PREPEND_MS) / 1000 # !!! + var silence := PoolByteArray() # !!! + silence.resize(silent_samples * 2) # 16bit samples in 8bit array # !!! + silence.fill(0) # !!! + output.data = silence + output.data # !!! + output.loop_begin += silent_samples # !!! + output.loop_end += silent_samples # !!! + # Append looped samples # !!! + if output.loop_begin >= output.loop_end: # !!! + return output # !!! var looped_samples = audio.data.subarray(audio.loop_begin * BYTES_PER_SAMPLE, -1) # !!! var loop_len = len(looped_samples) # !!! var target_len = (audio.mix_rate * HACK_EXTEND_LOOP_SAMPLE_EXTRA_MS / 1000) * BYTES_PER_SAMPLE # !!! while loop_len < target_len: # Keep doubling in length until it's long enough !!! looped_samples += looped_samples # !!! loop_len = len(looped_samples) # !!! - var output = audio.duplicate(true) # !!! - output.data = audio.data + looped_samples # !!! + output.data += looped_samples # !!! return output # !!! # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -81,14 +90,14 @@ func make_sample(buffer: StreamPeerBuffer, size: int, sample_rate: int) -> Audio return audio var num_packets := size/9 - var samples = PoolIntArray([0, 0]) # Start with two zero samples for filter purposes, strip them from the actual output + var samples = PoolIntArray([0, 0]) # Start with two zero samples for filter purposes, strip them from the actual output later var i := 2 for pkt in num_packets: # Decode a single 9byte BRR packet var header_byte := buffer.get_u8() var exponent := header_byte >> 4 var filter := (header_byte >> 2) & 0x03 - var loop := bool(header_byte & 0x02) + # var loop := bool(header_byte & 0x02) var end := bool(header_byte & 0x01) for sample in 8: var b := buffer.get_u8() @@ -109,30 +118,26 @@ func make_sample(buffer: StreamPeerBuffer, size: int, sample_rate: int) -> Audio if end: # print('End flag on packet') break - # Convert int array to byte array - var audio_data = PoolByteArray() - # Prepend silence, accounting for the two null samples - var silent_samples := ((sample_rate * PREPEND_MS) / 1000) - 2 - audio_data.resize(silent_samples * 2) # 16bit samples in 8bit array - audio_data.fill(0) + # Remove first two zero samples + samples.remove(0) + samples.remove(0) # Pack 16bit samples to 8bit array - for b in samples: - audio_data.append(b & 0xFF) - audio_data.append(b >> 8) - audio.data = audio_data + var out_buff = StreamPeerBuffer.new() + for sample in samples: + out_buff.put_16(sample) + audio.data = out_buff.data_array return audio func get_inst_sample_data(snes_data: Dictionary, buffer: StreamPeerBuffer, id: int) -> AudioStreamSample: var sample_rate := get_reference_pitch_samplerate(snes_data.bgm_instrument_samplerates[id] & 0xFF) - var silent_samples := ((sample_rate * PREPEND_MS) / 1000) var loop_start_packet: int = snes_data.bgm_instrument_loop_starts[id]/9 # Note that Instrument $1F Steel Guitar has a length of $088B but a loop point of $088D which is 243.22... packets. Luckily it doesn't matter. buffer.seek(snes_data.bgm_instrument_brr_pointers[id] & 0x3FFFFF) var size := buffer.get_u16() var num_samples := (size/9)*16 var audio := make_sample(buffer, size, sample_rate) audio.loop_mode = AudioStreamSample.LOOP_FORWARD - audio.loop_begin = (loop_start_packet * 16) + silent_samples # Each 9byte packet is 16 samples - audio.loop_end = silent_samples + num_samples + audio.loop_begin = (loop_start_packet * 16) # Each 9byte packet is 16 samples + audio.loop_end = num_samples # print_debug('Loaded instrument #%02X with lookup offset $%06X, BRR data offset $%06X, length $%04X (%f packets, %d samples) and loop point %d samples' % [id, lookup_offset, brr_offset, size, size/9.0, num_samples, audio.loop_begin]) return audio @@ -148,11 +153,10 @@ func load_sfx_samples_data(snes_data: Dictionary, buffer: StreamPeerBuffer): buffer.seek(brr_spc_addrs[i] + brr_spc_start) # print('Loading sfx sample #%X with BRR data offset $%06X' % [i, buffer.get_position()]) var sample_rate := get_reference_pitch_samplerate(snes_data.sfx_samplerates[i] & 0xFF) - var silent_samples := ((sample_rate * PREPEND_MS) / 1000) var audio := make_sample(buffer, 900, sample_rate) var loop_start_packet: int = brr_spc_loop_addrs[i] - brr_spc_addrs[i] audio.loop_mode = AudioStreamSample.LOOP_FORWARD - audio.loop_begin = (loop_start_packet * 16) + silent_samples # Each 9byte packet is 16 samples + audio.loop_begin = loop_start_packet * 16 # Each 9byte packet is 16 samples audio.loop_end = (len(audio.data)/2) sfx_samples.append(audio) # Use 900 as a limit, it won't be hit, parser stops after End packet anyway emit_signal('audio_sfx_sample_loaded', i) @@ -164,22 +168,97 @@ func load_samples(snes_data: Dictionary, buffer: StreamPeerBuffer): load_sfx_samples_data(snes_data, buffer) # For some reason, this is a bit slow currently under certain editor conditions. Might optimize later. for i in INST_NUM: - instrument_samples.append(get_inst_sample_data(snes_data, buffer, i)) + var samp := get_inst_sample_data(snes_data, buffer, i) + instrument_samples.append(samp) # Workaround for Godot 3.x quirk where looping samples are interpolated as if they go to nothing instead of looping - instrument_samples_HACK_EXTENDED_LOOPS.append(HACK_EXTEND_LOOP_SAMPLE(instrument_samples[i])) - print('Instrument %02X has mix_rate %d Hz'%[i, instrument_samples[i].mix_rate]) + instrument_samples_HACK_EXTENDED_LOOPS.append(HACK_EXTEND_LOOP_SAMPLE(samp)) + # print('Instrument %02X has mix_rate %d Hz and %d samples'%[i, samp.mix_rate, len(samp.data)/2]) emit_signal('audio_inst_sample_loaded', i) + # samp.save_to_wav('output/instrument%02d(%dHz)(loop from %d to %d of %d).wav' % [i, samp.mix_rate, samp.loop_begin, samp.loop_end, len(samp.data)/2]) + + +# We start the texture with a bunch of same-size headers +# int32 sample_start // The true start, after the prepended 3 frames of silence +# uint16 sample_length // 3 frames after the true end, because of how we loop +# uint16 sample_loop_begin // 3 frames after the true loop point +# uint16 mixrate +var samples_tex: ImageTexture +const TEX_WIDTH := 2048 +const FILTER_PAD := 32 +func samples_to_texture(): + var num_samples := INST_NUM + SFX_NUM + var header_length := num_samples * 5 + + # Create header and unwrapped payload separately first + var header_buffer := StreamPeerBuffer.new() + var payload_buffer := StreamPeerBuffer.new() + + for sample in instrument_samples + sfx_samples: + var sample_data_start: int = header_length + (payload_buffer.get_position()/2) + FILTER_PAD # After the prepended silence, in texels (2bytes) + var loop_begin: int = sample.loop_begin + var loop_length: int = sample.loop_end - loop_begin + var nonlooping: bool = loop_length <= 0 + print('Processing sample, nonlooping=%s'%nonlooping) + + for i in FILTER_PAD: # Prepend frames of silence + payload_buffer.put_16(0) + payload_buffer.put_data(sample.data) # Copy entire S16LE audio data + + if nonlooping: + # Append trailing silence for filter safety + for i in FILTER_PAD*5: + payload_buffer.put_16(0) + # Make it loop the trailing silence + loop_begin += FILTER_PAD + loop_length = 1 + else: + # Append copies of the loop for filter safety + # var loop_data = sample.data.subarray(sample.loop_begin*2, -1) + # for i in ceil((FILTER_PAD*4)/loop_length): + # payload_buffer.put_data(loop_data) + + # Copy frame by frame in case the loop is shorter than padding frames + for i in FILTER_PAD*4: + var pos := payload_buffer.get_position() + payload_buffer.seek(pos - loop_length*2) + var frame := payload_buffer.get_16() + payload_buffer.seek(pos) + payload_buffer.put_16(frame) + header_buffer.put_32(sample_data_start) + header_buffer.put_u16(loop_begin + FILTER_PAD) + header_buffer.put_u16(loop_length) + header_buffer.put_u16(sample.mix_rate) + # Combine the unwrapped arrays + var data := header_buffer.data_array + payload_buffer.data_array + var datasamp := AudioStreamSample.new() + datasamp.data = data + datasamp.mix_rate = 32000 + datasamp.format = AudioStreamSample.FORMAT_16_BITS + # datasamp.save_to_wav('output/texture_inst_data.wav') + var needed_rows := (len(data)/2)/float(TEX_WIDTH) + var rows := int(pow(2, ceil(log(needed_rows) / log(2)))) + if rows > TEX_WIDTH: + print_debug('Sound Sample Texture rows have exceeded width: %d > %d'%[rows, TEX_WIDTH]) + # Now that the full texture size is known, pad our existing data with zeroes until the end + var final_data_size_bytes = rows * TEX_WIDTH * 2 + if final_data_size_bytes > len(data): + var end_padding := PoolByteArray() + end_padding.resize(final_data_size_bytes - len(data)) + end_padding.fill(0) + data = data + end_padding + + # data is complete, turn it into an ImageTexture for the shader to use + var samples_img = Image.new() + samples_img.create_from_data(TEX_WIDTH, rows, false, Image.FORMAT_LA8, data) + self.samples_tex = ImageTexture.new() + self.samples_tex.create_from_image(samples_img, 0) #Texture.FLAG_FILTER) var player := AudioStreamPlayer.new() # Make one for each channel, later -var HACK_EXTEND_LOOP_SAMPLE_playback: bool = true func play_sample(id: int, pitch_scale: float = 1.0): print('Playing inst sample #%02X' % id) player.pitch_scale = pitch_scale - if HACK_EXTEND_LOOP_SAMPLE_playback: - player.stream = instrument_samples_HACK_EXTENDED_LOOPS[id] - else: - player.stream = instrument_samples[id] + player.stream = instrument_samples_HACK_EXTENDED_LOOPS[id] player.play(PLAY_START/pitch_scale) func play_sfx(id: int): diff --git a/scripts/loaders/snes/music.gd b/scripts/loaders/snes/music.gd index 01ceaa3..6fca066 100644 --- a/scripts/loaders/snes/music.gd +++ b/scripts/loaders/snes/music.gd @@ -108,6 +108,13 @@ static func tempo_to_bpm(tempo_byte: int) -> float: return 1.0 return (tempo_byte / 255.0) * 60000000.0 / 216000.0 # VGMTrans uses /256.0 but I don't trust that +# bpm * ppqn = ppm (pulses per minute) +# ppm / 60 = pps (pulses per second) +# 1/pps = seconds per pulse +static func tempo_to_seconds_per_pulse(tempo_byte: int) -> float: + # 125 * TIMER0_FREQUENCY = 4500 + return 4500.0 / (1000000.0 * tempo_byte / 255.0) + static func get_int_array(size: int) -> PoolIntArray: var array := PoolIntArray() array.resize(size) diff --git a/shaders/audio_renderer.gdshader b/shaders/audio_renderer.gdshader index 7392a4e..36d5666 100644 --- a/shaders/audio_renderer.gdshader +++ b/shaders/audio_renderer.gdshader @@ -1,5 +1,9 @@ +// ============================================================= BOILERPLATE ============================================================= +// While most of the data we are working with is integral, GPU conversion overheads mean almost all of this will be floats. +// Unfortunately, this loses type-checking on [0.0, 1.0] vs [0,255] etc. so a lot of this will involve comments declaring ranges. shader_type canvas_item; render_mode blend_premul_alpha; +const int INT_TEX_SIZE = 4096; const float TEX_SIZE = 4096.0; const float UV_QUANTIZE = TEX_SIZE; // I feel like these magic numbers are a bit more intuitive in hex @@ -10,14 +14,33 @@ const float x8000 = float(0x8000); // 32768.0 const float xFF00 = float(0xFF00); // 65280.0 const float xFFFF = float(0xFFFF); // 65535.0 const float x10000 = float(0x10000); // 65536.0 +const float x00FF0000 = float(0x00FF0000); +const float xFF000000 = float(0xFF000000); const vec2 INT16_DOT_BE = vec2(xFF00, x00FF); const vec2 INT16_DOT_LE = vec2(x00FF, xFF00); -uniform sampler2D tex : hint_normal; +const vec4 INT32_DOT_LE = vec4(x00FF, xFF00, x00FF0000, xFF000000); + +float unpack_uint16(vec2 uint16) { + // Convert packed 2byte integer, sampled as two [0.0, 1.0] range floats, to the original int value [0, 65535] in float32 + return dot(uint16, INT16_DOT_LE); +} + +float unpack_uint32_to_float(vec4 uint32) { + // Convert packed 4byte integer, sampled as four [0.0, 1.0] range floats, to the original int value [0, 0xFFFFFFFF] in float32 + // NOTE: THIS WILL LOSE PRECISION ON NUMBERS ABOVE 24BIT SIGNIFICANCE + // I CAN'T EVEN GUARANTEE THE 0xFF000000 CONSTANT WILL SURVIVE ROUNDING + return dot(uint32, INT32_DOT_LE); +} + +int unpack_int32(vec4 int32) { + // Convert packed 4byte integer, sampled as four [0.0, 1.0] range floats, to the original int value + // return int(unpack_uint16(int32.xy)) + (int(unpack_uint16(int32.zw)) << 16); + return int(unpack_uint16(int32.xy)) + (int(unpack_uint16(int32.zw)) * 0x10000); +} float unpack_int16(vec2 int16) { - // Convert packed 2byte integer, sampled as two [0.0, 1.0] range floats, - // to the original int value [-32768, 32767] or [0, 65535] but in float32 + // Convert packed 2byte integer, sampled as two [0.0, 1.0] range floats, to the original int value [-32768, 32767] in float32 float unsigned = dot(int16, INT16_DOT_LE); return unsigned - (unsigned < x7FFF ? 0.0 : x10000); } @@ -37,7 +60,7 @@ vec2 pack_float_to_int16(float value) { return vec2(LSB, MSB); } -vec4 test_writeback(vec2 uv) { +vec4 test_writeback(sampler2D tex, vec2 uv) { // Test importing and exporting the samples, // and exporting a value derived from the UV vec4 output; @@ -48,10 +71,148 @@ vec4 test_writeback(vec2 uv) { return output; } + +// ============================================================= LOGIC ============================================================= +// We have around 200k frames across 35 instrument samples +// 35 instrument samples and 8 sfx samples = 43 samples +// 2048x128 texture maybe? at 2bytes per texel, that's 512KiB of VRAM +// We start the texture with a bunch of same-size headers +// int32 smp_start // The true start, after the prepended frames of silence +// uint16 loop_begin // padded past the true loop point for filtering +// uint16 loop_length +// uint16 mixrate +// +// To accomodate filtering, every sample must begin with 3 frames of silence, and end with 6 frames of the beginning of the loop. +// Looped playback will go from the first 3 of 6 frames at the end, to the third frame after the loop start point, to avoid filter bleeding. +// If a sample does not loop, it must have 6 frames of silence at the end, not including the subsequent next sample's 3 frames of silence prefix. +// As such, every sample will have an additional 9 frames, 3 before, 6 after. +// Additionally, every row of the texture must have 3 redundant frames on either side - i.e., we only sample from [3, 2045) on any given row. +// So the payload of a 2048-wide texture will be 2042 per row, excluding the initial header. +// So for 43 samples, a header of 43*6 = 258 texels starts the first row, +// after which the first sample's 3 frames of silence (3 texels of (0.0, 0.0), 6 bytes of 0x00) may begin. +// A 2048x128 texture would have a payload of 2042x128 = 261376 frames (texels) excluding header +// With the 258 texel header, which uses 3 texels of margin, 255 would be subtracted from the above payload, +// leaving 261121 texels for the sample data. + +const float HEADER_LENGTH_TEXELS = 5.0; +uniform sampler2D instrument_samples; +uniform vec2 instrument_samples_size = vec2(2048.0, 128.0); +const int INSTRUMENT_SAMPLES_WIDTH = 2048; +uniform float reference_note = 71.0; // [0, 255], possibly [0, 127] +uniform float output_mixrate = 32000.0; // SNES SPC output is 32kHz +float sinc(float x) { + x = abs(x) + 0.00000000000001; // Avoid division by zero + return min(sin(x)/x, 1.0); +} + +float get_pitch_scale(float note) { + return exp2((note - reference_note)/12.0); +} + +vec2 get_inst_texel(vec2 xy) { + return texture(instrument_samples, (xy+0.5)/instrument_samples_size).xw; +} + +float get_inst_texel_int16(int smp) { + int x = smp % INSTRUMENT_SAMPLES_WIDTH; + int y = smp / INSTRUMENT_SAMPLES_WIDTH; + return unpack_int16(texture(instrument_samples, (vec2(float(x), float(y)) + 0.5)/instrument_samples_size).xw); +} + +float get_instrument_sample(float instrument_index, float note, float t) { + float header_offset = instrument_index * HEADER_LENGTH_TEXELS; + int smp_start = unpack_int32(vec4(get_inst_texel(vec2(header_offset, 0.0)), get_inst_texel(vec2(header_offset + 1.0, 0.0)))); // The true start, after the prepended frames of silence + float smp_loop_begin = unpack_uint16(get_inst_texel(vec2(header_offset + 2.0, 0.0))); // padded past the true loop point for filter + float smp_loop_length = unpack_uint16(get_inst_texel(vec2(header_offset + 3.0, 0.0))); + float sample_mixrate = unpack_uint16(get_inst_texel(vec2(header_offset + 4.0, 0.0))); + // Calculate the point we want to sample in linear space + float mixrate = sample_mixrate * get_pitch_scale(note); + float smp_t = t * mixrate; + // If we're past the end of the sample, we need to wrap it back to within the loop range + float overshoot = max(smp_t - smp_loop_begin, 0.0); + smp_t -= floor(overshoot/smp_loop_length) * smp_loop_length; + // if (smp_t > smp_loop_begin) { + // // return 0.0; + // smp_t = mod(smp_t - smp_loop_begin, smp_loop_length) + smp_loop_begin; + // } + + int smp_window_start = smp_start + int(smp_t) - 6; + float smp_rel_filter_target = fract(smp_t) + 6.0; + float output = 0.0; + for (int i = 0; i < 12; i++) { + int smp_filter = smp_window_start + i; + float s = get_inst_texel_int16(smp_filter); + // TODO: determine proper value for this. Might be based on instrument base mixrate. + output += s * sinc((smp_rel_filter_target - float(i)) * 3.1); + } + return rescale_int16(output); + // int target_texel = int(smp_t) + smp_start; + // return rescale_int16(get_inst_texel_int16(target_texel)); +} + +const int NUM_CHANNELS = 8; +const int MAX_CHANNEL_NOTE_EVENTS = 2048; +const int NUM_CHANNEL_NOTE_PROBES = 11; // log2(MAX_CHANNEL_NOTE_EVENTS) +uniform sampler2D midi_events : hint_normal; +uniform vec2 midi_events_size = vec2(2048.0, 16.0); +vec4 get_midi_texel(float x, float y) { + return texture(midi_events, vec2(x, y)/midi_events_size).xyzw; +} +vec4 render_song(int smp) { + // Each output texel rendered is a stereo S16LE frame representing 1/32000 of a second + // 2048 is an established safe texture dimension so may as well go 2048 wide + + float t = float(smp)/output_mixrate; + vec2 downmixed_stereo = vec2(0.0); + + // Binary search the channels + for (int channel = 0; channel < NUM_CHANNELS; channel++) { + float row = float(channel * 4); + float event_idx = 0.0; + int smp_start; + for (int i = 0; i < NUM_CHANNEL_NOTE_PROBES; i++) { + float step_size = exp2(float(NUM_CHANNEL_NOTE_PROBES - i - 1)); + smp_start = int(unpack_int32(get_midi_texel(event_idx + step_size, row))); + event_idx += (smp >= smp_start) ? step_size : 0.0; + } + smp_start = int(unpack_int32(get_midi_texel(event_idx, row))); + int smp_end = int(unpack_int32(get_midi_texel(event_idx, row+1.0))); + vec4 note_event_supplement = get_midi_texel(event_idx, row+2.0); // left as [0.0, 1.0] + float instrument_idx = trunc(note_event_supplement.x * 255.0); + float pitch_idx = note_event_supplement.y * 255.0; + float velocity = note_event_supplement.z; + float pan = note_event_supplement.w; + vec4 adsr = get_midi_texel(event_idx, row+3.0); // left as [0.0, 1.0] + // ====================At some point I'll look back into packing floats==================== + // TBD = note_event_supplement.zw; - tremolo/vibrato/noise/pan_lfo/pitchbend/echo remain + // ====================At some point I'll look back into packing floats==================== + float attack = 1.0 + adsr.x*255.0; //65535.0 + 1.0; // TODO: work out effective resolution for this + int smp_attack = int(attack) * 2; // Max value is 131072 samples = 4.096 seconds + + // For now, just branch this + if (smp_start < smp) { // First sample may not start at zero! + int smp_overrun = smp - smp_end; // 256 samples of linear decay to 0 after note_off + smp_overrun = (smp_overrun < 0) ? 0 : smp_overrun; + if (smp_overrun < 256) { + float t_start = float(smp_start)/output_mixrate; + float attack_factor = min(float(smp - smp_start)/float(smp_attack), 1.0); + float release_factor = float(255-smp_overrun)/255.0; // 256 samples of linear decay to 0 after note_off + float samp = get_instrument_sample(instrument_idx, pitch_idx, t-t_start); + samp *= velocity * attack_factor * release_factor; + // TODO: proper decay and sustain, revisit release + downmixed_stereo += samp * vec2(1.0-pan, pan) * 0.5; // TODO: double it to maintain the mono level on each channel at center=0.5? + } + } + } + // Convert the stereo float audio to S16LE + return vec4(pack_float_to_int16(downmixed_stereo.x), pack_float_to_int16(downmixed_stereo.y)); +} + void fragment() { // GLES2 vec2 uv = vec2(UV.x, 1.0-UV.y); - uv = (trunc(uv*UV_QUANTIZE)+0.5)/UV_QUANTIZE; - - COLOR.xyzw = test_writeback(uv); + // uv = (trunc(uv*UV_QUANTIZE)+0.5)/UV_QUANTIZE; + // COLOR.xyzw = test_writeback(TEXTURE, uv); + ivec2 xy = ivec2(trunc(uv*TEX_SIZE)); + COLOR.xyzw = render_song(xy.x + (xy.y*INT_TEX_SIZE)); } diff --git a/test/audio_renderer.gd b/test/audio_renderer.gd index 8aadbe1..6727d82 100644 --- a/test/audio_renderer.gd +++ b/test/audio_renderer.gd @@ -1,22 +1,39 @@ extends Control +const INPUT_TEX_WIDTH := 2048 +const INPUT_FORMAT := Image.FORMAT_RGBA8 # Image.FORMAT_LA8 +const INPUT_BYTES_PER_TEXEL := 4 # 2 +const OUTPUT_BYTES_PER_TEXEL := 4 +const OUTPUT_WIDTH := 4096 +const QUAD_COLOR := PoolColorArray([Color.white, Color.white, Color.white, Color.white]) var viewport: Viewport -var render_queue: Array # of PoolByteArrays -var result_queue: Array # of PoolByteArrays -var current_image: Image +var render_queue: Array # of Images +var result_queue: Array # of [String, PoolByteArray] var current_tex: ImageTexture # Needed to prevent GC before draw -var waiting_for_viewport: bool +var waiting_for_viewport: Array var done_first_draw: bool func _ready() -> void: self.viewport = get_parent() self.render_queue = [] self.result_queue = [] - self.waiting_for_viewport = false + self.waiting_for_viewport = [] self.done_first_draw = false - self.current_image = Image.new() self.current_tex = ImageTexture.new() +func push_image(img: Image, uv_rows: int = 4096, desc: String = '') -> void: + self.render_queue.append([img, uv_rows, desc]) + +func push_bytes(data: PoolByteArray, uv_rows: int = 4096, desc: String = '') -> void: + # print(data.subarray(0, 15)) + var rows = int(pow(2, ceil(log((len(data)/INPUT_BYTES_PER_TEXEL) / INPUT_TEX_WIDTH)/log(2)))) + var target_length = rows * INPUT_BYTES_PER_TEXEL * INPUT_FORMAT + while len(data) < target_length: # This is inefficient, but this function should be called with pre-padded data anyway + data.append(0) + var image := Image.new() + image.create_from_data(INPUT_TEX_WIDTH, rows, false, INPUT_FORMAT, data) + self.render_queue.append([image, uv_rows, desc]) + func _process(_delta) -> void: update() @@ -35,25 +52,33 @@ func _draw() -> void: return # Draw the next ImageTexture - var data: PoolByteArray = self.render_queue.pop_front() - print(data.subarray(0, 15)) - self.current_image.create_from_data(4096, 4096, false, Image.FORMAT_LA8, data) - self.current_tex.create_from_image(self.current_image, Texture.FLAG_FILTER) - self.material.set_shader_param('tex', self.current_tex) - draw_texture(self.current_tex, Vector2.ZERO) - self.waiting_for_viewport = true # Grab the result next draw + var image_and_uv_rows_and_desc = self.render_queue.pop_front() + self.current_tex.create_from_image(image_and_uv_rows_and_desc[0], 0) + self.material.set_shader_param('midi_events', self.current_tex) + self.material.set_shader_param('midi_events_size', self.current_tex.get_size()) + var uv_rows: int = image_and_uv_rows_and_desc[1] + var uv_rows_inv: int = 4096 - uv_rows + var uv_v: float = uv_rows / float(OUTPUT_WIDTH) + var points := PoolVector2Array([Vector2(0, uv_rows_inv), Vector2(OUTPUT_WIDTH, uv_rows_inv), Vector2(OUTPUT_WIDTH, OUTPUT_WIDTH), Vector2(0, OUTPUT_WIDTH)]) + var uvs := PoolVector2Array([Vector2(0, 1-uv_v), Vector2(1, 1-uv_v), Vector2(1, 1), Vector2(0, 1)]) + draw_primitive(points, QUAD_COLOR, uvs, self.current_tex) + self.waiting_for_viewport = [uv_rows, image_and_uv_rows_and_desc[2]] # Grab the result next draw func get_result() -> void: + var result_rows: int = waiting_for_viewport[0] + var result_desc: String = waiting_for_viewport[1] var result_texture := self.viewport.get_texture() var result_image := result_texture.get_data() var result_bytes := result_image.get_data() + var result_byte_count := result_rows * OUTPUT_WIDTH * OUTPUT_BYTES_PER_TEXEL + result_bytes.resize(result_byte_count) - # Debugging: compare a sequence of all the possible 16bit integers - print_debug('result_image format is %d and has size'%result_image.get_format(), result_image.get_size(), result_bytes.subarray(0, 11)) - test_readback(result_bytes) + self.result_queue.append([result_desc, result_bytes]) + self.waiting_for_viewport = [] - self.result_queue.append(result_bytes) - self.waiting_for_viewport = false + # # Debugging: compare a sequence of all the possible 16bit integers + # print_debug('result_image format is %d and has size'%result_image.get_format(), result_image.get_size(), result_bytes.subarray(0, 11)) + # test_readback(result_bytes) func test_readback(result_bytes: PoolByteArray): # Debugging: compare a sequence of all the possible 16bit integers diff --git a/test/audio_system.gd b/test/audio_system.gd index 0a08d05..a582b69 100644 --- a/test/audio_system.gd +++ b/test/audio_system.gd @@ -2,8 +2,20 @@ extends Node2D #warning-ignore-all:return_value_discarded const MusicPlayer := preload('res://scripts/MusicPlayer.gd') var MusicLoader := preload('res://scripts/loaders/snes/music_ff5.gd').new() +onready var bgm_titles := Common.load_glyph_table('res://data/5/bgm_titles.txt') +onready var audio_renderer := $'%audio_renderer' +onready var audio_player := $audio_player +var prerendered_bgms := {} +var prerendered_bgm_start_and_end_loops := {} var inst_buttons = [] var sfx_buttons = [] +var initialized_instrument_texture := false +var queued_bgm_playback := '' + +const NUM_CHANNELS := 8 +var music_player = null +var inst_sample_map := {} +var bgm_tracksets := {} func _create_sfx_buttons(): var disable_btn := !SoundLoader.has_loaded_audio_samples @@ -48,11 +60,6 @@ func _enable_inst_button(id: int): for i in id+1: inst_buttons[i].disabled = false -const NUM_CHANNELS := 8 -var music_player = null -var inst_sample_map := {} -var bgm_tracksets := {} - func evaluate_bgm(id: int): var buffer: StreamPeerBuffer = RomLoader.snes_buffer.duplicate() var bgm_song_ptr: int = RomLoader.snes_data.bgm_song_pointers[id] & 0x3FFFFF @@ -77,51 +84,166 @@ func evaluate_bgm(id: int): tracks.append(MusicLoader.unroll_track(buffer.duplicate(), bgm_song_ptr, track_ptr, end_ptr, '%02d:%02d'%[id, i])) bgm_tracksets[id] = tracks -func play_bgm(id: int) -> void: - var inst_indices = RomLoader.snes_data.bgm_instrument_indices[id] - for i in 16: - var inst_idx: int = inst_indices[i]-1 - if inst_idx < 0: - self.inst_sample_map[i + 0x20] = null - else: - if SoundLoader.HACK_EXTEND_LOOP_SAMPLE_playback: - self.inst_sample_map[i + 0x20] = SoundLoader.instrument_samples_HACK_EXTENDED_LOOPS[inst_idx] +func play_bgm(id: int, live: bool) -> void: + self._stop_all() + if live: + var inst_indices = RomLoader.snes_data.bgm_instrument_indices[id] + for i in 16: + var inst_idx: int = inst_indices[i]-1 + if inst_idx < 0: + self.inst_sample_map[i + 0x20] = null else: - self.inst_sample_map[i + 0x20] = SoundLoader.instrument_samples[inst_idx] - if self.music_player: - remove_child(music_player) - self.music_player = MusicPlayer.new(bgm_tracksets[id], self.inst_sample_map) - add_child(self.music_player) - self.music_player.is_playing = true + self.inst_sample_map[i + 0x20] = SoundLoader.instrument_samples_HACK_EXTENDED_LOOPS[inst_idx] + if self.music_player: + remove_child(music_player) + self.music_player = MusicPlayer.new(bgm_tracksets[id], self.inst_sample_map) + add_child(self.music_player) + self.music_player.is_playing = true + else: + # Play prerendered + var bgm_key = 'BGM%02d'%id + if bgm_key in self.prerendered_bgms: + self.audio_player.stream = self.prerendered_bgms[bgm_key] + self.audio_player.play() + else: + self.queue_prerender_bgm(id) + self.queued_bgm_playback = bgm_key print('Playing BGM%02d' % id) -func _play_bgm() -> void: - self.play_bgm($sb_bgm.value) +func _play_bgm_live() -> void: + self.play_bgm($sb_bgm.value, true) + +func _play_bgm_prerendered() -> void: + self.play_bgm($sb_bgm.value, false) + func _create_bgm_playback() -> void: $sb_bgm.max_value = SoundLoader.BGM_NUM - $btn_bgm.connect('pressed', self, '_play_bgm') + $sb_bgm.connect('value_changed', self, '_update_bgm_label') + self._update_bgm_label() + $btn_bgm_live.connect('pressed', self, '_play_bgm_live') + $btn_bgm_prerendered.connect('pressed', self, '_play_bgm_prerendered') + $btn_render.connect('pressed', self, 'render_all_bgm') for i in SoundLoader.SFX_NUM: self.inst_sample_map[i] = SoundLoader.sfx_samples[i] for i in SoundLoader.BGM_NUM: evaluate_bgm(i) + func _stop_all() -> void: if self.music_player: self.music_player.queue_free() self.music_player = null SoundLoader.player.stop() + self.audio_player.stop() + + +func _update_bgm_label(id = 0) -> void: + if id < len(bgm_titles): + $lbl_bgm_title.text = bgm_titles[id] + else: + $lbl_bgm_title.text = '' -func _update_loop_hack_status(enabled: bool) -> void: - SoundLoader.HACK_EXTEND_LOOP_SAMPLE_playback = enabled # Called when the node enters the scene tree for the first time. func _ready() -> void: self._create_sfx_buttons() self._create_bgm_playback() $btn_stop.connect('pressed', self, '_stop_all') - $btn_hack_loop_extension.connect('toggled', self, '_update_loop_hack_status') - $btn_hack_loop_extension.text += ' (%dms)'%SoundLoader.HACK_EXTEND_LOOP_SAMPLE_EXTRA_MS for i in len(RomLoader.snes_data.bgm_song_pointers): var pointer = RomLoader.snes_data.bgm_song_pointers[i] - print('BGM 0x%02X (%02d) at 0x%06X' % [i, i, pointer]) + # print('BGM 0x%02X (%02d) at 0x%06X' % [i, i, pointer]) + + +var load_start_tick: int +func get_ms(restart: bool = false) -> int: + if restart or not self.load_start_tick: + self.load_start_tick = Time.get_ticks_msec() + return 0 + return Time.get_ticks_msec() - self.load_start_tick + + +func initialize_instrument_texture() -> void: + get_ms(true) + SoundLoader.samples_to_texture() + audio_renderer.material.set_shader_param('instrument_samples', SoundLoader.samples_tex) + audio_renderer.material.set_shader_param('instrument_samples_size', SoundLoader.samples_tex.get_size()) + self.initialized_instrument_texture = true + print('@%dms - Initialized instrument samples texture' % get_ms()) + + +func queue_prerender_bgm(bgm_id: int) -> void: + if not self.initialized_instrument_texture: + self.initialize_instrument_texture() + var mp = MusicPlayer.new(bgm_tracksets[bgm_id], self.inst_sample_map) + var data_and_target_time_and_loops = mp.render_channels(0, 540, RomLoader.snes_data.bgm_instrument_indices[bgm_id]) + var data = data_and_target_time_and_loops[0] + var target_time = data_and_target_time_and_loops[1] + var target_samples = target_time * 32000 + var target_rows = ceil(target_samples/4096.0) + var bgm_key := 'BGM%02d'%bgm_id + audio_renderer.push_bytes(data, target_rows, bgm_key) + self.prerendered_bgm_start_and_end_loops[bgm_key] = data_and_target_time_and_loops[2] + + + +func render_all_bgm(bgms_to_render: int = 64) -> void: + self.initialize_instrument_texture() + for bgm_id in bgms_to_render: + self.queue_prerender_bgm(bgm_id) + if bgm_id % 10 == 9 and bgm_id < bgms_to_render-1: + print('@%dms - Processed %d/%d bgm tracks' % [get_ms(), bgm_id+1, bgms_to_render]) + print('@%dms - Processed %d bgm tracks and sent to gpu for rendering' % [get_ms(), bgms_to_render]) + + +const save_prerendered_audio := false +func _get_prerendered_audio(): + audio_renderer.get_result() + var result = audio_renderer.result_queue.pop_back() + var desc = result[0] + var rendered_audio := AudioStreamSample.new() + rendered_audio.data = result[1] + rendered_audio.stereo = true + rendered_audio.mix_rate = 32000 + rendered_audio.format = AudioStreamSample.FORMAT_16_BITS + if prerendered_bgm_start_and_end_loops[desc][0] >= 0: + rendered_audio.loop_begin = int(round(prerendered_bgm_start_and_end_loops[desc][0])) + rendered_audio.loop_end = int(round(prerendered_bgm_start_and_end_loops[desc][1])) + rendered_audio.loop_mode = AudioStreamSample.LOOP_FORWARD + self.prerendered_bgms[desc] = rendered_audio + if save_prerendered_audio: + var error = rendered_audio.save_to_wav('output/rendered_%s.wav'%desc) + print('@%dms - Saved render of %s (error code %s)' % [get_ms(), desc, globals.ERROR_CODE_STRINGS[error]]) + else: + print('@%dms - Rendered %s without saving' % [get_ms(), desc]) + if self.queued_bgm_playback == desc: + self.audio_player.stream = rendered_audio + self.audio_player.play() + self.queued_bgm_playback = '' + + +func get_shader_test_pattern() -> PoolByteArray: + var midi_events_bytes := StreamPeerBuffer.new() + var midi_events_bytes2 := StreamPeerBuffer.new() + var midi_events_bytes3 := StreamPeerBuffer.new() + var midi_events_bytes4 := StreamPeerBuffer.new() + for i in 2048: + var t = i * 2.0 + midi_events_bytes.put_32(t*32000) # t_start + midi_events_bytes2.put_32((t+1.75)*32000) # t_end + midi_events_bytes3.put_u8(i%35) # instrument + midi_events_bytes3.put_u8(71) # pitch_idx + # midi_events_bytes.put_float((35 + (i%40))) # pitch_idx + midi_events_bytes3.put_u8(255) # velocity + midi_events_bytes3.put_u8(0) # pan + # midi_events_bytes3.put_u8(i%256) # pan + midi_events_bytes4.put_32(0) # ADSR + return midi_events_bytes.data_array + midi_events_bytes2.data_array + midi_events_bytes3.data_array + midi_events_bytes4.data_array + + +func _process(_delta): + update() + +func _draw() -> void: + if audio_renderer.waiting_for_viewport: + self._get_prerendered_audio() diff --git a/test/audio_system.tscn b/test/audio_system.tscn index c92fcd3..17836c1 100644 --- a/test/audio_system.tscn +++ b/test/audio_system.tscn @@ -1,15 +1,22 @@ [gd_scene load_steps=6 format=2] [ext_resource path="res://test/audio_system.gd" type="Script" id=1] -[ext_resource path="res://theme/menu_theme.tres" type="Theme" id=2] [ext_resource path="res://test/audio_renderer.gd" type="Script" id=3] [ext_resource path="res://shaders/audio_renderer.gdshader" type="Shader" id=4] +[sub_resource type="Curve" id=3] +_data = [ Vector2( 0, 0 ), 0.0, 2.46705, 0, 1, Vector2( 0.335329, 0.827273 ), 2.46705, -1.6562, 1, 1, Vector2( 0.631737, 0.336364 ), -1.6562, 1.80207, 1, 1, Vector2( 1, 1 ), 1.80207, 0.0, 1, 0 ] + [sub_resource type="ShaderMaterial" id=2] shader = ExtResource( 4 ) +shader_param/instrument_samples_size = Vector2( 2048, 128 ) +shader_param/reference_note = 71.0 +shader_param/output_mixrate = 32000.0 +shader_param/midi_events_size = Vector2( 2048, 16 ) [node name="audio_system" type="Node2D"] script = ExtResource( 1 ) +curve = SubResource( 3 ) [node name="viewport_audio_renderer" type="Viewport" parent="."] size = Vector2( 4096, 4096 ) @@ -36,31 +43,45 @@ margin_right = 198.0 margin_bottom = 176.0 [node name="sb_bgm" type="SpinBox" parent="."] -margin_top = 192.0 +margin_top = 188.0 margin_right = 38.0 -margin_bottom = 216.0 +margin_bottom = 212.0 rect_min_size = Vector2( 38, 0 ) +max_value = 69.0 align = 2 -[node name="btn_bgm" type="Button" parent="."] +[node name="btn_bgm_live" type="Button" parent="."] margin_left = 40.0 -margin_top = 192.0 -margin_right = 102.0 -margin_bottom = 216.0 -text = "Play BGM" +margin_top = 188.0 +margin_right = 126.0 +margin_bottom = 212.0 +text = "Play BGM live" + +[node name="btn_bgm_prerendered" type="Button" parent="."] +margin_left = 132.0 +margin_top = 188.0 +margin_right = 272.0 +margin_bottom = 212.0 +text = "Play BGM prerendered" + +[node name="btn_render" type="Button" parent="."] +margin_left = 278.0 +margin_top = 188.0 +margin_right = 374.0 +margin_bottom = 212.0 +text = "Render All BGM" [node name="btn_stop" type="Button" parent="."] margin_left = 320.0 -margin_top = 192.0 +margin_top = 216.0 margin_right = 374.0 -margin_bottom = 216.0 +margin_bottom = 240.0 text = "Stop All" -[node name="btn_hack_loop_extension" type="CheckBox" parent="."] -margin_left = 105.0 -margin_top = 192.0 -margin_right = 315.0 -margin_bottom = 216.0 -theme = ExtResource( 2 ) -pressed = true -text = "HACK: Loop Extension" +[node name="lbl_bgm_title" type="Label" parent="."] +margin_left = 2.0 +margin_top = 220.0 +margin_right = 42.0 +margin_bottom = 234.0 + +[node name="audio_player" type="AudioStreamPlayer" parent="."]