\ extensions.fs -- extensions.scm -> extensions.fs

\ Translator/Author: Michael Scholz <mi-scholz@users.sourceforge.net>
\ Created: 05/12/18 19:21:00
\ Changed: 17/12/02 03:19:40
\
\ @(#)extensions.fs	1.51 12/2/17

\ With comments and doc strings from extensions.scm.
\
\ remove-sound-property		( key :optional snd -- props )
\ set-sound-property-save-state-ignore ( key snd -- val )
\ remove-channel-property	( key :optional snd chn -- props )
\ set-channel-property-save-state-ignore ( key snd chn -- val )
\ channel-sync			( snd chn -- val )
\ set-channel-sync		( snd chn val -- )
\
\ normalize-mix			( filename beg in-chn snd chn -- scl )
\ enveloped-mix			( filename beg env -- )
\ map-sound-files		( func :optional dir -- lst )
\ for-each-sound-file		( func :optional dir -- )
\ match-sound-files		( func :optional dir -- ary )
\ selection-members		( -- array-of-arrays )
\ make-selection		( :optional beg end snd chn -- )
\
\ mix-channel			( fdata :optional beg dur s c edp -- val )
\ insert-channel		( fdata :optional beg 0 dur s c edp -- val )
\ redo-channel			( :optional edits snd chn -- )
\ undo-channel			( :optional edits snd chn -- )
\ any-env-channel		( en fc :optional beg dur s c edp or -- val )
\ sine-ramp			( r0 r1 :optional beg dur s c edpos -- val )
\ sine-env-channel		( en :optional beg dur s c edpos -- val )
\ blackman4-ramp		( r0 r1 :optional beg dur s c ep -- val )
\ blackman4-env-channel		( en :optional beg dur s c ep -- val )
\ ramp-squared			( r0 r1 :optional sym beg dur s c ep -- val )
\ env-squared-channel		( en :optional sym beg dur s c ep -- val )
\ ramp-expt			( rmp0 rmp1 exponent ... )
\ env-expt-channel		( en exponent ... )
\ offset-channel		( amount :optional beg dur s c ep -- val )
\ offset-sound			( offset :optional beg dur snd -- )
\ pad-sound			( beg dur :optional snd -- )
\ dither-channel		( :optional amount beg dur s c ep -- val )
\ dither-sound			( :optional amount beg dur s -- )
\ contrast-channel		( index :optional beg dur s c ep -- val )
\ contrast-sound		( index :optional beg dur snd -- )
\ scale-sound			( scl :optional beg dur snd -- )
\ normalize-sound		( amp :optional beg dur snd -- )
\ 
\ channels=			( s1 c1 s2 c2 :optional allowable-diff -- f )
\ channels-equal?		( s1 c1 s2 c2 :optional allowable-diff -- f )
\ mono->stereo			( new-name snd1 chn1 snd2 chn2 -- snd )
\ mono-files->stereo		( new-name chan1-name chan2-name -- snd )
\ stereo->mono			( orig-s c1-name c2-name -- snd0 snd1 )
\
\ with-reopen-menu		( -- )
\ with-buffers-menu		( -- )
\ 
\ if-cursor-follows-play-it-stays-where-play-stopped ( :optional enable -- )

require clm
require examp

\ ;;; -------- sound-property

: remove-sound-property <{ key :optional snd #f -- props }>
	doc" Remove key-value pair in the given sound's \
property list and return altered list."
	snd sound-properties key array-assoc-remove!
;

\ 'save-state-ignore key snd set-sound-property
: set-sound-property-save-state-ignore <{ key :optional snd #f -- val }>
	'save-state-ignore snd sound-property { ign }
	ign array? if
		ign key array-push
	else
		#( 'save-state-ignore key )
	then to ign
	'save-state-ignore ign snd set-sound-property
;

\ ;;; -------- channel-property

: remove-channel-property <{ key :optional snd #f chn #f -- props }>
	doc" Remove key-value pair in the given channel's \
property list and return altered list."
	snd chn channel-properties key array-assoc-remove!
;

: set-channel-property-save-state-ignore
    <{ key :optional snd #f chn #f -- val }>
	'save-state-ignore snd chn channel-property { ign }
	ign array? if
		ign key array-push
	else
		#( 'save-state-ignore key )
	then to ign
	'save-state-ignore ign snd chn set-channel-property
;

: channel-sync { snd chn -- val }
	'sync snd chn channel-property
;

: set-channel-sync { snd chn val -- }
	'sync val snd chn set-channel-property drop
;

\ ;;; -------- mix with result at original peak amp

: normalize-mix ( filename beg in-chn snd chn -- scl )
	doc" It is like mix but the mix result has same peak \
amp as unmixed SND/CHN (returns scaler)."
	{ filename beg in-chan snd chn }
	snd chn #f maxamp { original-maxamp }
	filename beg in-chan snd chn undef undef undef mix drop
	snd chn #f maxamp { new-maxamp }
	original-maxamp new-maxamp f<> if
		original-maxamp new-maxamp f/ { scaler }
		snd sync { old-sync }
		0 snd set-sync drop
		scaler snd chn scale-by drop
		old-sync snd set-sync drop
		scaler
	else
		1.0
	then
;

\ ;;;-------- mix with envelope on mixed-in file
\ ;;;
\ ;;; there are lots of ways to do this; this version uses functions
\ ;;;   from Snd, CLM, and Sndlib.

hide
: em-cb { amp-env rd -- prc; y self -- val }
	1 proc-create ( prc ) amp-env , rd ,
  does> { y self -- val }
	self @ { amp-env }
	self cell+ @ { rd }
	amp-env env rd readin f* y f+
;
set-current

: enveloped-mix ( filename beg en -- )
	doc" Mix FILENAME starting at BEG with amplitude envelope EN.\n\
\"pistol.snd\" 0 #( 0 0 1 1 2 0 ) enveloped-mix."
	{ filename beg en }
	filename framples { len }
	:envelope en :length len make-env { amp-env }
	filename make-readin { rd }
	amp-env rd em-cb beg len map-channel drop
;
previous

\ ;;; -------- map-sound-files, match-sound-files
\ ;;;
\ ;;; apply a function to each sound in dir
\ ;;;
\ ;;;   (map-sound-files (lambda (n) (if (> (mus-sound-duration n) 10.0)
\ ;;;     (snd-print n))))

: map-sound-files <{ func :optional dir "." -- lst }>
	doc" Apply FUNC to each sound file in DIR."
	dir sound-files-in-directory map
		func #( *key* ) run-proc
	end-map
;
\ lambda: <{ n -- str|#f }>
\ 	n mus-sound-duration 10.0 f> if
\		n snd-print ( n remains on stack )
\		cr
\	else
\		#f
\	then
\ ; map-sound-files

: for-each-sound-file <{ func :optional dir "." -- }>
	doc" Apply FUNC to each sound file in DIR."
	dir sound-files-in-directory each { f }
		func #( f ) run-proc drop
	end-each
;
0 [if]
	"/home/bil/sf" value loop-path
	lambda: <{ n -- }>
		loop-path "/" $+ n $+
		    <'> mus-sound-loop-info #t nil fth-catch if
			stack-reset
		else
			( loop-list ) empty? unless
				n snd-print drop
				cr
			then
		then
	; loop-path for-each-sound-file
[then]

: match-sound-files <{ func :optional dir "." -- ary }>
	doc" Apply FUNC to each sound file in DIR and \
returns an array of files for which FUNC does not return #f."
	#() { matches }
	dir sound-files-in-directory each { f }
		func #( f ) run-proc if
			matches f array-push drop
		then
	end-each
	matches
;
\ lambda: <{ n -- f }> /\.(wav?|snd)$/ n regexp-match ; "." match-sound-files

\ ;;; -------- selection-members
\ ;;;
\ ;;; returns a list of lists of (snd chn): channels in current selection

: selection-members ( -- array-of-lists )
	doc" Return an array of lists of #( snd chn ) indicating \
the channels participating in the current selection."
	#() { sndlist }
	undef selection? if
		sounds each { snd }
			snd channels 0 ?do
				snd i selection-member? if
					sndlist #( snd i ) array-push drop
				then
			loop
		end-each
	then
	sndlist
;

\ ;;; -------- make-selection
\ ;;;
\ ;;; the regularized form of this would use dur not end

hide
: add-chan-to-selection { beg end snd chn -- }
	beg integer? unless
		0 to beg
	then
	end integer? if
		end 1+
	else
		snd chn #f framples
	then beg - to end
	#t snd chn set-selection-member? drop
	beg snd chn set-selection-position drop
	end snd chn set-selection-framples drop
;
set-current

: make-selection <{ :optional beg 0 end #f snd #f chn #f -- }>
	doc" Make a selection like make-region but without creating a region.  \
It follows SND's sync field, and applies to all SND's \
channels if CHN is not specified.  \
END defaults to end of channel, BEG defaults to 0, \
SND defaults to the currently selected sound."
	snd snd-snd { current-sound }
	current-sound sound? unless
		'no-such-sound
		    #( "%s: can't find sound %s" get-func-name snd ) fth-throw
	then
	current-sound sync { current-sync }
	unselect-all drop
	chn integer? if
		beg end snd chn add-chan-to-selection
	else
		sounds each { s }
			snd #t =
			s current-sound = ||
			current-sync 0<> 
			current-sync s sync = && || if
				s channels 0 ?do
					beg end s i add-chan-to-selection
				loop
			then
		end-each
	then
;
previous

\ ;;; -------- mix-channel, insert-channel, c-channel

hide
: mc-cb { rd -- prc; y self -- val }
	1 proc-create ( prc )
	rd ,
  does> { y self -- val }
	self @ ( rd ) next-sample y f+
;
set-current

: mix-channel <{ file :optional beg 0 dur #f snd #f chn #f edpos #f -- r }>
	doc" Mix in FILE.  \
FILE can be the file name or a list #( file-name [beg [channel]] )."
	file string? if
		file
	else
		file 0 array-ref
	then { file-name }
	file-name find-file to file-name
	file-name unless
		'no-such-file #( "%s: %S" get-func-name file-name ) fth-throw
	then
	file string?
	file length 2 < || if
		0
	else
		file 1 array-ref
	then { file-beg }
	file string?
	file length 3 < || if
		0
	else
		file 2 array-ref
	then { file-channel }
	dur file-name mus-sound-framples file-beg - || { len }
	beg 0< if
		'no-such-sample #( "%s: %s" get-func-name beg ) fth-throw
	then
	len 0> if
		file-beg file-name file-channel 1 #f make-sampler { reader }
		"%S %s %s %s" #( file beg dur get-func-name )
		    string-format { origin }
		reader mc-cb beg len snd chn edpos origin map-channel
	else
		#f
	then
;
previous

: insert-channel <{ file :optional beg 0 dur #f snd #f chn #f edpos #f -- r }>
	doc" Insert the FILE.  \
FILE can be the file name or a list #( file-name [beg [channel]] )."
	file string? if
		file
	else
		file 0 array-ref
	then { file-name }
	file-name find-file to file-name
	file-name unless
		'no-such-file #( "%s: %S" get-func-name file-name ) fth-throw
	then
	file string?
	file length 2 < || if
		0
	else
		file 1 array-ref
	then { file-beg }
	file string?
	file length 3 < || if
		0
	else
		file 2 array-ref
	then { file-channel }
	dur file-name mus-sound-framples file-beg - || { len }
	beg 0< if
		'no-such-sample #( "%s: %s" get-func-name beg ) fth-throw
	then
	len 0> if
		file-beg file-name file-channel 1 #f make-sampler { reader }
		len 0.0 make-vct map!
			reader next-sample
		end-map { data }
		reader free-sampler drop
		"%S %s %s %s" #( file beg dur get-func-name )
		    string-format { origin }
		beg len data snd chn edpos #f origin insert-samples
	else
		#f
	then
;

\ ;;; -------- redo-channel, undo-channel

: redo-channel <{ :optional edits 1 snd #f chn #f -- }>
	doc" It's the regularized version of redo."
	snd fixnum?
	snd sync 0<> &&
	chn fixnum?  && if
		snd chn edit-position edits + snd chn set-edit-position drop
	else
		edits snd chn redo drop
	then
;

: undo-channel <{ :optional edits 1 snd #f chn #f -- }>
	doc" It's the regularized version of undo."
	snd fixnum?
	snd sync 0<> &&
	chn fixnum?  && if
		snd chn edit-position edits - 0 max
		    snd chn set-edit-position drop
	else
		edits snd chn undo drop
	then
;

\ ;;; -------- any-env-channel

hide
: aec-cb { en func beg dur snd chn edpos -- prc; self -- val }
	0 proc-create ( prc )
	en , func , beg , dur , snd , chn , edpos ,
  does> { self -- val }
	self           @ { en }
	self   cell+   @ { func }
	self 2 cells + @ { beg }
	self 3 cells + @ { dur }
	self 4 cells + @ { snd }
	self 5 cells + @ { chn }
	self 6 cells + @ { edpos }
	0.0 0.0 { x0 y0 }
	en 0 array-ref { x1 }
	en 1 array-ref { y1 }
	en envelope-last-x en 0 array-ref f- { xrange }
	beg { ramp-beg }
	0 { ramp-dur }
	en length 1- 2 ?do
		x1 to x0
		y1 to y0
		en i    array-ref to x1
		en i 1+ array-ref to y1
		x1 x0 f- xrange f/ dur f* fround->s to ramp-dur
		y0 y1 f= if
			y0 ramp-beg ramp-dur snd chn edpos scale-channel
		else
			func #( y0 y1 ramp-beg ramp-dur snd chn edpos ) run-proc
		then
		ramp-dur +to ramp-beg
	2 +loop
;
set-current

: any-env-channel
    <{ en func :optional beg 0 dur #f snd #f chn #f edpos #f origin #f -- val }>
	en nil? if
		#f
		exit
	then
	en envelope-length ( pts ) 1 = if
		en 0 array-ref beg dur snd chn edpos scale-channel
	else
		dur integer? unless
			snd chn #f framples to dur
		then
		en func beg dur snd chn edpos aec-cb origin as-one-edit
	then
;
previous

\ ;;; -------- sine-ramp sine-env-channel 

hide
: sine-ramp-cb { rmp0 rmpd incr -- prc; y self -- val }
	1 proc-create ( prc )
	rmp0 , rmpd , pi fnegate ( angle ) , incr ,
  does> { y self -- val }
	self           @ { rmp0 }
	self 1 cells + @ { rmpd }
	self 2 cells + @ { angle }
	self 3 cells + @ { incr }
	angle fcos 0.5 f* 0.5 f+ rmpd f* rmp0 f+ y f* ( val )
	angle incr f+ self 2 cells + ! ( angle += incr )
	( val )
;
set-current

: sine-ramp <{ rmp0 rmp1 :optional beg 0 dur #f snd #f chn #f edpos #f -- val }>
	doc" Produce a sinsusoidal connection from RMP0 to RMP1."
	"%s %s %s %s %s" #( rmp0 rmp1 beg dur get-func-name )
	    string-format { origin }
	dur number? if
		dur
	else
		snd chn edpos framples  beg  f-
	then pi swap f/ { incr }
	rmp0 rmp1 rmp0 f- incr sine-ramp-cb
	    beg dur snd chn edpos origin map-channel
;
previous

: sine-env-channel <{ en :optional beg 0 dur #f snd #f chn #f edpos #f -- val }>
	doc" Connect ENV's dots with sinusoids."
	"%s %s %s %s" #( en beg dur get-func-name ) string-format { origin }
	en <'> sine-ramp beg dur snd chn edpos origin any-env-channel
;
\ #( 0 0 1 1 2 -0.5 3 1 ) sine-env-channel

\ ;;; an obvious extension of this idea is to use the blackman fft
\ ;;;   window formulas to get sharper sinusoids (i.e. use the sum of n
\ ;;;   cosines, rather than just 1)
\ 
\ ;;; -------- blackman4-ramp, blackman4-env-channel

hide
: b4r-cb { rmp0 rmpd incr -- prc; y self -- val }
	1 proc-create ( prc )
	rmp0 , rmpd , 0.0 ( angle ) , incr ,
  does> { y self -- val }
	self           @ { rmp0 }
	self 1 cells + @ { rmpd }
	self 2 cells + @ { angle }
	self 3 cells + @ { incr }
	angle fcos { cx }
	cx 0.041194 f* -0.20762 f+
	cx f* 0.375696 f+ 
	cx f* -0.29145 f+ 
	cx f* 0.084037 f+
	rmpd f* rmp0 f+ 
	y f* ( val )
	angle incr f+ self 2 cells + ! ( angle += incr )
	( val )
;
set-current

: blackman4-ramp <{ rm0 rm1 :optional beg 0 dur #f snd #f chn #f ep #f -- val }>
	"%s %s %s %s %s" #( rm0 rm1 beg dur get-func-name )
	    string-format { origin }
	dur number? if
		dur
	else
		snd chn ep framples beg f-
	then pi swap f/ { incr }
	rm0 rm1 rm0 f- incr b4r-cb beg dur snd chn ep origin map-channel
;
previous

: blackman4-env-channel <{ en :optional beg 0 dur #f snd #f chn #f ep #f -- r }>
	"%s %s %s %s" #( en beg dur get-func-name ) string-format { origin }
	en <'> blackman4-ramp beg dur snd chn ep origin any-env-channel
;

\ ;;; -------- ramp-squared, env-squared-channel

hide
: rsq-cb { rmp0 rmpd incr -- prc; y self -- val }
	1 proc-create ( prc )
	rmp0 , rmpd , 0.0 ( angle ) , incr ,
  does> { y self -- val }
	self           @ { rmp0 }
	self 1 cells + @ { rmpd }
	self 2 cells + @ { angle }
	self 3 cells + @ { incr }
	angle dup f* rmpd f* rmp0 f+ y f* ( val )
	angle incr f+ self 2 cells + ! ( angle += incr )
	( val )
;
set-current

: ramp-squared <{ r0 r1 :optional sy #t beg 0 dur #f snd #f chn #f ep #f -- r }>
	doc" Connect R0 and R1 with an x^2 curve."
	"%s %s %s %s %s %s" #( r0 r1 sy beg dur get-func-name )
	    string-format { origin }
	dur number? if
		dur
	else
		snd chn ep framples beg f-
	then 1/f { incr }
	r0 r1 r0 f- incr rsq-cb beg dur snd chn ep origin map-channel
;
previous

hide
: esqc-cb { symmetric -- prc; r0 r1 b d s c e self -- val }
	7 proc-create ( prc )
	symmetric ,
  does> { r0 r1 b d s c e self -- val }
	r0 r1 self @ ( symmetric ) b d s c e ramp-squared
;
set-current

: env-squared-channel <{ en :optional sy #t beg 0 dur #f s #f c #f ep #f -- r }>
	doc" Connect ENV's dots with x^2 curves."
	"%s %s %s %s %s" #( en sy beg dur get-func-name )
	    string-format { origin }
	en sy esqc-cb beg dur s c ep origin any-env-channel
;
previous
\ #( 0 0 1 1 2 -0.5 3 1 ) env-squared-channel

\ ;;; -------- ramp-expt, env-expt-channel

: ramp-expt
    <{ rmp0 rmp1 expt :optional sym #t beg 0 dur #f snd #f chn #f ep #f -- r }>
	doc" Connect RMP0 and RMP1 with an x^EXPT curve."
	\ ;; a^x = exp(x * log(a))
	"%s %s %s %s %s %s %s"
	    #( rmp0 rmp1 expt sym beg dur get-func-name )
	    string-format { origin }
	dur number? if
		dur
	else
		snd chn ep framples beg d-
	then { len }
	len 1/f { incr }
	beg len snd chn ep samples { data }
	rmp1 rmp0 f- { scl }
	0.0 { angle }
	sym
	rmp1 rmp0 f< && if
		scl fnegate to scl
		1.0 to angle
		len 0 ?do
			data i
			    data i vct-ref
			    rmp1 scl angle expt f** f* f+
			    f*
			    vct-set! drop
			angle incr f- to angle
		loop
	else
		0.0 to angle
		len 0 ?do
			data i
			    data i vct-ref
			    rmp0 scl angle expt f** f* f+
			    f*
			    vct-set! drop
			angle incr f+ to angle
		loop
	then
	data beg len snd chn current-edit-position origin vct->channel
;

hide
: expc-cb { sym expt -- prc; r0 r1 b d s c e self -- val }
	7 proc-create ( prc ) expt , sym ,
  does> { r0 r1 b d s c e self -- val }
	r0 r1 self @ ( expt ) self cell+ @ ( sym ) b d s c e ramp-expt
;
set-current

: env-expt-channel
    <{ en ex :optional sym #t beg 0 dur #f snd #f chn #f ep #f -- r }>
	doc" Connect ENV's dots with x^exponent curves."
	ex 1.0 f= if
		en beg dur snd chn ep env-channel
	else
		"%s %s %s %s %s %s" #( en ex sym beg dur get-func-name )
		    string-format { origin }
		sym ex expc-cb { prc }
		en prc beg dur snd chn ep origin any-env-channel
	then
;
previous

\ ;;; -------- offset-channel

hide
: offc-cb { dc -- prc; y self -- val }
	1 proc-create ( prc )
	dc ,
  does> { y self -- val }
	self @ ( dc ) y f+
;
set-current

: offset-channel <{ amount :optional beg 0 dur #f snd #f chn #f edpos #f -- r }>
	doc" Add AMOUNT to each sample."
	"%s %s %s %s" #( amount beg dur get-func-name ) string-format { origin }
	amount offc-cb beg dur snd chn edpos origin map-channel
;
previous

: offset-sound <{ offset :optional beg 0 dur #f snd #f -- }>
	doc" Add OFFSET to every sample in SND."
	snd snd-snd to snd
	snd sound? if
		snd channels 0 ?do
			offset beg dur snd i ( chn ) #f offset-channel drop
		loop
	else
		'no-such-sound #( "%s: %s" get-func-name snd ) fth-throw
	then
;

\ ;;; -------- pad-sound

: pad-sound <{ beg dur :optional snd #f -- }>
	doc" Place a block of DUR zeros in every \
channel of SND starting at BEG."
	snd snd-snd to snd
	snd sound? if
		snd channels 0 ?do
			beg dur snd i ( chn ) #f pad-channel drop
		loop
	else
		'no-such-sound #( "%s: %s" get-func-name snd ) fth-throw
	then
;

\ ;;; -------- dither-channel

hide
: dith-cb { dither -- prc; y self -- val }
	1 proc-create ( prc )
	dither ,
  does> { y self -- val }
	self @ ( dither ) dup mus-random swap mus-random f+ y f+
;
set-current

: dither-channel
    <{ :optional amount 0.00006 beg 0 dur #f snd #f chn #f ep #f -- r }>
	doc" Add AMOUNT dither to each sample."
	"%s %s %s %s" #( amount beg dur get-func-name ) string-format { origin }
	amount f2/ dith-cb beg dur snd chn ep origin map-channel
;
previous

: dither-sound <{ :optional amount 0.00006 beg 0 dur #f snd #f -- }>
	doc" Add dithering to every sample of SND."
	snd snd-snd to snd
	snd sound? if
		snd channels 0 ?do
			amount beg dur snd i ( chn ) #f dither-channel drop
		loop
	else
		'no-such-sound #( "%s: %s" get-func-name snd ) fth-throw
	then
;

\ ;;; -------- contrast-channel

hide
: cntr-cb { index -- prc; y self -- val }
	1 proc-create ( prc )
	index ,
  does> { y self -- val }
	y two-pi f* fsin self @ ( index ) f*  y half-pi f*  f+ fsin
;
set-current

: contrast-channel <{ idx :optional beg 0 dur #f snd #f chn #f edpos #f -- r }>
	doc" Apply contrast-enhancement to the sound."
	"%s %s %s %s" #( idx beg dur get-func-name ) string-format { origin }
	idx cntr-cb beg dur snd chn edpos origin map-channel
;
previous

: contrast-sound <{ index :optional beg 0 dur #f snd #f -- }>
	doc" Apply contrast-enhancement to every channel of SND."
	snd snd-snd to snd
	snd sound? if
		snd channels 0 ?do
			index beg dur snd i ( chn ) #f contrast-channel drop
		loop
	else
		'no-such-sound #( "%s: %s" get-func-name snd ) fth-throw
	then
;

\ ;;; -------- scale-sound

: scale-sound <{ scl :optional beg 0 dur #f snd #f -- }>
	doc" Multiply every sample in SND by SCL."
	snd snd-snd to snd
	snd sound? if
		snd channels 0 ?do
			scl beg dur snd i ( chn ) #f scale-channel drop
		loop
	else
		'no-such-sound #( "%s: %s" get-func-name snd ) fth-throw
	then
;

\ ;;; -------- normalize-sound

: normalize-sound <{ amp :optional beg 0 dur #f snd #f -- }>
	doc" Scale SND to peak amplitude AMP."
	snd snd-snd to snd
	snd sound? if
		amp 0.0 snd #t #f maxamp each ( mx )
			fabs fmax
		end-each f/ { scl }
		snd channels 0 ?do
			scl beg dur snd i ( chn ) #f scale-channel drop
		loop
	else
		'no-such-sound #( "%s: %s" get-func-name snd ) fth-throw
	then
;

\ ;;; -------- channels-equal

hide
: c-equal-cb { rd diff -- prc; y self -- f }
	1 proc-create ( prc )
	rd , diff ,
  does> { y self -- f }
	self @ ( rd ) read-sample y f- fabs self cell+ @ ( diff ) f>
;
set-current

: channels= <{ snd1 chn1 snd2 chn2 :optional allowable-difference 0.0 -- f }>
	doc" Return #t if the two channels are the \
same (within diff) modulo trailing 0's."
	snd1 snd2 =
	chn1 chn2 = && if
		#t
	else
		snd1 chn1 #f maxamp { mx1 }
		snd2 chn2 #f maxamp { mx2 }
		mx1 mx2 f- fabs allowable-difference f> if
			#f
		else
			snd1 chn1 #f framples { len1 }
			snd2 chn2 #f framples { len2 }
			len1 len2 >= if
				len1 snd1 snd2 chn1 chn2
			else
				len2 snd2 snd1 chn2 chn1
			then { len s1 s2 c1 c2 }
			0 s2 c2 1 #f make-sampler { read2 }
			read2 allowable-difference c-equal-cb
			    0 len s1 c1 #f #f scan-channel not
		then
	then
;
previous

: channels-equal? <{ s1 c1 s2 c2 :optional allowable-difference 0.0 -- f }>
	doc" Return #t if the two channels are the same (within diff)."
	s1 c1 #f framples s2 c2 #f framples <> if
		#f
	else
		s1 c1 s2 c2 allowable-difference channels=
	then
;

\ ;;; -------- mono->stereo, mono-files->stereo

: mono->stereo ( new-name snd1 chn1 snd2 chn2 -- snd )
	doc" Take the two channels and combine \
them into a stereo sound NEW-NAME."
	{ new-name snd1 chn1 snd2 chn2 }
	snd1 chn1 edit-position { old-ed1 }
	snd2 chn2 edit-position { old-ed2 }
	:file new-name :channels 2 :srate snd1 srate new-sound { ind }
	ind 0 snd1 chn1 0 #f #f swap-channels drop
	ind 1 snd2 chn2 0 #f #f swap-channels drop
	old-ed1 snd1 chn1 set-edit-position drop
	old-ed2 snd2 chn2 set-edit-position drop
	ind
;
\ "test.snd" 0 0 1 0 mono->stereo

: mono-files->stereo ( new-name chan1-name chan2-name -- snd )
	doc" Combine two mono files into the stereo file NEW-NAME."
	{  new-name chan1-name chan2-name }
	chan1-name find-file to chan1-name
	chan1-name unless
		'no-such-file #( "%s: %S" get-func-name chan1-name ) fth-throw
	then
	chan1-name open-sound { ind1 }
	chan2-name find-file to chan2-name
	chan2-name unless
		'no-such-file #( "%s: %S" get-func-name chan2-name ) fth-throw
	then
	chan2-name open-sound { ind2 }
	new-name ind1 0 ind2 0 mono->stereo { ind3 }
	ind1 close-sound drop
	ind2 close-sound drop
	ind3
;
\ "test.snd" "oboe.snd" "pistol.snd" mono-files->stereo

: stereo->mono ( orig-snd chan1-name chan2-name -- snd0 snd1 )
	doc" Split a stereo sound into two mono sounds \
named CHAN1-NAME and CHAN2-NAME."
	{ orig-snd chan1-name chan2-name }
	orig-snd 0 edit-position { old-ed0 }
	orig-snd 1 edit-position { old-ed1 }
	:file chan1-name :srate orig-snd srate new-sound { chan1 }
	:file chan2-name :srate orig-snd srate new-sound { chan2 }
	orig-snd 0 chan1 0 0 #f #f swap-channels drop
	orig-snd 1 chan2 0 0 #f #f swap-channels drop
	old-ed0 orig-snd 0 set-edit-position drop
	old-ed1 orig-snd 1 set-edit-position drop
	chan1 chan2
;
\ 0 "hi1.snd" "hi2.snd" stereo->mono

\ === PREFERENCES DIALOG ===

\ --- reopen menu ---

hide
"empty" value reopen-empty
#() value reopen-names
#f value reopen-menu
16 value reopen-max-length

<'> noop 0 make-proc constant extensions-noop

: reopen-select-cb { brief-name long-name -- prc; self -- }
	0 proc-create ( prc )
	long-name , brief-name ,
  does> ( self -- )
	{ self }
	self @ { long-name }
	self cell+ @ { brief-name }
	reopen-menu brief-name remove-from-menu drop
	long-name file-exists? if
		long-name open-sound drop
	then
;

: add-to-reopen-menu <{ snd -- #f }>
	snd short-file-name { brief-name }
	snd file-name { long-name }
	reopen-names brief-name array-member? if
		#f
		exit
	then
	reopen-names reopen-empty array-member? if
		reopen-menu reopen-empty remove-from-menu drop
		#() to reopen-names
	then
	reopen-menu brief-name brief-name long-name reopen-select-cb
	    0 add-to-menu drop
	reopen-names brief-name array-push to reopen-names
	reopen-names length reopen-max-length > if
		reopen-menu reopen-names array-shift remove-from-menu drop
	then
	#f
;

: check-reopen-menu <{ file -- }>
	file #f file-basename { brief-name }
	reopen-names brief-name array-member? if
		reopen-menu brief-name remove-from-menu drop
		reopen-names brief-name array-index { idx }
		idx 0>= if
			reopen-names idx array-delete! drop
		then
	then
	reopen-names empty? if
		reopen-menu reopen-empty extensions-noop undef add-to-menu drop
		reopen-names reopen-empty array-push to reopen-names
	then
;
set-current

#f value including-reopen-menu	\ for prefs

: with-reopen-menu ( -- )
	including-reopen-menu unless
		#() to reopen-names
		reopen-menu false? if
			"Reopen" extensions-noop add-to-main-menu to reopen-menu
		then
		reopen-menu reopen-empty extensions-noop 0 add-to-menu drop
		reopen-names reopen-empty array-push to reopen-names
		#t to including-reopen-menu
		close-hook <'> add-to-reopen-menu add-hook!
		open-hook  <'> check-reopen-menu  add-hook!
	then
;
previous

\ --- buffers menu ---

hide
"empty" value buffer-empty
#() value buffer-names
#f value buffer-menu

: buffer-select-cb { file -- prc ; self -- }
	0 proc-create ( prc )
	file ,
  does> { self -- }
	self @ ( file ) 0 find-sound dup sound? if
		select-sound
	then drop
;

: open-buffer <{ file -- }>
	buffer-names buffer-empty array-member? if
		buffer-menu buffer-empty remove-from-menu drop
		#() to buffer-names
	then
	buffer-menu file file buffer-select-cb -1 add-to-menu drop
	buffer-names file array-push to buffer-names
;

: close-buffer <{ snd -- #f }>
	buffer-menu snd file-name remove-from-menu drop
	buffer-names snd file-name array-index { idx }
	idx 0>= if
		buffer-names idx array-delete! drop
	then
	buffer-names empty? if
		buffer-menu buffer-empty extensions-noop 0 add-to-menu drop
		buffer-names buffer-empty array-push to buffer-names
	then
	#f
;
set-current

#f value including-buffers-menu	\ for prefs

: with-buffers-menu ( -- )
	including-buffers-menu unless
		#() to buffer-names
		buffer-menu unless
			"Buffers" extensions-noop
			    add-to-main-menu to buffer-menu
		then
		buffer-menu buffer-empty extensions-noop 0 add-to-menu drop
		buffer-names buffer-empty array-push to buffer-names
		#t to including-buffers-menu
		open-hook  <'> open-buffer  add-hook!
		close-hook <'> close-buffer add-hook!
	then
;
previous

\ ;;; -------- cursor-follows-play and stays where it was when the play ended
\ moved from examp.fs to extensions.fs [ms]

hide
: current-cursor { snd chn -- cur }
	'cursor snd chn channel-property
;

: set-current-cursor { snd chn val -- }
	'cursor val snd chn set-channel-property
;

: original-cursor { snd chn -- cur }
	'original-cursor snd chn channel-property
;

: set-original-cursor { snd chn val -- }
	'original-cursor val snd chn set-channel-property
;

: local-start-playing-func <{ snd -- val }>
	snd channels 0 ?do
		snd i #f cursor { cur }
		snd i cur set-original-cursor
		snd i cur set-current-cursor
	loop
	#f
;

: local-stop-playing-func <{ snd -- val }>
	snd 0 current-cursor snd #t #f set-cursor
;
set-current

: if-cursor-follows-play-it-stays-where-play-stopped <{ :optional enable #t }>
	enable if
		start-playing-hook <'> local-start-playing-func add-hook!
		stop-playing-hook <'> local-stop-playing-func add-hook!
	else
		start-playing-hook <'> local-start-playing-func
		    remove-hook! drop
		stop-playing-hook <'> local-stop-playing-func remove-hook! drop
	then
;
previous

\ extensions.fs ends here
