—package
Audio::TinySoundFont::Builder;
use
v5.14;
use
warnings;
our
$VERSION
=
'0.12'
;
use
Carp;
use
Moo;
has
soundfont
=> (
is
=>
'ro'
,
isa
=> InstanceOf [
'Audio::TinySoundFont'
],
required
=> 1,
);
has
play_script
=> (
is
=>
'rwp'
,
isa
=> ArrayRef,
default
=>
sub
{ [] },
coerce
=> \
&_coerce_play_script
,
);
*SAMPLE_RATE
= \
&Audio::TinySoundFont::XS::SAMPLE_RATE
;
sub
_coerce_play_script
{
my
$new_script
=
shift
;
croak
"play_script requires an ArrayRef"
if
ref
$new_script
ne
'ARRAY'
;
my
@result
;
foreach
my
$item
(
@$new_script
)
{
croak
"Script items must be a HashRef, not: "
.
ref
$item
if
ref
$item
ne
'HASH'
;
push
@result
, {
in_seconds
=> (
$item
->{in_seconds} // 1 ) + 0,
at
=> (
$item
->{at} // 0 ) + 0,
for
=> (
$item
->{
for
} // 1 ) + 0,
note
=> (
$item
->{note} // 60 ) + 0,
vel
=> (
$item
->{vel} // 0.5 ) + 0,
preset
=> (
$item
->{preset} //
''
),
};
}
return
\
@result
;
}
sub
clear
{
my
$self
=
shift
;
$self
->set( [] );
return
;
}
sub
set
{
my
$self
=
shift
;
my
$script
=
shift
;
$self
->_set_play_script(
$script
);
return
;
}
sub
add
{
my
$self
=
shift
;
my
$script
=
shift
;
croak
"add requires an ArrayRef"
if
ref
$script
ne
'ARRAY'
;
my
$old_script
=
$self
->play_script;
$self
->_set_play_script( [
@$old_script
,
@$script
] );
return
;
}
sub
render
{
my
$self
=
shift
;
my
%args
=
@_
;
my
$vol
=
$args
{volume} //
$self
->soundfont->db_to_vol(
$args
{db} );
my
$old_vol
;
if
(
defined
$vol
)
{
$old_vol
=
$self
->soundfont->volume;
$self
->soundfont->volume(
$vol
);
}
my
$script
=
$self
->play_script;
my
$SR
=
$self
->SAMPLE_RATE;
my
$result
=
''
;
croak
"Cannot process play_script when TinySoundFont is active"
if
$self
->soundfont->is_active;
# Create a specialized structure to create a rendering:
# [ timestamp, fn, preset, note, vel ]
my
@insrs
;
foreach
my
$item
(
@$script
)
{
my
$at
=
$item
->{at};
my
$to
=
$at
+
$item
->{
for
};
if
(
$item
->{in_seconds} )
{
$at
*=
$SR
;
$to
*=
$SR
;
}
push
@insrs
, [
int
$at
,
'note_on'
,
@$item
{
qw/preset note vel/
} ];
push
@insrs
, [
int
$to
,
'note_off'
,
@$item
{
qw/preset note vel/
} ];
}
@insrs
=
sort
{
$a
->[0] <=>
$b
->[0] }
@insrs
;
my
$current_ts
= 0;
my
$soundfont
=
$self
->soundfont;
my
$tsf
=
$soundfont
->_tsf;
foreach
my
$i
( 0 ..
$#insrs
)
{
my
(
$ts
,
$fn
,
@args
) = @{
$insrs
[
$i
] };
$result
.=
$tsf
->render(
$ts
-
$current_ts
);
$soundfont
->
$fn
(
@args
);
$current_ts
=
$ts
;
}
my
$cleanup_samples
= 4096;
for
( 1 .. 256 )
{
last
if
!
$tsf
->active_voices;
$result
.=
$tsf
->render(
$cleanup_samples
);
}
if
(
defined
$old_vol
)
{
$self
->soundfont->volume(
$old_vol
);
}
return
$result
;
}
sub
render_unpack
{
my
$self
=
shift
;
return
unpack
(
's<*'
,
$self
->render(
@_
) );
}
1;
__END__
=encoding utf-8
=head1 NAME
Audio::TinySoundFont::Builder - Construct SoundFont audio by description of play events.
=head1 SYNOPSIS
use Audio::TinySoundFont;
my $tsf = Audio::TinySoundFont->new('soundfont.sf2');
my $builder = $tsf->new_builder(
[
{
preset => 'Clarinet',
note => 59,
at => 0,
for => 2,
},
]
);
$builder->add(
[
{
preset => $preset,
note => 60,
at => 44100,
for => 44100 * 2,
in_seconds => 0,
},
]
);
my $samples = $builder->render;
=head1 DESCRIPTION
Audio::TinySoundFont::Builder can be used to automate the creation of audio
samples by describing the events by what times that notes should be turned on
and their duration.
=head1 CONSTRUCTOR
Audio::TinySoundFont::Builder is not constructed manually, instead you get an
instance from the L<preset|Audio::TinySoundFont/new_builder> method of
L<Audio::TinySoundFont>.
=head1 METHODS
=head2 soundfont
my $tsf = $builder->soundfont;
The L<Audio::TinySoundFont> object that this Preset object was created from.
=head2 play_script
my $script = $builder->play_script;
The current script that is ready to generate. The play_script is an ArrayRef
of HashRefs, each defining a single event. See L</add> for details of the
events.
NOTE: This structure should only be read, not modified. To make modifications,
use L</set> or L</add>.
=head2 clear
$builder->clear;
Clear the play_script.
=head2 add
$builder->add( [ @script_items ] );
Add a list of events to the play_script. Each event HashRef can have the
following keys, all other keys will be ignored.
=over
=item in_seconds
A boolean to indicate if C<at> and C<for> are expressed in seconds or samples.
If it is true, which is the default, C<at> and C<for> will be interpreted as
seconds.
=item at
A number that indicates when to trigger the L<note_on|Audio::TinySoundFont/note_on>
call. If C<in_seconds> is true, this number will be taken as seconds, otherwise
it will be number of samples. The default is to start at 0.
=item for
A number that indicates when to trigger the L<note_off|Audio::TinySoundFont/note_off>
call. If C<in_seconds> is true, this number will be taken as seconds, otherwise
it will be number of samples. The default is to end at 1, which if C<in_seconds>
is false, will probably not be what you want.
=item note
The MIDI note number to activate which will be passed to L<note_on|Audio::TinySoundFont/note_on>
and L<note_off|Audio::TinySoundFont/note_off>. The default is 60, which is
Middle C.
=item vel
The note velocity floating point number between 0.0 and 1.0 that will be passed
to L<note_on|Audio::TinySoundFont/note_on>. The default is 0.5.
=item preset
The preset to activate which will be passed to L<note_on|Audio::TinySoundFont/note_on>
and L<note_off|Audio::TinySoundFont/note_off>. It can be a preset name or a
L<Preset|Audio::TinySoundFont::Preset> object. The default is '', which is
likely not what you want because it is unlikely to exist.
=back
=head2 set
$builder->set( [ @script_items ] );
Replaces the play_script with a new list of events. See L</add> for the format
of the event HashRef.
=head2 render
=head2 render_unpack
my $samples = $builder->render( %options );
my @samples = $builder->render_unpack( %options );
Returns a string of 16-bit, little endian sound samples for the play_script
using TinySoundFont. The result can be unpacked using C<unpack("s<*")>
or you can call L</render_unpack> instead to get an array. It accepts an
options hash that allow you to customize how the sound should be generated.
This method cannot be used if the L</soundfont> has any
L<active voices|Audio::TinySoundFont/is_active> and will croak if that is the
case.
=over
=item volume
The general volume of the returned sample, expressed as a float between 0.0 and
1.0. If both L</volume> and L</db> are given, volume will be used.
=item db
The volume of the sample, expressed in as a logarithmic dB float between -100
and 0, with -100 being equivalent to silent and 0 being as loud as TinySoundFont
can go. If both L</volume> and L</db> are given, volume will be used.
=back
=head1 AUTHOR
Jon Gentle E<lt>cpan@atrodo.orgE<gt>
=head1 COPYRIGHT
Copyright 2020- Jon Gentle
=head1 LICENSE
This is free software. You may redistribute copies of it under the terms of the Artistic License 2 as published by The Perl Foundation.
=head1 SEE ALSO
L<Audio::TinySoundFont>, L<TinySoundFont|https://github.com/schellingb/TinySoundFont>
=cut