## 自己紹介
- 仕事: .NETでGUIアプリ
- Twitter: @techno_neko
- 所属: Hokkaido.pm
## 手順
1. 適当なソフトで音を作る
1. 録音してWAVファイルにする
1. 加工する
1. 切り出す
1. アプリに組み込む
1. 気に食わない!
1. 最初に戻る
## 何が気に食わないのか?
- BGMに埋もれる(音程を変えたい)
- 音が長い/短い
- 音を変えたい
## それで解決?
- BGMができてない場合は?
- 録音しまくるの?
- 加工しまくるの?
- 切り出しまくるの?
## なんとかしました
https://github.com/techno-cat/p5-Cassis
## これは何?
短い音を生成するためのシンセ・モジュールです
Synthesizer Modules for generating a short sound.
## 何ができるの?
- WAVファイルの書き出し
- 単純な波形の生成
- モジュレーション
- フィルタリング
## 「Perl meets beats」との差分
- ADSRによるエンベロープ
- フィルタを実装(LPF, HPF, BPF, BEF)
- PortAudioで使用することを考慮(Audio::PortAudio)
- ノイズ周りも見直し
## 今後の課題
- Sampleフォルダ以下の充実
- プリセット的なものを用意する
- ランダムな効果音の生成
- 後はゲームを作るだけ!
## おまけ1
```
package MySynth;
use strict;
use warnings;
use Cassis;
sub new {
my $class = shift;
my %args = @_;
my $fs = ( exists $args{fs} ) ? $args{fs} : 44100;
bless {
samples => [],
fs => $fs
}, $class;
}
sub exec {
my $self = shift;
my %args = @_;
my $dco_class = 'Cassis::DCO::' . $args{type};
my $dco = $dco_class->new( fs => $self->{fs} );
my $amp = Cassis::Amp->new();
$amp->set_volume( $args{volume} );
my $wav = $amp->exec(
src => $dco->exec( num => $args{num} )
);
push @{$self->{samples}}, @{$wav};
}
sub write {
my $self = shift;
my %args = @_;
$args{sf} = $self->{sf};
$args{channels} = [ $self->{samples} ];
Cassis::File::write( %args );
}
package main;
use strict;
use warnings;
my @settings = (
{ type => 'Sin', volume => 1.0 },
{ type => 'Tri', volume => 1.0 },
{ type => 'Saw', volume => 0.4 },
{ type => 'Pulse', volume => 0.3 }
);
foreach ( @settings ) {
my %args = %{$_};
$args{num} = 44100; # 1sec
my $s = MySynth->new();
$s->exec( %args );
$s->write( file => lc($args{type}) . '.wav' );
}
```
## おまけ2
```
package MySynth;
use strict;
use warnings;
use Cassis;
sub new {
my $class = shift;
my %args = @_;
my $fs = ( exists $args{fs} ) ? $args{fs} : 44100;
bless {
samples => [],
fs => $fs
}, $class;
}
sub exec {
my $self = shift;
my %args = @_;
my $env = Cassis::EG->new(
fs => $self->{fs},
adsr => [ 0.01, 0.3, 0.05, 1.0 ],
);
my $env_out = $env->one_shot( gatetime => $args{gatetime} );
my $dco_class = 'Cassis::DCO::' . $args{type};
my $dco = $dco_class->new( fs => $self->{fs} );
my $amp = Cassis::Amp->new();
$amp->set_volume( $args{volume} );
my $wav = $amp->exec(
src => $dco->exec( num => scalar(@{$env_out}) ),
mod_volume => {
src => $env_out,
depth => 1.0
}
);
push @{$self->{samples}}, @{$wav};
}
sub write {
my $self = shift;
my %args = @_;
$args{sf} = $self->{sf};
$args{channels} = [ $self->{samples} ];
Cassis::File::write( %args );
}
package main;
use strict;
use warnings;
my @settings = (
{ type => 'Sin', volume => 1.0 },
{ type => 'Tri', volume => 1.0 },
{ type => 'Saw', volume => 0.4 },
{ type => 'Pulse', volume => 0.3 }
);
foreach ( @settings ) {
my %args = %{$_};
$args{gatetime} = 0.5; # (sec)
my $s = MySynth->new();
$s->exec( %args );
$s->write( file => lc($args{type}) . '_env.wav' );
}
```
## おまけ3
```
package MySynth;
use strict;
use warnings;
use Cassis;
sub new {
my $class = shift;
my %args = @_;
my $fs = ( exists $args{fs} ) ? $args{fs} : 44100;
bless {
samples => [],
fs => $fs
}, $class;
}
sub exec {
my $self = shift;
my %args = @_;
my $env = Cassis::EG->new(
fs => $self->{fs},
adsr => [ 0.01, 0.3, 0.05, 1.0 ],
);
my $env_out = $env->one_shot( gatetime => $args{gatetime} );
my $dco_class = 'Cassis::DCO::' . $args{type};
my $dco = $dco_class->new( fs => $self->{fs} );
my $lpf = Cassis::Iir2::LPF->new( cutoff => 0.05, q => 6.0 );
my $amp = Cassis::Amp->new();
$amp->set_volume( $args{volume} * 0.8);
my $wav = $amp->exec(
src => $lpf->exec(
src => $dco->exec( num => scalar(@{$env_out}) )
),
mod_volume => {
src => $env_out,
depth => 1.0
}
);
push @{$self->{samples}}, @{$wav};
}
sub write {
my $self = shift;
my %args = @_;
$args{sf} = $self->{sf};
$args{channels} = [ $self->{samples} ];
Cassis::File::write( %args );
}
package main;
use strict;
use warnings;
my @settings = (
{ type => 'Sin', volume => 1.0 },
{ type => 'Tri', volume => 1.0 },
{ type => 'Saw', volume => 0.4 },
{ type => 'Pulse', volume => 0.3 }
);
foreach ( @settings ) {
my %args = %{$_};
$args{gatetime} = 0.5; # (sec)
my $s = MySynth->new();
$s->exec( %args );
$s->write( file => lc($args{type}) . '_lpf.wav' );
}
```
## おまけ4
```
package MySynth;
use strict;
use warnings;
use Cassis;
sub new {
my $class = shift;
my %args = @_;
my $fs = ( exists $args{fs} ) ? $args{fs} : 44100;
bless {
samples => [],
fs => $fs
}, $class;
}
sub exec {
my $self = shift;
my %args = @_;
my $env = Cassis::EG->new(
fs => $self->{fs},
adsr => [ 0.01, 0.3, 0.05, 1.0 ],
);
my $env_out = $env->one_shot( gatetime => $args{gatetime} );
my $dco_class = 'Cassis::DCO::' . $args{type};
my $dco = $dco_class->new( fs => $self->{fs} );
my $lpf = Cassis::Iir2::LPF->new( cutoff => 0.05, q => 6.0 );
my $amp = Cassis::Amp->new();
$amp->set_volume( $args{volume} * 0.8);
my $wav = $amp->exec(
src => $lpf->exec(
src => $dco->exec( num => scalar(@{$env_out}) ),
mod_cutoff => {
src => $env_out,
depth => 0.3
}
),
mod_volume => {
src => $env_out,
depth => 1.0
}
);
push @{$self->{samples}}, @{$wav};
}
sub write {
my $self = shift;
my %args = @_;
$args{sf} = $self->{sf};
$args{channels} = [ $self->{samples} ];
Cassis::File::write( %args );
}
package main;
use strict;
use warnings;
my @settings = (
{ type => 'Sin', volume => 1.0 },
{ type => 'Tri', volume => 1.0 },
{ type => 'Saw', volume => 0.4 },
{ type => 'Pulse', volume => 0.3 }
);
foreach ( @settings ) {
my %args = %{$_};
$args{gatetime} = 0.5; # (sec)
my $s = MySynth->new();
$s->exec( %args );
$s->write( file => lc($args{type}) . '_mod_cutoff.wav' );
}
```
## おまけ5
```
package MySynth;
use strict;
use warnings;
use Cassis;
sub new {
my $class = shift;
my %args = @_;
my $fs = ( exists $args{fs} ) ? $args{fs} : 44100;
bless {
fs => $fs,
modules => {
DCO1 => Cassis::DCO::Saw->new( fs => $fs ),
DCO2 => Cassis::DCO::Tri->new( fs => $fs ),
LPF => Cassis::Iir2::LPF->new( cutoff => 0.01, q => 6.0 ),
LFO1 => Cassis::Osc::Pulse->new( fs => $fs ),
LFO2 => Cassis::Osc::Sin->new( fs => $fs ),
AMP => Cassis::Amp->new(),
EG1 => Cassis::EG->new(
fs => $fs,
adsr => [ 0.01, 0.1, 0.5, 0.5 ],
curve => 1.5
),
EG2 => Cassis::EG->new(
fs => $fs,
adsr => [ 0.01, 0.4, 0.2, 0.5 ],
curve => 1.5
)
}
}, $class;
}
sub note_on {
my $self = shift;
my %args = @_;
my ( $dco1, $dco2, $env1, $env2 ) = map {
$self->{modules}->{$_};
} qw(DCO1 DCO2 EG1 EG2);
$dco1->set_pitch( $args{pitch} - 0.01);
$dco2->set_pitch( $args{pitch} - 1.0 );
$env1->trigger( gatetime => $args{gatetime} ) if ( exists $args{gatetime} );
$env2->trigger( gatetime => $args{gatetime} ) if ( exists $args{gatetime} );
}
sub exec {
my $self = shift;
my %args = @_;
my ( $dco1, $dco2, $lpf, $lfo1, $lfo2, $amp, $env1, $env2 ) = map {
$self->{modules}->{$_};
} qw(DCO1 DCO2 LPF LFO1 LFO2 AMP EG1 EG2);
$lfo1->set_freq( 2000 );
$lfo2->set_freq( 2200 );
$amp->set_volume( 0.5 );
my $lfo1_out = $lfo1->exec( num => $args{num} );
my $lfo2_out = $lfo2->exec( num => $args{num} );
my $mixer_out = Cassis::Mixer::mix(
{
src => $dco1->exec( num => $args{num}, mod_pitch => {
src => $lfo1_out, depth => 0.2
} ),
volume => 0.6
},
{
src => $dco2->exec( num => $args{num}, mod_pitch => {
src => $lfo2_out, depth => 0.1
} ),
volume => 0.4
}
);
my $filter_out = $lpf->exec( src => $mixer_out, mod_cutoff => {
src => $env2->exec( num => $args{num} ), depth => 0.5
} );
return $amp->exec( src => $filter_out, mod_volume => {
src => $env1->exec( num => $args{num} ), depth => 1.0
} );
}
use strict;
use warnings;
use Audio::PortAudio;
use constant SAMPLING_RATE => 44100;
my %NOTE_TO_PITCH = (
C => -9 / 12,
D => -7 / 12,
E => -5 / 12,
F => -4 / 12,
G => -2 / 12,
A => 0 / 12,
B => 2 / 12
);
play( MySynth->new(fs => SAMPLING_RATE) );
sub note_to_pitch {
my $note = shift;
my $pitch = 0;
if ( $note =~ /^[A-G][+|-]?[\d]?/ ) {
my @tmp = split //, $note;
$pitch = $NOTE_TO_PITCH{ shift @tmp };
foreach my $ch (@tmp) {
if ( $ch eq '+' ) {
$pitch += ( 1 / 12 );
}
elsif ( $ch eq '-' ) {
$pitch -= ( 1 / 12 );
}
else {
$pitch += $ch;
}
}
}
else {
warn 'cannot parse => ', $note;
}
return $pitch;
}
sub play {
my $synth = shift;
my $sample_rate = SAMPLING_RATE;
my ( $frames_per_buffer, $stream_flags ) = ( 512, undef );
my $api = Audio::PortAudio::default_host_api();
printf STDERR "Going to play via %s\nCtrl+c to stop...", $api->name;
my $device = $api->default_output_device;
my $stream = $device->open_write_stream( {
channel_count => 1, # 1:mono, 2:stereo
sample_format => 'int16' # 'float32', 'int16', 'int32', 'int24', 'int8', 'uint8'
},
$sample_rate,
$frames_per_buffer,
$stream_flags,
);
my $i = SAMPLING_RATE;
# Infinite loop...
while (1) {
my $wa = $stream->write_available;
$i += $wa;
if ( SAMPLING_RATE <= $i ) {
$i -= SAMPLING_RATE;
$synth->note_on(
pitch => 4.0,
gatetime => 0.5
);
}
my $wav = $synth->exec( num => $wa );
my @buffer_ary = map {
my $vol = $_ * 32767.0;
( $vol < -32767 ) ? -32767 : ((32767 < $vol) ? 32767 : $vol);
} @{$wav};
my $buffer = pack("s*", @buffer_ary);
$stream->write($buffer);
}
}
```
## おまけ6
```
package MySynth;
use strict;
use warnings;
use Cassis;
sub new {
my $class = shift;
my %args = @_;
my $fs = ( exists $args{fs} ) ? $args{fs} : 44100;
bless {
fs => $fs,
modules => {
DCO1 => Cassis::DCO::Saw->new( fs => $fs ),
DCO2 => Cassis::DCO::Tri->new( fs => $fs ),
LPF => Cassis::Iir2::LPF->new( cutoff => 0.01, q => 6.0 ),
LFO1 => Cassis::Osc::Pulse->new( fs => $fs ),
LFO2 => Cassis::Osc::Sin->new( fs => $fs ),
AMP => Cassis::Amp->new(),
EG1 => Cassis::EG->new(
fs => $fs,
adsr => [ 0.01, 0.05, 0.5, 0.1 ],
curve => 1.5
),
EG2 => Cassis::EG->new(
fs => $fs,
adsr => [ 0.01, 0.8, 0.05, 0.3 ],
curve => 1.5
)
}
}, $class;
}
sub note_on {
my $self = shift;
my %args = @_;
my ( $dco1, $dco2, $env1, $env2 ) = map {
$self->{modules}->{$_};
} qw(DCO1 DCO2 EG1 EG2);
$dco1->set_pitch( $args{pitch} + 1.0 -0.005 );
$dco2->set_pitch( $args{pitch} + 1.0 );
$env1->trigger( gatetime => $args{gatetime} ) if ( exists $args{gatetime} );
$env2->trigger( gatetime => $args{gatetime} ) if ( exists $args{gatetime} );
}
sub exec {
my $self = shift;
my %args = @_;
my ( $dco1, $dco2, $lpf, $lfo1, $lfo2, $amp, $env1, $env2 ) = map {
$self->{modules}->{$_};
} qw(DCO1 DCO2 LPF LFO1 LFO2 AMP EG1 EG2);
$lfo1->set_freq( 2000 );
$lfo2->set_freq( 2200 );
$amp->set_volume( 0.5 );
my $lfo1_out = $lfo1->exec( num => $args{num} );
my $lfo2_out = $lfo2->exec( num => $args{num} );
my $mixer_out = Cassis::Mixer::mix(
{
src => $dco1->exec( num => $args{num}, mod_pitch => {
src => $lfo1_out, depth => 0.2
} ),
volume => 0.6
},
{
src => $dco2->exec( num => $args{num}, mod_pitch => {
src => $lfo2_out, depth => 0.1
} ),
volume => 0.4
}
);
my $filter_out = $lpf->exec( src => $mixer_out, mod_cutoff => {
src => $env2->exec( num => $args{num} ), depth => 0.5
} );
return $amp->exec( src => $filter_out, mod_volume => {
src => $env1->exec( num => $args{num} ), depth => 1.0
} );
}
use strict;
use warnings;
use Audio::PortAudio;
use constant {
SAMPLING_RATE => 44100,
BEATS_RESOLUTION => 12
};
my %NOTE_TO_PITCH = (
C => -9 / 12,
D => -7 / 12,
E => -5 / 12,
F => -4 / 12,
G => -2 / 12,
A => 0 / 12,
B => 2 / 12
);
play( MySynth->new(fs => SAMPLING_RATE) );
sub note_to_pitch {
my $note = shift;
my $pitch = 0;
if ( $note =~ /^[A-G][+|-]?[\d]?/ ) {
my @tmp = split //, $note;
$pitch = $NOTE_TO_PITCH{ shift @tmp };
foreach my $ch (@tmp) {
if ( $ch eq '+' ) {
$pitch += ( 1 / 12 );
}
elsif ( $ch eq '-' ) {
$pitch -= ( 1 / 12 );
}
else {
$pitch += $ch;
}
}
}
else {
warn 'cannot parse => ', $note;
}
return $pitch;
}
sub play {
my $synth = shift;
my $sample_rate = SAMPLING_RATE;
my ( $frames_per_buffer, $stream_flags ) = ( 512, undef );
my $api = Audio::PortAudio::default_host_api();
printf STDERR "Going to play via %s\nCtrl+c to stop...", $api->name;
my $device = $api->default_output_device;
my $stream = $device->open_write_stream( {
channel_count => 1, # 1:mono, 2:stereo
sample_format => 'int16' # 'float32', 'int16', 'int32', 'int24', 'int8', 'uint8'
},
$sample_rate,
$frames_per_buffer,
$stream_flags,
);
my $bpm = 180;
my @pattern = (
[ 0, 0, 'D4', 0.1 ],
[ 0, 4, 'D4', 0.1 ],
[ 0, 8, 'D4', 0.1 ],
[ 1, 0, 'D4', 1.2 ],
[ 2, 0, 'C4', 1.1 ],
[ 3, 0, 'E4', 1.1 ],
[ 4, 0, 'D4', 6.0 ],
[ 999, 0, 'C4', 0x10 ] # 不正参照しないためだけのダミーデータ
);
my $pattern_length = 7;
# 1秒間に4分音符が鳴る間隔
my $interval = $sample_rate / ($bpm / 60.0); # beats per sec
my $i = 0;
my $counter = 0;
# Infinite loop...
while (1) {
my $wa = $stream->write_available;
$counter -= $wa;
if ( $counter < 0 ) {
$synth->note_on(
pitch => note_to_pitch( $pattern[$i]->[2] ),
gatetime => ( $pattern[$i]->[3] / BEATS_RESOLUTION )
);
if ( $pattern[$i+1]->[0] < 999 ) {
my $tmp = ($pattern[$i+1]->[0] + ($pattern[$i+1]->[1] / BEATS_RESOLUTION))
- ($pattern[$i+0]->[0] + ($pattern[$i+0]->[1] / BEATS_RESOLUTION));
$counter += ( $tmp * $interval );
$i++;
}
else {
my $tmp = $pattern_length
- ($pattern[$i+0]->[0] + ($pattern[$i+0]->[1] / BEATS_RESOLUTION));
$counter += ( $tmp * $interval );
$i = 0;
}
}
my $wav = $synth->exec( num => $wa );
my @buffer_ary = map {
my $vol = $_ * 32767.0;
( $vol < -32767 ) ? -32767 : ((32767 < $vol) ? 32767 : $vol);
} @{$wav};
my $buffer = pack("s*", @buffer_ary);
$stream->write($buffer);
}
}
```
## おまけ7
```
package MySynth;
use strict;
use warnings;
use Cassis;
sub new {
my $class = shift;
my %args = @_;
my $fs = ( exists $args{fs} ) ? $args{fs} : 44100;
bless {
fs => $fs,
modules => {
DCO1 => Cassis::DCO::Saw->new( fs => $fs ),
DCO2 => Cassis::DCO::Tri->new( fs => $fs ),
LPF => Cassis::Iir2::LPF->new( cutoff => 0.30, q => 6.0 ),
LFO1 => Cassis::Osc::Pulse->new( fs => $fs ),
LFO2 => Cassis::Osc::Sin->new( fs => $fs ),
AMP1 => Cassis::Amp->new(),
AMP2 => Cassis::Amp->new(),
EG1 => Cassis::EG->new(
fs => $fs,
adsr => [ 0.01, 0.05, 0.2, 0.05 ],
curve => 1.5
),
EG2 => Cassis::EG->new(
fs => $fs,
adsr => [ 0.01, 0.05, 0.05, 0.3 ],
curve => 1.5
)
}
}, $class;
}
sub note_on {
my $self = shift;
my %args = @_;
my ( $dco1, $dco2, $env1, $env2 ) = map {
$self->{modules}->{$_};
} qw(DCO1 DCO2 EG1 EG2);
$dco1->set_pitch( $args{pitch} + 1.01);
$dco2->set_pitch( $args{pitch} + 1.000 );
$env1->trigger( gatetime => $args{gatetime} ) if ( exists $args{gatetime} );
$env2->trigger( gatetime => $args{gatetime} ) if ( exists $args{gatetime} );
}
sub exec {
my $self = shift;
my %args = @_;
my ( $dco1, $dco2, $lpf, $lfo1, $lfo2, $amp1, $amp2, $env1, $env2 ) = map {
$self->{modules}->{$_};
} qw(DCO1 DCO2 LPF LFO1 LFO2 AMP1 AMP2 EG1 EG2);
$lfo1->set_freq( 1 );
$lfo2->set_freq( 1/15 );
$amp1->set_volume( 0.5 );
my $lfo1_out = $lfo1->exec( num => $args{num} );
my $lfo2_out = $lfo2->exec( num => $args{num} );
my $mixer_out = Cassis::Mixer::mix(
{
src => $dco1->exec( num => $args{num} ),
volume => 0.4
},
{
src => $dco2->exec( num => $args{num} ),
volume => 0.6
}
);
my $filter_out = $lpf->exec( src => $mixer_out, mod_cutoff => {
src => $amp2->exec(
src => $lfo2_out, mod_volume => {
src => $env2->exec( num => $args{num} ),
depth => 1.0
} ),
depth => -0.18
} );
return $amp1->exec( src => $filter_out, mod_volume => {
src => $env1->exec( num => $args{num} ), depth => 1.0
} );
}
use strict;
use warnings;
use Audio::PortAudio;
use constant {
SAMPLING_RATE => 44100,
BEATS_RESOLUTION => 12
};
my %NOTE_TO_PITCH = (
C => -9 / 12,
D => -7 / 12,
E => -5 / 12,
F => -4 / 12,
G => -2 / 12,
A => 0 / 12,
B => 2 / 12
);
play( MySynth->new(fs => SAMPLING_RATE) );
sub note_to_pitch {
my $note = shift;
my $pitch = 0;
if ( $note =~ /^[A-G][+|-]?[\d]?/ ) {
my @tmp = split //, $note;
$pitch = $NOTE_TO_PITCH{ shift @tmp };
foreach my $ch (@tmp) {
if ( $ch eq '+' ) {
$pitch += ( 1 / 12 );
}
elsif ( $ch eq '-' ) {
$pitch -= ( 1 / 12 );
}
else {
$pitch += $ch;
}
}
}
else {
warn 'cannot parse => ', $note;
}
return $pitch;
}
sub play {
my $synth = shift;
my $sample_rate = SAMPLING_RATE;
my ( $frames_per_buffer, $stream_flags ) = ( 512, undef );
my $api = Audio::PortAudio::default_host_api();
printf STDERR "Going to play via %s\nCtrl+c to stop...", $api->name;
my $device = $api->default_output_device;
my $stream = $device->open_write_stream( {
channel_count => 1, # 1:mono, 2:stereo
sample_format => 'int16' # 'float32', 'int16', 'int32', 'int24', 'int8', 'uint8'
},
$sample_rate,
$frames_per_buffer,
$stream_flags,
);
my $bpm = 138;
my @pattern = (
[ 'D2', 0.02 ],
[ 'D2', 0.02 ],
[ 'D3', 0.02 ],
[ 'D3', 0.02 ]
);
my $pattern_mask = scalar(@pattern) - 1;
# 1秒間に4分音符が鳴る間隔
my $interval = $sample_rate / ($bpm / 60.0); # beats per sec
my $i = 0;
my $counter = 0;
# Infinite loop...
while (1) {
my $wa = $stream->write_available;
$counter -= $wa;
if ( $counter < 0 ) {
if ( $pattern[$i]->[0] ) {
$synth->note_on(
pitch => note_to_pitch( $pattern[$i]->[0] ),
gatetime => ( $pattern[$i]->[1] )
);
}
$counter += ($interval / 4);
$i = ($i + 1) & $pattern_mask;
}
my $wav = $synth->exec( num => $wa );
my @buffer_ary = map {
my $vol = $_ * 32767.0;
( $vol < -32767 ) ? -32767 : ((32767 < $vol) ? 32767 : $vol);
} @{$wav};
my $buffer = pack("s*", @buffer_ary);
$stream->write($buffer);
}
}
```