Browse Source

update

master
acheney 7 months ago
commit
5ec3cb9454
  1. 875
      boot.pixi
  2. 1037
      demo.pixi
  3. 109
      ffmpeg_video_export.pixi
  4. BIN
      media/artist.png
  5. BIN
      media/background.png
  6. BIN
      media/cover.png
  7. BIN
      media/logo.png
  8. BIN
      media/song.mp3
  9. 391
      mjpeg.pixi
  10. 111
      riff.pixi

875
boot.pixi

@ -0,0 +1,875 @@ @@ -0,0 +1,875 @@
// pixilang-based music visualizer
// by acheney (with help from nightradio, silent broadcast, and others)
// under mit license
include "demo.pixi"
// - CONFIG BEGIN -
// -- DEMO LIB CONFIG --
// demo width in pixels
demo_xsize = 1920
// demo height in pixels
demo_ysize = 1080
// demo use opengl
demo_opengl = 1
// demo video export filename, comment it out if you don't want to export
//demo_video_export = "video.avi"
// demo video export fps (frames per second)
demo_video_export_fps = 60
// demo video export quality (0 - 100)
demo_video_export_q = 100
// -- FILE CONFIG --
// note: all of these files should be located in the "media" directory of this program
// the filename of the audio file
audio_filename = "song.mp3"
// the filename of the artist image
artist_filename = "artist.png"
// the filename of the logo image
logo_filename = "logo.png"
// the filename of the cover image
cover_filename = "cover.png"
// the filename of the background image (set this to be the same as the cover image if you don't want a separate background
background_filename = "background.png"
// -- TEXT CONFIG --
// text (song title, song artist, song release title)
text_title = "dead girl"
text_artist = "DREADNOUGHT"
text_release = "DREADNOUGHT"
// text distance scaling (0 - 1)
text_scale = 0.25
// text color
text_color = #FFFFFF
// -- SPECTRUM/VISUALIZER CONFIG --
// goertzel size
vis_size = 2048
// visualizer color
vis_color = #FFFFFF
// linear visualizer (only enabled when 1, logarithmic when 0)
vis_linear = 1
// visualizer linear interpolation (only enabled when 1, bars when 0)
vis_int = 0
// visualizer number of bars
vis_bars = 100
// visualizer bar width factor ( 0 - 1 )
vis_bar_width = 0.5
// linear visualizer minimum frequency (hz)
vis_min_freq = 20
// linear visualizer maximum frequency (hz)
vis_max_freq = 3000
// logarithmic visualizer minimum midi pitch
vis_min_pitch = 15
// logarithmic visualizer maximum midi pitch
vis_max_pitch = 102
// visualizer smoothing factor ( >= 1 )
vis_smooth = 1.5
// visualizer vertical scale
vis_scale = 50
// -- TIME CONFIG --
// time for fade to black
all_exit = 6000
// time for logo + artist + blur transitions
all_enter = 6000
// -- LOGO CONFIG --
// logo size scale ( % of image size )
logo_scale = 0.25
// logo size scale ( persistent bottom-right corner ) ( % of demo_xsize )
logo_scale_persistent = 0.075
// -- ARTIST CONFIG --
// artist size scale ( % of image size )
artist_scale = 0.25
// -- COVER CONFIG --
//cover scale ( persistent top-right corner ) ( % of demo_xsize )
cover_scale = 0.075
// -- BACKGROUND CONFIG --
// background gaussian blur radius
background_blur_rad = 20
// background brightness scale ( 0 - 1 )
background_brightness = 0.8
// background flash speed ( 0 - 1 )
background_flash_speed = 0.75
// background shake amplitude ( affects both amp and the zoom of the background )
background_shake_amp = 2
// background shake speed ( the lower the value, the faster it is )
background_shake_speed = 0.9
// -- BEGINNING FLASH CONFIG --
// speed of the flash at the beginning in seconds
beginning_flash_speed = 0.05
// fade out of the flash at the beginning in seconds
beginning_flash_fade = 2
// exponential curve of the flash at the beginning ( >=0 )
beginning_flash_curve = 5
// - END CONFIG -
media = "media/"
audio_filename_internal = ""
artist_filename_internal = ""
logo_filename_internal = ""
cover_filename_internal = ""
background_filename_internal = ""
strcat( audio_filename_internal, media )
strcat( audio_filename_internal, audio_filename )
strcat( artist_filename_internal, media )
strcat( artist_filename_internal, artist_filename )
strcat( logo_filename_internal, media )
strcat( logo_filename_internal, logo_filename )
strcat( cover_filename_internal, media )
strcat( cover_filename_internal, cover_filename )
strcat( background_filename_internal, media )
strcat( background_filename_internal, background_filename )
demo_load_wav( audio_filename_internal )
cover = load( cover_filename_internal )
logo = load( logo_filename_internal )
artist = load( artist_filename_internal )
background = load( background_filename_internal )
set_flags( background, CFLAG_INTERP )
set_flags( cover, CFLAG_INTERP )
demo_length = 0
goertzel_values = new( vis_bars, 1, FLOAT )
cover_xsize = get_xsize(cover)
cover_ysize = get_ysize(cover)
cover_size = cover_xsize * cover_ysize
cover_src = clone( cover )
background_xsize = get_xsize( background )
background_ysize = get_ysize( background )
background_src = clone( background )
logo_xsize = get_xsize(logo)
logo_ysize = get_ysize(logo)
corner_x = -demo_xsize div 2
corner_y = -demo_ysize div 2
logo_xsize = get_xsize(logo)
logo_ysize = get_ysize(logo)
artist_xsize = get_xsize(artist)
artist_ysize = get_ysize(artist)
demo_init()
custom_init()
demo_add_scene( 0, 0, 0, view )
demo_add_scene( 0, 0, demo_length, DEMO_STOP )
demo_play()
custom_deinit()
demo_deinit()
fn view( $t1, $t2, $len )
{
clear( #000000 )
bg_render( $t1 )
vis_render( $t1 )
cover_render( $t1 )
logo_render( $t1 )
text_render( $t1 )
screen_blur( $t1 )
logos_render( $t1 )
beginning_flash_render( $t1 )
black( $t1 )
transp( 255 )
}
fn vis_render( $t1 )
{
transp( 255 )
if ( vis_linear ) {
if ( vis_int ) {
freq_range = vis_max_freq - vis_min_freq
freq_unit = freq_range / vis_bars
bar_space = demo_xsize / vis_bars
i = 0 while i < vis_bars {
i_freq = ( freq_unit * i ) + vis_min_freq
k = ( 0.5 + ( ( vis_size * i_freq ) / demo_sound_rate ) )
real_w = 2 * cos( 2 * M_PI * k / vis_size )
imag_w = sin( 2 * M_PI * k / vis_size )
d1 = 0
d2 = 0
j = 0 while j < vis_size {
j = j + 1
while 1 {
if demo_sound_type == FLOAT32 {
// audio is 32-bit float
v = demo_sound_pcm_stream[ ( demo_sound_pcm_stream_ptr + j ) * demo_sound_channels ] / pow( 2, 8 )
break
}
if demo_sound_type == INT16 {
// audio is 16-bit int
v = demo_sound_pcm_stream[ ( demo_sound_pcm_stream_ptr + j ) * demo_sound_channels ] / pow( 2, 23 )
break
}
}
win = sin( M_PI * j / vis_size )
x = v * win
y = ( x * vis_size ) + real_w * d1 - d2
d2 = d1
d1 = y
}
result_r = 0.5 * real_w * d1 - d2
result_i = imag_w * d1
amp = result_r * result_r + result_i * result_i
amp = sqrt( amp )
old_amp = goertzel_values[ i ]
goertzel_values[ i ] = old_amp + ( amp - old_amp ) * ( 1 / vis_smooth )
amp_scaled = ( goertzel_values[ i ] / 500 ) * vis_scale
goertzel_values[ i ] = amp_scaled
i = i + 1
}
i = 0 while i < demo_xsize {
bin = ( i / demo_xsize ) * vis_bars
idx = floor( bin )
frac = bin - idx
amp = ( 1 - frac ) * goertzel_values[ idx ] + frac * goertzel_values[ idx + 1 ]
i_adjusted = i - ( demo_xsize / 2 )
line( i_adjusted, 0, i_adjusted, amp, vis_color )
line( i_adjusted, 0, i_adjusted, -amp, vis_color )
i = i + 1
}
} else {
freq_range = vis_max_freq - vis_min_freq
freq_unit = freq_range / vis_bars
bar_space = demo_xsize / vis_bars
i = 0 while i < vis_bars {
i_freq = ( freq_unit * i ) + vis_min_freq
k = ( 0.5 + ( ( vis_size * i_freq ) / demo_sound_rate ) )
real_w = 2 * cos( 2 * M_PI * k / vis_size )
imag_w = sin( 2 * M_PI * k / vis_size )
d1 = 0
d2 = 0
j = 0 while j < vis_size {
j = j + 1
while 1 {
if demo_sound_type == FLOAT32 {
// audio is 32-bit float
v = demo_sound_pcm_stream[ ( demo_sound_pcm_stream_ptr + j ) * demo_sound_channels ] / pow( 2, 8 )
break
}
if demo_sound_type == INT16 {
// audio is 16-bit int
v = demo_sound_pcm_stream[ ( demo_sound_pcm_stream_ptr + j ) * demo_sound_channels ] / pow( 2, 23 )
break
}
}
win = sin( M_PI * j / vis_size )
x = v * win
y = ( x * vis_size ) + real_w * d1 - d2
d2 = d1
d1 = y
}
result_r = 0.5 * real_w * d1 - d2
result_i = imag_w * d1
amp = result_r * result_r + result_i * result_i
amp = sqrt( amp )
old_amp = goertzel_values[ i ]
goertzel_values[ i ] = old_amp + ( amp - old_amp ) * ( 1 / vis_smooth )
amp_scaled = ( goertzel_values[ i ] / 500 ) * vis_scale
fbox( ( -demo_xsize / 2 ) + ( bar_space * i ), -amp_scaled, bar_space * vis_bar_width, amp_scaled, vis_color )
fbox( ( -demo_xsize / 2 ) + ( bar_space * i ), 0, bar_space * vis_bar_width, amp_scaled, vis_color )
i = i + 1
}
}
} else {
if (vis_int) {
pitch_range = vis_max_pitch - vis_min_pitch
pitch_unit = pitch_range / vis_bars
bar_space = demo_xsize / vis_bars
i = 0 while i < vis_bars {
i_pitch = ( pitch_unit * i ) + vis_min_pitch
i_freq = pow( 2, i_pitch / 12) * 8.175799
k = ( 0.5 + ( ( vis_size * i_freq ) / demo_sound_rate ) )
real_w = 2 * cos( 2 * M_PI * k / vis_size )
imag_w = sin( 2 * M_PI * k / vis_size )
d1 = 0
d2 = 0
j = 0 while j < vis_size {
j = j + 1
while 1 {
if demo_sound_type == FLOAT32 {
// audio is 32-bit float
v = demo_sound_pcm_stream[ ( demo_sound_pcm_stream_ptr + j ) * demo_sound_channels ] / pow( 2, 8 )
break
}
if demo_sound_type == INT16 {
// audio is 16-bit int
v = demo_sound_pcm_stream[ ( demo_sound_pcm_stream_ptr + j ) * demo_sound_channels ] / pow( 2, 23 )
break
}
}
win = sin( M_PI * j / vis_size )
x = v * win
y = ( x * vis_size ) + real_w * d1 - d2
d2 = d1
d1 = y
}
result_r = 0.5 * real_w * d1 - d2
result_i = imag_w * d1
amp = result_r * result_r + result_i * result_i
amp = sqrt( amp )
old_amp = goertzel_values[ i ]
goertzel_values[ i ] = old_amp + ( amp - old_amp ) * ( 1 / vis_smooth )
amp_scaled = ( goertzel_values[ i ] / 500 ) * vis_scale
goertzel_values[ i ] = amp_scaled
i = i + 1
}
i = 0 while i < demo_xsize {
bin = ( i / demo_xsize ) * vis_bars
idx = floor( bin )
frac = bin - idx
amp = ( 1 - frac ) * goertzel_values[ idx ] + frac * goertzel_values[ idx + 1 ]
i_adjusted = i - ( demo_xsize / 2 )
line( i_adjusted, 0, i_adjusted, amp, vis_color )
line( i_adjusted, 0, i_adjusted, -amp, vis_color )
i = i + 1
}
} else {
pitch_range = vis_max_pitch - vis_min_pitch
pitch_unit = pitch_range / vis_bars
bar_space = demo_xsize / vis_bars
i = 0 while i < vis_bars {
i_pitch = ( pitch_unit * i ) + vis_min_pitch
i_freq = pow( 2, i_pitch / 12) * 8.175799
k = ( 0.5 + ( ( vis_size * i_freq ) / demo_sound_rate ) )
real_w = 2 * cos( 2 * M_PI * k / vis_size )
imag_w = sin( 2 * M_PI * k / vis_size )
d1 = 0
d2 = 0
j = 0 while j < vis_size {
j = j + 1
while 1 {
if demo_sound_type == FLOAT32 {
// audio is 32-bit float
v = demo_sound_pcm_stream[ ( demo_sound_pcm_stream_ptr + j ) * demo_sound_channels ] / pow( 2, 8 )
break
}
if demo_sound_type == INT16 {
// audio is 16-bit int
v = demo_sound_pcm_stream[ ( demo_sound_pcm_stream_ptr + j ) * demo_sound_channels ] / pow( 2, 23 )
break
}
}
win = sin( M_PI * j / vis_size )
x = v * win
y = ( x * vis_size ) + real_w * d1 - d2
d2 = d1
d1 = y
}
result_r = 0.5 * real_w * d1 - d2
result_i = imag_w * d1
amp = result_r * result_r + result_i * result_i
amp = sqrt( amp )
old_amp = goertzel_values[ i ]
goertzel_values[ i ] = old_amp + ( amp - old_amp ) * ( 1 / vis_smooth )
amp_scaled = ( goertzel_values[ i ] / 500 ) * vis_scale
fbox( ( -demo_xsize / 2 ) + ( bar_space * i ), -amp_scaled, bar_space * vis_bar_width, amp_scaled, vis_color )
fbox( ( -demo_xsize / 2 ) + ( bar_space * i ), 0, bar_space * vis_bar_width, amp_scaled, vis_color )
i = i + 1
}
}
}
transp( 255 )
}
fn bg_render($t1) {
transp( 255 )
base = 255 * background_brightness
sample = ( abs( demo_sound_pcm_stream[ demo_sound_pcm_stream_ptr * demo_sound_channels ] ) + 0.5 )
old_flash = flash
flash = base * sample
flash = old_flash * ( 1 - background_flash_speed ) + flash * background_flash_speed
shake_x_func = sin( 0.6 * ( $t1 / ( 1000 * background_shake_speed ) ) ) + sin( 2.65 * ( $t1 / ( 1000 * background_shake_speed ) ) ) + sin( 3.7 * ( $t1 / ( 1000 * background_shake_speed ) ) ) * background_shake_amp
shake_y_func = sin( 0.4 * ( $t1 / ( 1000 * background_shake_speed ) ) ) + sin( 1.45 * ( $t1 / ( 1000 * background_shake_speed ) ) ) + sin( 4.5 * ( $t1 / ( 1000 * background_shake_speed ) ) ) * background_shake_amp
pixi( background, shake_x_func , shake_y_func , get_color( flash, flash, flash ), ( ( demo_xsize / background_xsize ) ) * ( 1 + ( background_shake_amp / 30 ) ) , ( ( demo_xsize / background_xsize ) ) * ( 1 + ( background_shake_amp / 30 ) ) )
transp( 255 )
}
fn logos_render($t1) {
transp( 255 )
y_pos = smooth_staircase( ( ( $t1 / ( all_enter / 3 ) ) - 1 ), 1, 15, 1, -0.5 ) * ( all_enter / 3 )
pixi(artist, 0, -y_pos, WHITE, artist_scale, artist_scale)
pixi(logo, 0, -y_pos + ( all_enter / 3 ), WHITE, logo_scale, logo_scale)
transp( 255 )
}
fn logo_render($t1) {
transp( 255 )
rel_time = 1 - clip( max( $t1 - ( all_enter + 2000 ) , 0 ) / 1000 )
enter_func = ( pow( 50, rel_time ) - 1 ) / ( 50 - 1 )
enter_func_adjusted = ( 1 - enter_func ) * ( demo_ysize / 2 )
if (logo_xsize > logo_ysize) {
logo_adjust_factor = demo_xsize / logo_ysize
x_pos = corner_x - ( ( logo_ysize * logo_adjust_factor * logo_scale_persistent ) / 2 ) - demo_xsize / 50
y_pos = corner_y - ( ( logo_ysize * logo_adjust_factor *logo_scale_persistent ) / 2 ) + demo_xsize / 50 + ( ( demo_ysize / 2 ) - enter_func_adjusted )
x_scale = logo_adjust_factor * logo_scale_persistent
y_scale = logo_adjust_factor * logo_scale_persistent
pixi( logo, x_pos , y_pos, WHITE, x_scale, y_scale, ( ( logo_xsize - logo_ysize ) / 2 ), 0, logo_ysize, logo_ysize )
} else {
logo_adjust_factor = demo_xsize / logo_xsize
x_pos = -corner_x - ( ( logo_xsize * logo_adjust_factor * logo_scale_persistent ) / 2 ) - demo_xsize / 50
y_pos = -corner_y - ( ( logo_xsize * logo_adjust_factor * logo_scale_persistent ) / 2 ) - demo_xsize / 50 + ( ( demo_ysize / 2 ) - enter_func_adjusted )
x_scale = logo_adjust_factor * logo_scale_persistent
y_scale = logo_adjust_factor * logo_scale_persistent
pixi( logo, x_pos , y_pos, WHITE, x_scale, y_scale, ( ( logo_ysize - logo_xsize ) / 2 ), 0, logo_xsize, logo_xsize )
}
transp( 255 )
}
fn cover_render( $t1 ) {
transp( 255 )
rel_time = 1 - clip( max( $t1 - ( all_enter + 1000 ) , 0 ) / 1000 )
enter_func = ( pow( 50, rel_time ) - 1 ) / ( 50 - 1 )
enter_func_adjusted = ( 1 - enter_func ) * ( demo_ysize / 2 )
if (cover_xsize > cover_ysize) {
cover_adjust_factor = demo_xsize / cover_ysize
x_pos = -corner_x - ( ( cover_ysize * cover_adjust_factor * cover_scale ) / 2 ) - demo_xsize / 50
y_pos = corner_y + ( ( cover_ysize * cover_adjust_factor * cover_scale ) / 2 ) + demo_xsize / 50 - ( ( demo_ysize / 2 ) - enter_func_adjusted )
x_scale = cover_adjust_factor * cover_scale
y_scale = cover_adjust_factor * cover_scale
pixi( cover_src, x_pos , y_pos, WHITE, x_scale, y_scale, ( ( cover_xsize - cover_ysize ) / 2 ), 0, cover_ysize, cover_ysize )
} else {
cover_adjust_factor = demo_xsize / cover_xsize
x_pos = -corner_x - ( ( cover_xsize * cover_adjust_factor * cover_scale ) / 2 ) - demo_xsize / 50
y_pos = corner_y + ( ( cover_xsize * cover_adjust_factor * cover_scale ) / 2 ) + demo_xsize / 50 - ( ( demo_ysize / 2 ) - enter_func_adjusted )
x_scale = cover_adjust_factor * cover_scale
y_scale = cover_adjust_factor * cover_scale
pixi( cover_src, x_pos , y_pos, WHITE, x_scale, y_scale, ( ( cover_ysize - cover_xsize ) / 2 ), 0, cover_xsize, cover_xsize )
}
transp( 255 )
}
fn black( $t1 ) {
transp( 255 )
exit_point = demo_length - all_exit
rel_time = max($t1, exit_point) - exit_point
speed = 255 / (all_exit)
trans = min(255 - (rel_time * speed), 255)
transp(255 - trans)
fbox(corner_x, corner_y, demo_xsize, demo_ysize, BLACK)
transp( 255 )
}
fn text_render($t1) {
transp( 255 )
rel_time = 1 - clip( max( $t1 - ( all_enter + 1000 ) , 0 ) / 1000 )
enter_func = ( pow( 50, rel_time ) - 1 ) / ( 50 - 1 )
enter_func_adjusted = ( 1 - enter_func ) * ( demo_ysize / 2 )
if (cover_xsize > cover_ysize) {
cover_adjust_factor = demo_xsize / cover_ysize
x_pos = -corner_x - ( ( cover_ysize * cover_adjust_factor * cover_scale ) ) - demo_xsize / 25
y_pos = corner_y + ( ( cover_ysize * cover_adjust_factor * cover_scale ) / 2 ) + demo_xsize / 50 - ( ( demo_ysize / 2 ) - enter_func_adjusted )
x_scale = cover_adjust_factor * cover_scale
y_scale = cover_adjust_factor * cover_scale
print( text_title, x_pos , y_pos - ( ( ( cover_ysize * cover_adjust_factor * cover_scale ) / 2 ) * text_scale ) , text_color , TOP | RIGHT )
print( text_artist, x_pos , y_pos , text_color , TOP | RIGHT )
print( text_release, x_pos , y_pos + ( ( ( cover_ysize * cover_adjust_factor * cover_scale ) / 2 ) * text_scale ) , text_color , TOP | RIGHT )
} else {
cover_adjust_factor = demo_xsize / cover_xsize
x_pos = -corner_x - ( ( cover_xsize * cover_adjust_factor * cover_scale ) ) - demo_xsize / 25
y_pos = corner_y + ( ( cover_xsize * cover_adjust_factor * cover_scale ) / 2 ) + demo_xsize / 50 - ( ( demo_ysize / 2 ) - enter_func_adjusted )
x_scale = cover_adjust_factor * cover_scale
y_scale = cover_adjust_factor * cover_scale
print( text_title, x_pos , y_pos - ( ( ( cover_ysize * cover_adjust_factor * cover_scale ) / 2 ) * text_scale ) , text_color , TOP | RIGHT )
print( text_artist, x_pos , y_pos , text_color , TOP | RIGHT )
print( text_release, x_pos , y_pos + ( ( ( cover_ysize * cover_adjust_factor * cover_scale ) / 2 ) * text_scale ) , text_color , TOP | RIGHT )
}
transp( 255 )
}
fn screen_blur( $t1 ) {
if (demo_opengl) {
$prev_screen = get_screen()
if temp_img <= 0 { temp_img = new( demo_xsize, demo_ysize, PIXEL ) } else { resize( temp_img, demo_xsize, demo_ysize ) }
copy( temp_img, $prev_screen )
set_screen( temp_img )
effector( EFF_VBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_HBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_VBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_HBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_VBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_HBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
set_screen( $prev_screen )
update_gl_data( temp_img )
pixi( temp_img )
} else {
effector( EFF_VBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_HBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_VBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_HBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_VBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
effector( EFF_HBLUR, ( 1 - ( clip( ( $t1 - all_enter ) / 1000 ) ) ) * 20 , WHITE, corner_x, corner_y, demo_xsize, demo_ysize )
}
}
fn beginning_flash_render( $t1 ) {
transp_time = max( 0 , $t1 - ( 1000 * beginning_flash_speed ) )
transp_value = 255 - ( 255 * ( transp_time / ( 1000 * beginning_flash_fade ) ) )
transp_value_curved = pow( transp_value / 255 , beginning_flash_curve ) * 255
transp( transp_value_curved )
c = 255 * ( $t1 / ( 1000 * beginning_flash_speed ) )
fbox(corner_x, corner_y, demo_xsize, demo_ysize, get_color( c , c , c ))
transp( 255 )
}
fn custom_init()
{
gauss( background_src, background, background_blur_rad )
demo_length = (demo_sound_len / demo_sound_rate) * 1000
}
fn custom_deinit()
{
remove( goertzel_values )
}
fn max($a, $b) {
if ($a >= $b) {
ret($a)
} else {
ret($b)
}
}
fn min($a, $b) {
if ($a < $b) {
ret($a)
} else {
ret($b)
}
}
fn clip( $a ) {
if ($a < 0) { ret( 0 ) }
if ($a >= 0) { ret( $a ) }
if ($a >= 1) { ret( 1 ) }
}
fn gauss( $src, $dest, $rad ) {
sigma = max(($rad / 2), 1)
kernel_width = (2 * $rad) + 1
kernel = new(kernel_width, kernel_width, FLOAT)
sum = 0
for (x = -$rad ; x <= $rad ; x + 1) {
for (y = -$rad ; y <= $rad ; y + 1) {
exp_num = -(x * x + y * y)
exp_dem = 2 * sigma * sigma
expression = pow(M_E, exp_num / exp_dem)
ker_val = (expression / (2 * M_PI * sigma * sigma))
kernel[(x + $rad), (y + $rad)] = ker_val
sum = sum + ker_val
}
}
FIXED_POINT_MUL = 32768
for (x = 0 ; x < kernel_width ; x + 1) {
for (y = 0 ; y < kernel_width ; y + 1) {
kernel[x, y] = kernel[x, y] / sum * FIXED_POINT_MUL
}
}
convert_type( kernel, INT )
conv_filter( $dest, $src, kernel, FIXED_POINT_MUL, 0, CONV_FILTER_COLOR )
}
fn smooth_staircase( $x, $h, $a, $w, $o ) {
part_1 = ( $a * ( $x - $o ) ) / ( $w )
part_2 = ( $a * floor( ( $x - $o ) / $w ) )
part_3 = ( $a / 2 )
part_4 = tanh( part_1 - part_2 - part_3 )
part_5 = ( 2 * tanh( $a / 2 ) )
part_6 = ( part_4 / part_5 )
whole = ( $h * ( part_6 + ( 1 / 2 ) + floor( ( $x - $o ) / $w ) ) )
ret( whole )
}
show_memory_debug_messages( 1 )

1037
demo.pixi

File diff suppressed because it is too large Load Diff

109
ffmpeg_video_export.pixi

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
//
// ffmpeg/avconv-based video export functions
// (experimental)
//
fn ffmpeg_video_export_thread( $thread_id, $ff )
{
if $ff < 0 { ret( 0 ) }
$ts = ""
$audio = ""
$audio2 = ""
if $ff.audio_file_name > 0
{
sprintf( $audio, "-i \"%s\"", $ff.audio_file_name )
$audio2 = "-acodec pcm_s16le"
}
$vcodec = ""
if $ff.kbits == 0
{
sprintf( $vcodec, "-vcodec libx264 -b:v 2000k -preset fast" )
}
else
{
sprintf( $vcodec, "-vcodec libx264 -b:v %uk -preset slow", $ff.kbits )
}
sprintf(
$ts,
"%s %s -y -pix_fmt bgr32 -s %dx%d -f rawvideo -r %d -i \"%s\" %s %s \"%s\"",
g_avconv,
$audio,
$ff.xsize,
$ff.ysize,
$ff.fps,
$ff.pipe_name,
$vcodec,
$audio2,
$ff.name )
printf( "FFmpeg (output) command: %s\n", $ts )
system( $ts )
}
fn ffmpeg_video_export_open( $filename, $xsize, $ysize, $fps, $kbits, $max_frames, $audio_file_name )
{
g_avconv = -1
while 1
{
if system( "ffmpeg -version" ) == 0 { g_avconv = "ffmpeg" break }
if system( "avconv -version" ) == 0 { g_avconv = "avconv" break }
break
}
if g_avconv == -1
{
logf( "No AV converters detected. Please install ffmpeg or avconv!\n" )
logf( "Please also note that this library is designed for *nix systems (Linux, macOS, BSD, etc.) only.\n" )
ret( -1 )
}
$ff = new( 1, 1, INT )
$ff.xsize = $xsize
$ff.ysize = $ysize
$ff.fps = $fps
$ff.kbits = $kbits
$ff.name = $filename
$ff.pipe_name = new( 1, 1, INT8 )
sprintf( $ff.pipe_name, "/tmp/avconv_vpipe_%d", $ff )
$ff.frame = 0
$ff.max_frames = $max_frames
if $0 >= 7
{
$ff.audio_file_name = $audio_file_name
}
$ts = ""
sprintf( $ts, "mkfifo %s", $ff.pipe_name )
system( $ts )
$ff.th = thread_create( ffmpeg_video_export_thread, $ff )
ret( $ff )
}
fn ffmpeg_video_export_close( $ff )
{
if $ff < 0 { ret( 0 ) }
if $ff.pipe_f
{
fclose( $ff.pipe_f )
}
thread_destroy( $ff.th, INT_MAX )
remove_file( $ff.pipe_name )
remove( $ff.pipe_name )
remove( $ff )
}
fn ffmpeg_video_export_write( $ff, $buf )
{
if $ff < 0 { ret( 0 ) }
if $ff.pipe_f == 0 { $ff.pipe_f = fopen( $ff.pipe_name, "wb" ) }
if $ff.pipe_f
{
fwrite( $buf, get_size( $buf ) * get_esize( $buf ), $ff.pipe_f )
$ff.frame + 1
if $ff.max_frames && $ff.frame >= $ff.max_frames
{
fputs( "FFmpeg export stop\n" )
ret( 1 )
}
}
ret( 0 )
}

BIN
media/artist.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
media/background.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
media/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
media/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
media/song.mp3

Binary file not shown.

391
mjpeg.pixi

@ -0,0 +1,391 @@ @@ -0,0 +1,391 @@
//
// AVI MJPEG encoder
//
include "riff.pixi"
AVIH_FLAG_HASINDEX = 0x00000010 //Index at end of file; NOT RECOGNIZED BY MPLAYER & VLC :(
AVIH_FLAG_MUSTUSEINDEX = 0x00000020
AVIH_FLAG_ISINTERLEAVED = 0x00000100
AVIH_FLAG_TRUSTCKTYPE = 0x00000800 //Use CKType to find key frames
AVIH_FLAG_WASCAPTUREFILE = 0x00010000
AVIH_FLAG_COPYRIGHTED = 0x00020000
MJPEG_ENCODER_FLAG_HASSOUND = ( 1 << 0 )
MJPEG_ENCODER_FLAG_USEINDEX = ( 1 << 1 )
fn mjpeg_fn_error( $fn_name, $pars )
{
logf( "%s() MJPEG error: wrong number of parameters (%d)\n", $fn_name, $pars )
}
fn mjpeg_encoder_open(
$fps,
$width,
$height,
$quality,
$audio_channels,
$audio_frames_per_second,
$audio_sample_type,
$flags,
$output_stream )
{
if $0 != 9 { mjpeg_fn_error( "mjpeg_encoder_open", $0 ) ret( -1 ) }
$mj = new( 1, 1, INT )
$mj.fps = $fps
$mj.frames = 0
$mj.width = $width
$mj.height = $height
$mj.quality = $quality
$mj.audio_ch = $audio_channels
$mj.audio_freq = $audio_frames_per_second
$mj.audio_type = $audio_sample_type
$mj.flags = $flags
$mj.out = $output_stream
$mj.idx = new( 4, 1, INT32 )
$mj.idx_ptr = 0
$mj.jpeg = -1
$mj.silent_chunk = -1
$mj.audio_bits = 0
if $audio_sample_type == INT8 { $mj.audio_bits = 8 }
if $audio_sample_type == INT16 { $mj.audio_bits = 16 }
if $audio_sample_type == FLOAT32 { $mj.audio_bits = 32 }
if $mj.audio_bits == 0
{
logf( "MJPEG encoder error: wrong audio sample type. Possible values: INT8, INT16, FLOAT32\n" )
ret( -1 )
}
//AVI:
$mj.c_avi = riff_chunk_open( -1, "RIFF", $mj.out )
riff_chunk_write_data( $mj.c_avi, "AVI ", 0, 0 )
//Header:
$c_hdrl = riff_chunk_open( $mj.c_avi, "LIST", $mj.out )
riff_chunk_write_data( $c_hdrl, "hdrl", 0, 0 )
$c = riff_chunk_open( $c_hdrl, "avih", $mj.out )
$avih = new( 14, 1, INT32 )
clean( $avih )
$avih[ 0 ] = 1000000 div $mj.fps //microSecPerFrame
$avih[ 1 ] = 10000000 //maxBytesPerSec
$avih[ 3 ] = AVIH_FLAG_ISINTERLEAVED | AVIH_FLAG_TRUSTCKTYPE | AVIH_FLAG_WASCAPTUREFILE //flags
if $flags & MJPEG_ENCODER_FLAG_USEINDEX
{
$avih[ 3 ] | AVIH_FLAG_HASINDEX
}
$avih[ 4 ] = 0 //totalFrames
$mj.fix1_offset = ftell( $mj.out ) + 4 * 4
if $flags & MJPEG_ENCODER_FLAG_HASSOUND { $avih[ 6 ] = 2 } else { $avih[ 6 ] = 1 }
$avih[ 7 ] = 1024 * 1024 //suggestedBufferSize
$avih[ 8 ] = $mj.width
$avih[ 9 ] = $mj.height
riff_chunk_write_data( $c, $avih, 0, 0 )
riff_chunk_close( $c )
remove( $avih )
//Video stream info:
$c_strl = riff_chunk_open( $c_hdrl, "LIST", $mj.out )
riff_chunk_write_data( $c_strl, "strl", 0, 0 )
$c = riff_chunk_open( $c_strl, "strh", $mj.out )
$strh = new( 14, 1, INT32 )
clean( $strh )
$strh[ 0 ] = 'vids' //type
$strh[ 1 ] = 'MJPG' //handler
$strh[ 5 ] = 1 //scale
$strh[ 6 ] = $fps //rate
$strh[ 8 ] = 0 //length
$mj.fix2_offset = ftell( $mj.out ) + 8 * 4
$strh[ 9 ] = 1024 * 1024 //suggestedBufferSize
$strh[ 10 ] = -1 //quality
$strh[ 13 ] = $width | ( $height << 16 )
riff_chunk_write_data( $c, $strh, 0, 0 )
riff_chunk_close( $c )
remove( $strh )
$c = riff_chunk_open( $c_strl, "strf", $mj.out )
$bmph = new( 10, 1, INT32 )
clean( $bmph )
$bmph[ 0 ] = get_size( $bmph ) * get_esize( $bmph ) //size
$bmph[ 1 ] = $width
$bmph[ 2 ] = $height
$bmph[ 3 ] = 1 | ( 24 << 16 ) //planes and bitCount
$bmph[ 4 ] = 'MJPG' //compression
$bmph[ 5 ] = $width * $height * 3 //imgSize
riff_chunk_write_data( $c, $bmph, 0, 0 )
riff_chunk_close( $c )
remove( $bmph )
riff_chunk_close( $c_strl )
if $flags & MJPEG_ENCODER_FLAG_HASSOUND
{
//Audio stream info (uncompressed interleaved):
$c_strl = riff_chunk_open( $c_hdrl, "LIST", $mj.out )
riff_chunk_write_data( $c_strl, "strl", 0, 0 )
$c = riff_chunk_open( $c_strl, "strh", $mj.out )
$strh = new( 14, 1, INT32 )
clean( $strh )
$strh[ 0 ] = 'auds' //type
$strh[ 1 ] = 0 //handler
$strh[ 5 ] = 1 //scale
$strh[ 6 ] = $audio_frames_per_second //rate
$strh[ 8 ] = 0 //length
$mj.fix3_offset = ftell( $mj.out ) + 8 * 4
$strh[ 9 ] = 0 //suggestedBufferSize
$strh[ 10 ] = -1 //quality
$strh[ 11 ] = $audio_channels * ( $mj.audio_bits / 8 ) //sampleSize
riff_chunk_write_data( $c, $strh, 0, 0 )
riff_chunk_close( $c )
remove( $strh )
$c = riff_chunk_open( $c_strl, "strf", $mj.out )
$fmt = new( 4, 1, INT32 )
clean( $fmt )
$audio_format = 1; if $mj.audio_bits == 32 { $audio_format = 3 }
$fmt[ 0 ] = $audio_format | ( $audio_channels << 16 )
$fmt[ 1 ] = $audio_frames_per_second
$fmt[ 2 ] = $audio_frames_per_second * $audio_channels * ( $mj.audio_bits / 8 ) //bytes per second
$fmt[ 3 ] = $audio_channels * ( $mj.audio_bits / 8 ) | ( $mj.audio_bits << 16 ) //block align + bits
riff_chunk_write_data( $c, $fmt, 0, 0 )
riff_chunk_close( $c )
remove( $fmt )
riff_chunk_close( $c_strl )
}
riff_chunk_close( $c_hdrl )
//Video and Audio data:
$mj.c_movi = riff_chunk_open( $mj.c_avi, "LIST", $mj.out )
riff_chunk_write_data( $mj.c_movi, "movi", 0, 0 )
ret( $mj )
}
/*fn mjpeg_encoder_jfif_to_avi1( $jpeg )
{
if $0 != 1 { mjpeg_fn_error( "mjpeg_encoder_jfif_to_avi1", $0 ) ret( 0 ) }
$p = -1
$i = 0 while $i < 256
{
if $jpeg[ $i ] == 'J'
{
if $jpeg[ $i + 1 ] == 'F'
{
if $jpeg[ $i + 2 ] == 'I'
{
if $jpeg[ $i + 3 ] == 'F'
{
$p = $i
break
}
}
}
}
$i + 1
}
if $p >= 0
{
$size = get_size( $jpeg )
$jpeg[ $p ] = 'A' $p + 1
$jpeg[ $p ] = 'V' $p + 1
$jpeg[ $p ] = 'I' $p + 1
$jpeg[ $p ] = '1' $p + 1
$jpeg[ $p ] = 0 $p + 1
$jpeg[ $p ] = 0 $p + 1
$jpeg[ $p ] = $size & 255 $p + 1
$jpeg[ $p ] = ( $size >> 8 ) & 255 $p + 1
$jpeg[ $p ] = ( $size >> 16 ) & 255 $p + 1
$jpeg[ $p ] = ( $size >> 24 ) & 255 $p + 1
$jpeg[ $p ] = $size & 255 $p + 1
$jpeg[ $p ] = ( $size >> 8 ) & 255 $p + 1
$jpeg[ $p ] = ( $size >> 16 ) & 255 $p + 1
$jpeg[ $p ] = ( $size >> 24 ) & 255 $p + 1
}
ret( 0 )
}*/
fn mjpeg_encoder_write_jpeg( $mj, $jpeg, $offset_bytes, $size_bytes )
{
if $0 != 4 { mjpeg_fn_error( "mjpeg_encoder_write_jpeg", $0 ) ret( 0 ) }
if $mj <= 0 { ret( 0 ) }
if $mj.flags & MJPEG_ENCODER_FLAG_USEINDEX
{
mjpeg_encoder_add_index( $mj, '00dc', 0x10, ftell( $mj.out ) - ( $mj.c_movi.size_ptr + 4 ), $size_bytes )
}
$c = riff_chunk_open( $mj.c_movi, "00dc", $mj.out )
riff_chunk_write_data( $c, $jpeg, $offset_bytes, $size_bytes )
riff_chunk_close( $c )
ret( $size_bytes )
}
fn mjpeg_encoder_write_image( $mj, $img )
{
if $0 != 2 { mjpeg_fn_error( "mjpeg_encoder_write_image", $0 ) ret( 0 ) }
if $mj <= 0 { ret( 0 ) }
if $mj.jpeg <= 0
{
$mj.jpeg = new( 1, 1, INT8 )
}
$jpeg = $mj.jpeg
if $img != -999
{
$f = fopen_mem( $jpeg )
fsave( $img, $f, FORMAT_JPEG, $mj.quality )
fclose( $f )
}
if $mj.flags & MJPEG_ENCODER_FLAG_USEINDEX
{
mjpeg_encoder_add_index( $mj, '00dc', 0x10, ftell( $mj.out ) - ( $mj.c_movi.size_ptr + 4 ), get_size( $jpeg ) )
}
$c = riff_chunk_open( $mj.c_movi, "00dc", $mj.out )
riff_chunk_write_data( $c, $jpeg, 0, 0 )
riff_chunk_close( $c )
ret( get_size( $jpeg ) )
}
fn mjpeg_encoder_get_audio_size( $mj ) //Number of audio frames per chunk
{
if $0 != 1 { mjpeg_fn_error( "mjpeg_encoder_get_audio_size", $0 ) ret( 0 ) }
if $mj <= 0 { ret( 0 ) }
$len1 = ( ( $mj.frames - 1 ) * $mj.audio_freq ) div $mj.fps
$len2 = ( $mj.frames * $mj.audio_freq ) div $mj.fps
$len = $len2 - $len1
ret( $len )
}
fn mjpeg_encoder_write_audio( $mj, $audio, $offset_bytes, $size_bytes )
{
if $0 != 4 { mjpeg_fn_error( "mjpeg_encoder_write_audio", $0 ) ret( 0 ) }
if $mj <= 0 { ret( 0 ) }
$bytes = mjpeg_encoder_get_audio_size( $mj ) * $mj.audio_ch * ( $mj.audio_bits div 8 )
if $mj.c_01wb <= 0
{
if $mj.flags & MJPEG_ENCODER_FLAG_USEINDEX
{
mjpeg_encoder_add_index( $mj, '01wb', 0, ftell( $mj.out ) - ( $mj.c_movi.size_ptr + 4 ), $bytes )
}
$mj.c_01wb = riff_chunk_open( $mj.c_movi, "01wb", $mj.out )
}
if $audio <= 0
{
//Silent chunk:
if $mj.silent_chunk <= 0
{
$mj.silent_chunk = new( $bytes, 1, INT8 )
clean( $mj.silent_chunk )
}
$audio = $mj.silent_chunk
}
riff_chunk_write_data( $mj.c_01wb, $audio, $offset_bytes, $size_bytes )
ret( $size_bytes )
}
fn mjpeg_encoder_next_frame( $mj )
{
if $0 != 1 { mjpeg_fn_error( "mjpeg_encoder_next_frame", $0 ) ret( -1 ) }
if $mj <= 0 { ret( -1 ) }
if $mj.c_01wb > 0
{
//Close audio chunk:
riff_chunk_close( $mj.c_01wb )
$mj.c_01wb = -1
}
$mj.frames + 1
ret( 0 )
}
fn mjpeg_encoder_add_index( $mj, $id, $flags, $offset, $size )
{
if $0 != 5 { mjpeg_fn_error( "mjpeg_encoder_add_index", $0 ) ret( -1 ) }
if $mj <= 0 { ret( -1 ) }
$idx = $mj.idx
$idx_ptr = $mj.idx_ptr
$idx[ $idx_ptr * 4 + 0 ] = $id
$idx[ $idx_ptr * 4 + 1 ] = $flags
$idx[ $idx_ptr * 4 + 2 ] = $offset
$idx[ $idx_ptr * 4 + 3 ] = $size
$idx_ptr + 1
if $idx_ptr >= get_size( $idx ) / 4
{
resize( $idx, ( $idx_ptr + 256 ) * 4, 1, INT32 )
}
$mj.idx_ptr = $idx_ptr
ret( 0 )
}
fn mjpeg_encoder_close( $mj )
{
if $0 != 1 { mjpeg_fn_error( "mjpeg_encoder_close", $0 ) ret( -1 ) }
if $mj <= 0 { ret( -1 ) }
if $mj.frames <= 0
{
logf( "MJPEG encoder error: no frames\n" )
}
riff_chunk_close( $mj.c_movi )
//Index:
if $mj.flags & MJPEG_ENCODER_FLAG_USEINDEX
{
$idx = $mj.idx
$idx_ptr = $mj.idx_ptr
$c = riff_chunk_open( $mj.c_avi, "idx1", $mj.out )
riff_chunk_write_data( $c, $idx, 0, $idx_ptr * 4 * 4 )
riff_chunk_close( $c )
}
//Fixup:
fseek( $mj.out, $mj.fix1_offset, SEEK_SET )
riff_write_int32( $mj.frames, $mj.out ) //totalFrames
fseek( $mj.out, $mj.fix2_offset, SEEK_SET )
riff_write_int32( $mj.frames, $mj.out ) //video stream length
if $mj.flags & MJPEG_ENCODER_FLAG_HASSOUND
{
fseek( $mj.out, $mj.fix3_offset, SEEK_SET )
riff_write_int32( ( $mj.frames * $mj.audio_freq ) div $mj.fps, $mj.out ) //audio stream length (number of frames)
}
riff_chunk_close( $mj.c_avi )
remove( $mj.silent_chunk )
remove( $mj.jpeg )
remove( $mj.idx )
remove( $mj )
ret( 0 )
}

111
riff.pixi

@ -0,0 +1,111 @@ @@ -0,0 +1,111 @@
/*
Functions for working with Resource Interchange File Format (RIFF)
How to write:
c = riff_chunk_open( -1, "NAME", stream )
riff_chunk_write_data( c, data_container, 0, 0 )
riff_chunk_close( c )
How to read: not implemented yet.
*/
fn riff_write_int32( $val, $f )
{
$w = 0
while 1
{
if fputc( $val & 255, $f ) == -1 { $w = 0 break }
if fputc( ( $val >> 8 ) & 255, $f ) == -1 { $w = 0 break }
if fputc( ( $val >> 16 ) & 255, $f ) == -1 { $w = 0 break }
if fputc( ( $val >> 24 ) & 255, $f ) == -1 { $w = 0 break }
$w = 4
break
}
ret( $w )
}
fn riff_chunk_open( $parent, $id, $f )
{
$err = 0
$c = new( 1, 1, INT )
$c.parent = $parent
if $parent > 0
{
$parent.size + 8
$parent2 = $parent.parent
while 1
{
if $parent2 == -1 { break }
$parent2.size + 8
$parent2 = $parent2.parent
}
}
$c.id = $id
$c.size = 0
$c.size_ptr = 0
$c.f = $f
while 1
{
if fwrite( $id, 4, $f ) != 4 { $err = 1 break } //Save chunk ID
$c.size_ptr = ftell( $f )
if riff_write_int32( 0, $f ) != 4 { $err = 1 break } //Save chunk size (zero by default)
break
}
if $err
{
remove( $c )
$c = -1
}
ret( $c )
}
fn riff_chunk_close( $c )
{
if $c <= 0 { logf( "Close: no chunk\n" ) ret }
if $c.size
{
$f = $c.f
$prev_pos = ftell( $f )
fseek( $f, $c.size_ptr, SEEK_SET )
riff_write_int32( $c.size, $f )
fseek( $f, $prev_pos, SEEK_SET )
if $c.size & 1
{
fputc( 0, $f );
$parent = $c.parent
while 1
{
if $parent == -1 { break }
$parent.size + 1
$parent = $parent.parent
}
}
}
remove( $c )
}
fn riff_chunk_write_data( $c, $data, $offset_bytes, $size_bytes )
{
$w = 0
if $c <= 0 { logf( "Write: no chunk\n" ) ret }
if $size_bytes <= 0
{
$size_bytes = get_size( $data ) * get_esize( $data )
}
$f = $c.f
$w = fwrite( $data, $size_bytes, $f, $offset_bytes )
if $w == $size_bytes
{
$c.size + $size_bytes
$parent = $c.parent
while 1
{
if $parent == -1 { break }
$parent.size + $size_bytes
$parent = $parent.parent
}
}
ret( $w )
}
Loading…
Cancel
Save