Perlで効果音を生成する方法

Created by @techno_neko

## 自己紹介 - 仕事: .NETでGUIアプリ - Twitter: @techno_neko - 所属: Hokkaido.pm

ゲームの効果音を作るの
面倒じゃないですか?

iOSで動くゲームとか・・・

## 手順 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); } } ```
## ご静聴ありがとうございました!