extends Node signal audio_samples_loaded signal audio_inst_sample_loaded(id) signal audio_sfx_sample_loaded(id) var has_loaded_audio_samples := false const BGM_NUM := 70 const INST_NUM := 35 const SFX_NUM := 8 const PREPEND_MS := 20 # Prepend 20ms of silence to each sample for preplay purposes const PLAY_START := PREPEND_MS / 1000.0 const BYTES_PER_SAMPLE := 2 # 16bit samples # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # !!! Testing a workaround for a Godot 3.x AudioStreamSample playback: !!! # !!! Copy the looped samples one or more times to avoid the break in interpolation at buffer end. !!! # !!! 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 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 # !!! return output # !!! # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! var bgm_tracks = [] var instrument_samples = [] var instrument_samples_HACK_EXTENDED_LOOPS = [] var sfx_samples = [] func read_rom_address(buffer: StreamPeerBuffer) -> int: # Read a 3-byte little-endian address and wrap the bank to ROM space return buffer.get_u16() + ((buffer.get_u8() & 0x3F) << 16) const MAX_15B = 1 << 15 const MAX_16B = 1 << 16 func unsigned16_to_signed(unsigned): return (unsigned + MAX_15B) % MAX_16B - MAX_15B func get_reference_pitch_samplerate(tuning1: int, tuning2: int = 0) -> int: # This is non-trivial and subject to change var pitch_scale = tuning1/255.0 + tuning2/65535.0 if tuning1 < 0x80: pitch_scale += 1.0 return int(pitch_scale * 36000) # return (unsigned16_to_signed(pitch) + 0x8000) * 32000/4096 func process_sample(mantissa: int, exponent: int) -> int: # For filter arithmetic the samples need to be in signed form. # Sign-extend if mantissa >= 8: mantissa |= 0xFFF0 if exponent > 12: exponent = 12 var unsigned = (mantissa << exponent) & 0xFFFF return unsigned16_to_signed(unsigned) func clamp_short(i: int) -> int: if i < -0x8000: return -0x8000 if i > 0x7FFF: return 0x7FFF return i func make_sample(buffer: StreamPeerBuffer, size: int, sample_rate: int) -> AudioStreamSample: var audio := AudioStreamSample.new() audio.mix_rate = sample_rate audio.stereo = false audio.set_format(AudioStreamSample.FORMAT_16_BITS) if (size % 9) != 0: print_debug('Oh no! An instrument sample has an invalid size of %d! at $%06X' % [size, buffer.get_position()-2]) 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 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 end := bool(header_byte & 0x01) for sample in 8: var b := buffer.get_u8() samples.append(process_sample(b >> 4, exponent)) samples.append(process_sample(b & 0x0F, exponent)) # Apply filter var l := len(samples) match filter: 1: for j in range(l-16, l): samples[j] = clamp_short(samples[j] + (samples[j-1]*15)/16) 2: for j in range(l-16, l): samples[j] = clamp_short(samples[j] + (samples[j-1]*61)/32 - (samples[j-2]*15)/16) 3: for j in range(l-16, l): samples[j] = clamp_short(samples[j] + (samples[j-1]*115)/64 - (samples[j-2]*13)/16) 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) # Pack 16bit samples to 8bit array for b in samples: audio_data.append(b & 0xFF) audio_data.append(b >> 8) audio.data = audio_data 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 # 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 # const SFX_BRR_START := 0x041E3F + 2 # First two bytes are the length of the block, 0x010E = 270 bytes = 16 BRR packets = 480 samples func load_sfx_samples_data(snes_data: Dictionary, buffer: StreamPeerBuffer): var brr_spc_addrs = [] var brr_spc_loop_addrs = [] for two_of_u16 in snes_data.sfx_brr_pointers: brr_spc_addrs.append(two_of_u16[0]) brr_spc_loop_addrs.append(two_of_u16[1]) var brr_spc_start = Common.SNES_PSX_addresses.sfx_brr_data.SNES - brr_spc_addrs[0] # Refactor this later for i in SFX_NUM: 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_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) # print('size of %d samples' % sfx_samples[i].data.size()) # Called when the node enters the scene tree for the first time. 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)) # 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]) emit_signal('audio_inst_sample_loaded', i) 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.play(PLAY_START/pitch_scale) func play_sfx(id: int): print('Playing sfx sample #%02X' % id) player.pitch_scale = 1.0 player.stream = sfx_samples[id] player.play(PLAY_START) func parse_rom(snes_data: Dictionary, buffer: StreamPeerBuffer): load_samples(snes_data, buffer) #load_bgms(buffer) has_loaded_audio_samples = true emit_signal('audio_samples_loaded') func _ready() -> void: add_child(player)