Compare commits
12 Commits
master
...
audio_expe
Author | SHA1 | Date |
---|---|---|
Luke Hubmayer-Werner | a3231c1ba9 | |
Luke Hubmayer-Werner | 302eaac7ac | |
Luke Hubmayer-Werner | bfd093317e | |
Luke Hubmayer-Werner | e9b0db8277 | |
Luke Hubmayer-Werner | fbd3dd52c7 | |
Luke Hubmayer-Werner | e077c4e036 | |
Luke Hubmayer-Werner | ef970396f6 | |
Luke Hubmayer-Werner | 5577b6ec73 | |
Luke Hubmayer-Werner | b1cd7b9d7e | |
Luke Hubmayer-Werner | 6f0f6fa1a2 | |
Luke Hubmayer-Werner | 08f50779a9 | |
Luke Hubmayer-Werner | 89d244eb88 |
|
@ -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
|
||||
|
|
|
|
@ -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]
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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="."]
|
||||
|
|
Loading…
Reference in New Issue