use v5.14;
our $VERSION = '0.12';
use Carp;
use Moo;
use Types::Standard qw/ArrayRef HashRef InstanceOf/;
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