278 lines
13 KiB
GDScript
278 lines
13 KiB
GDScript
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: # !!!
|
|
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) # !!!
|
|
output.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 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 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
|
|
# Remove first two zero samples
|
|
samples.remove(0)
|
|
samples.remove(0)
|
|
# Pack 16bit samples to 8bit array
|
|
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 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) # 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
|
|
|
|
# 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 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 # 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:
|
|
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(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
|
|
func play_sample(id: int, pitch_scale: float = 1.0):
|
|
print('Playing inst sample #%02X' % id)
|
|
player.pitch_scale = pitch_scale
|
|
player.stream = instrument_samples_HACK_EXTENDED_LOOPS[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)
|