Compare commits

...

12 Commits

12 changed files with 1074 additions and 107 deletions

View File

@ -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

1 Label SNES PSX_file PSX_offset format Comment
14 sfx_brr_data 0x041E41 Use the below SPC pointers
15 bytelength_sfx_brr_pointers 0x041F4F u16 Used by the memcpy routine that copies the below data to the SPC (0x0020 = 32 bytes)
16 sfx_brr_pointers 0x041F51 8 of 2 of u16 SPC memory addresses not ROM. Start address followed by loop address.
17 sfx_adsrs 0x041F71 8 of u16 8 of 4 of u4
18 sfx_samplerates 0x041F83 8 of u16
19 sfx_data 0x041F95 Contains SPC pointers and tracks
20 bgm_song_pointers 0x043B97 72 of u24
21 bgm_instrument_brr_pointers 0x043C6F 35 of u24
22 bgm_instrument_loop_starts 0x043CD8 35 of u16
23 bgm_instrument_samplerates 0x043D1E 35 of u16
24 bgm_instrument_adsrs 0x043D64 35 of u16 35 of 4 of u4
25 bgm_instrument_indices 0x043DAA 72 of 16 of u16 length 0x900
26
27 worldmap_compressed_tilesets 0x070000 tilesets 0 up to 0x434

70
data/5/bgm_titles.txt Normal file
View File

@ -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]

View File

@ -1,8 +1,100 @@
[gd_resource type="AudioBusLayout" load_steps=2 format=2]
[gd_resource type="AudioBusLayout" load_steps=4 format=2]
[sub_resource type="AudioEffectCompressor" id=1]
[sub_resource type="AudioEffectCompressor" id=3]
resource_name = "Compressor"
[sub_resource type="AudioEffectPanner" id=1]
resource_name = "Panner"
[sub_resource type="AudioEffectDelay" id=2]
resource_name = "Delay"
[resource]
bus/0/effect/0/effect = SubResource( 1 )
bus/0/effect/0/effect = SubResource( 3 )
bus/0/effect/0/enabled = true
bus/1/name = "BGM"
bus/1/solo = false
bus/1/mute = false
bus/1/bypass_fx = false
bus/1/volume_db = -6.0
bus/1/send = "Master"
bus/2/name = "BGM0"
bus/2/solo = false
bus/2/mute = false
bus/2/bypass_fx = false
bus/2/volume_db = 0.0
bus/2/send = "BGM"
bus/2/effect/0/effect = SubResource( 1 )
bus/2/effect/0/enabled = true
bus/2/effect/1/effect = SubResource( 2 )
bus/2/effect/1/enabled = true
bus/3/name = "BGM1"
bus/3/solo = false
bus/3/mute = false
bus/3/bypass_fx = false
bus/3/volume_db = 0.0
bus/3/send = "BGM"
bus/3/effect/0/effect = SubResource( 1 )
bus/3/effect/0/enabled = true
bus/3/effect/1/effect = SubResource( 2 )
bus/3/effect/1/enabled = true
bus/4/name = "BGM2"
bus/4/solo = false
bus/4/mute = false
bus/4/bypass_fx = false
bus/4/volume_db = 0.0
bus/4/send = "BGM"
bus/4/effect/0/effect = SubResource( 1 )
bus/4/effect/0/enabled = true
bus/4/effect/1/effect = SubResource( 2 )
bus/4/effect/1/enabled = true
bus/5/name = "BGM3"
bus/5/solo = false
bus/5/mute = false
bus/5/bypass_fx = false
bus/5/volume_db = 0.0
bus/5/send = "BGM"
bus/5/effect/0/effect = SubResource( 1 )
bus/5/effect/0/enabled = true
bus/5/effect/1/effect = SubResource( 2 )
bus/5/effect/1/enabled = true
bus/6/name = "BGM4"
bus/6/solo = false
bus/6/mute = false
bus/6/bypass_fx = false
bus/6/volume_db = 0.0
bus/6/send = "BGM"
bus/6/effect/0/effect = SubResource( 1 )
bus/6/effect/0/enabled = true
bus/6/effect/1/effect = SubResource( 2 )
bus/6/effect/1/enabled = true
bus/7/name = "BGM5"
bus/7/solo = false
bus/7/mute = false
bus/7/bypass_fx = false
bus/7/volume_db = 0.0
bus/7/send = "BGM"
bus/7/effect/0/effect = SubResource( 1 )
bus/7/effect/0/enabled = true
bus/7/effect/1/effect = SubResource( 2 )
bus/7/effect/1/enabled = true
bus/8/name = "BGM6"
bus/8/solo = false
bus/8/mute = false
bus/8/bypass_fx = false
bus/8/volume_db = 0.0
bus/8/send = "BGM"
bus/8/effect/0/effect = SubResource( 1 )
bus/8/effect/0/enabled = true
bus/8/effect/1/effect = SubResource( 2 )
bus/8/effect/1/enabled = true
bus/9/name = "BGM7"
bus/9/solo = false
bus/9/mute = false
bus/9/bypass_fx = false
bus/9/volume_db = 0.0
bus/9/send = "BGM"
bus/9/effect/0/effect = SubResource( 1 )
bus/9/effect/0/enabled = true
bus/9/effect/1/effect = SubResource( 2 )
bus/9/effect/1/enabled = true

View File

@ -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

View File

@ -35,7 +35,7 @@ HTML5="*res://scripts/managers/HTML5.gd"
[debug]
settings/fps/force_fps=60
settings/fps/force_fps=120
gdscript/warnings/unused_variable=false
gdscript/warnings/integer_division=false

View File

@ -54,6 +54,7 @@ func _init(tracks: Array, instrument_map: Dictionary):
for i in num_tracks:
self.players.append(AudioStreamPlayer.new())
add_child(self.players[-1])
self.players[-1].bus = 'BGM%d' % i
self.channel_pointer.append(0)
self.channel_instrument_idx.append(0)
self.channel_next_pulse.append(0)
@ -83,6 +84,13 @@ func _init(tracks: Array, instrument_map: Dictionary):
self.channel_pitchmod_on.append(0)
self.channel_echo_on.append(0)
self.channel_echo_volume.append(0)
var panner: AudioEffectPanner = AudioServer.get_bus_effect(i+2, 0)
panner.set_pan(0.0)
var delay: AudioEffectDelay = AudioServer.get_bus_effect(i+2, 1)
delay.set_dry(1.0)
delay.set_tap1_active(false)
delay.set_tap2_active(false)
delay.set_feedback_active(false)
func play_channel(channel: int, time_offset: float = 0.0) -> int:
# Executes the track events until it hits a note/rest, in which case it returns the pulse count to the next action, or the end of the events, in which case it returns -1
@ -117,9 +125,11 @@ func play_channel(channel: int, time_offset: float = 0.0) -> int:
self.channel_velocity[channel] = event[2]
EventType.PAN: # TODO: implement slides
self.channel_pan[channel] = event[1]
AudioServer.get_bus_effect(channel+2, 0).set_pan(1.0 - event[1]/127.0)
EventType.PAN_SLIDE: # TODO: implement slides
var slide_duration: int = event[1]
self.channel_pan[channel] = event[2]
AudioServer.get_bus_effect(channel+2, 0).set_pan(1.0 - event[2]/127.0)
EventType.PITCH_SLIDE: # TODO: implement slides
var slide_duration: int = event[1]
var target_pitch: int = event[2] # Signed
@ -233,3 +243,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]]

View File

@ -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 # !!!
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@ -47,7 +56,7 @@ func unsigned16_to_signed(unsigned):
func get_reference_pitch_samplerate(tuning1: int, tuning2: int = 0) -> int:
# This is non-trivial and subject to change
var pitch_scale = tuning1/256.0 + tuning2/65536.0
var pitch_scale = tuning1/255.0 + tuning2/65535.0
if tuning1 < 0x80:
pitch_scale += 1.0
return int(pitch_scale * 36000)
@ -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):

View File

@ -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)

View File

@ -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));
}

View File

@ -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

View File

@ -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()

View File

@ -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="."]