Skip to content

Commit 9a6ba44

Browse files
committed
Add support for multiple soundfonts. Tweak whittington11study.
1 parent cc3a183 commit 9a6ba44

File tree

4 files changed

+131
-52
lines changed

4 files changed

+131
-52
lines changed

CITATION.cff

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ references:
6868
type: software
6969
url: https://ffmpeg.org/
7070

71+
- title: File::Format::RIFF
72+
type: software
73+
url: https://metacpan.org/dist/File-Format-RIFF
74+
7175
- title: Fluidsynth
7276
type: software
7377
url: https://www.fluidsynth.org/
@@ -189,7 +193,7 @@ references:
189193
date-accessed: 2021-08-20
190194
url: http://lilypond.org/doc/v2.22/Documentation/notation/percussion-notes
191195

192-
- title: Perl
196+
- title: Perl 5
193197
version: 5.14
194198
type: software
195199
url: https://www.perl.org/

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Please see the [sample config file](doc/chimerrc.example).
5555
Dependencies
5656
------------
5757
- [Pulseaudio](https://www.freedesktop.org/wiki/Software/PulseAudio/)-based Linux system
58-
- [Perl](https://www.perl.org/) 5 with Unicode support (version 5.14 or later)
58+
- [Perl 5](https://www.perl.org/) with Unicode support (version 5.14 or later)
5959
- [mpv](https://github.com/mpv-player/mpv)
6060
to play audio samples
6161
- [ffmpeg](https://ffmpeg.org/)
@@ -76,6 +76,7 @@ If using *samples* method:
7676
If using *synth* method:
7777
- [Fluidsynth](https://www.fluidsynth.org/)
7878
- [Expect::Simple](https://metacpan.org/pod/Expect::Simple)
79+
- [File::Format::RIFF](https://metacpan.org/dist/File-Format-RIFF)
7980
- The [Timbres of Heaven](http://midkar.com/soundfonts/) soundfont by Don Allen
8081

8182
Please

chimer

Lines changed: 123 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ use POSIX ":sys_wait_h"; # for nonblocking read
507507
use DateTime;
508508
use Time::HiRes; # overrides (qw) do not seem to work
509509
use Expect::Simple;
510+
use File::Format::RIFF;
510511

511512
use Exporter;
512513
require Exporter;
@@ -554,6 +555,7 @@ use vars qw( %expected_latency );
554555

555556
use vars qw( $wood_note );
556557
use vars qw( %instrument_name_to_channel_number );
558+
use vars qw( %instrument_name_to_mf_velocity );
557559
use vars qw( $fluidR3 );
558560
$fluidR3 = '/usr/share/sounds/sf2/FluidR3_GM.sf2';
559561

@@ -611,6 +613,7 @@ use vars qw( %alias_to_canonical_instrument );
611613
%alias_to_canonical_instrument = (
612614
'Keyboard' => 'Piano',
613615
'Carillon' => 'Bell',
616+
'Chimes' => 'Church Bells',
614617
);
615618

616619
use vars qw( %laisser_vibrer_p );
@@ -1788,10 +1791,10 @@ sub define_chimes () {
17881791
x = { \tempo 4 = 96 \time 3/4 }
17891792
y = { \set Staff.instrumentName = #"Organ" }
17901793
melodyonly = { <>-\fff }
1791-
melodywaccomp = { <>-\fff \set Staff.instrumentName = #"Piano" }
1792-
quarteraccomp = { <>-\p \y }
1793-
halfaccomp = { <>-\mp \y }
1794-
houraccomp = { <>-\mp \y }
1794+
melodywaccomp = { <>-\ff \set Staff.instrumentName = #"Carillon" }
1795+
quarteraccomp = { <>-\mp \y }
1796+
halfaccomp = { <>-\mf \y }
1797+
houraccomp = { <>-\f \y }
17951798
z = { <>-\mf }
17961799
A = \transpose c b { c'4 g' f' e' }
17971800
B = \transpose c b { c'4 g' f' e' g' d' }
@@ -2956,15 +2959,34 @@ sub send_midi_command ($) {
29562959
}
29572960
}
29582961
2959-
sub probe_synth_instruments () {
2960-
send_midi_command('inst 1');
2961-
my @instruments = $synth->before;
2962-
my %instruments = map { /^(\d+)-(\d+)\s+(.*)$/s? ($3 => [$1 + 0, $2 + 0], "$1-$2" => $3): () } split(/\r?\n/, $synth->before);
2962+
sub probe_synth_instruments ($) {
2963+
my($soundfonts) = @_;
2964+
my %instruments;
2965+
for (my $i = 0; $i < @$soundfonts; $i += 1) {
2966+
my $font = $i + 1;
2967+
send_midi_command(sprintf('inst %d', $font));
2968+
for my $s (split(/\r?\n/, $synth->before)) {
2969+
if ($s =~ /^(\d+)-(\d+)\s+(.*?)\r?$/s) {
2970+
if (!defined $instruments{$3}) {
2971+
$instruments{$3} = [$font, $1 + 0, $2 + 0, $soundfonts->[$i]->{'zero'}];
2972+
log_debug sprintf "instrument %s defined as %s", $3, Debug::cvs($instruments{$3}) if $debug > 1;
2973+
} else {
2974+
log_debug sprintf "instrument %s from soundfont %d ignored", $3, $font if $debug > 1;
2975+
}
2976+
if (!defined $instruments{"$1-$2"}) {
2977+
$instruments{"$1-$2"} = $3;
2978+
log_debug sprintf "bank %s-%s from soundfont %s defined as alias for instrument %s", $1, $2, $font, $3 if $debug > 1;
2979+
} else {
2980+
log_debug sprintf "alias %s-%s from soundfont %s ignored", $1, $2, $font if $debug > 1;
2981+
}
2982+
}
2983+
}
2984+
}
29632985
return wantarray? %instruments: \%instruments;
29642986
}
29652987
2966-
sub find_synth_instrument ($@) {
2967-
my($instruments, @preferences) = @_;
2988+
sub find_synth_instrument ($$@) {
2989+
my($instruments, $soundfonts, @preferences) = @_;
29682990
my $it;
29692991
for my $candidate (@preferences) {
29702992
$it = $instruments->{$candidate} if defined $instruments->{$candidate};
@@ -3014,7 +3036,7 @@ sub probe_score_instruments () {
30143036
sub connect_synth ($) {
30153037
my($id) = @_;
30163038
my $output = get_cache_pathname_for_event($id);
3017-
my($sf_path, $gain);
3039+
my($sf_path, $master_gain);
30183040
my $immediate_p = $id eq 'test';
30193041
if (!defined $synth && ($immediate_p || defined $output)) { # if we're connected don't try to create a second instance, it was disastrous
30203042
my %need_instrument = probe_score_instruments;
@@ -3027,31 +3049,66 @@ sub connect_synth ($) {
30273049
log_debug sprintf "needed instruments adjusted to %s", Debug::cvs [keys %need_instrument] if $debug;
30283050
}
30293051
3030-
# Figure out where our soundfont is
3031-
for my $key ('Timbres Of Heaven', 'FluidR3') {
3032-
if (defined $config->{'/'}->{$key}) {
3033-
foreach my $candidate (@{$config->{'/'}->{$key}}) {
3034-
$sf_path = $candidate if -f $candidate;
3035-
last if defined $sf_path;
3052+
# Figure out where our soundfont(s) is (are)
3053+
my $soundfonts;
3054+
if (defined $config->{'/'}->{'soundfont'}) {
3055+
my $prio = 0;
3056+
for my $input (@{$config->{'/'}->{'soundfont'}}) {
3057+
if (open(INPUT, '<', $input)) {
3058+
my $sf = new File::Format::RIFF;
3059+
$sf->read(*INPUT);
3060+
if ($sf->type eq 'sfbk') {
3061+
my $chunk = $sf->at(0); # XXX assume first chunk is INFO
3062+
if ($chunk->type eq 'INFO') {
3063+
my($inam, $iprd);
3064+
for (my $j = 0; $j < $chunk->numChunks; $j += 1) {
3065+
my $subchunk = $chunk->at($j);
3066+
$inam = $subchunk->data if $subchunk->id eq 'INAM';
3067+
$iprd = $subchunk->data if $subchunk->id eq 'IPRD';
3068+
}
3069+
my($type, $mf); # MIDI velocity that measures ~0 dB in japa when fluidsynth gain = 0.48 (MIDI velocity 64 supposedly = mf)
3070+
if ($iprd =~ /^Aegean Symphonic Orchestra\b/) { # INAM identifies as Musescore_General
3071+
($type, $mf) = ('Aegean Symphonic Orchestra', 64);
3072+
} elsif ($inam =~ /^Sonatina Symphonic Orchestra\b/) {
3073+
($type, $mf) = ('Sonatina Symphonic Orchestra', 48);
3074+
} elsif ($inam =~ /^Timbres Of Heaven\b/) {
3075+
($type, $mf) = ('Timbres Of Heaven', 32);
3076+
} elsif ($inam =~ /^Musescore_General\b/) {
3077+
($type, $mf) = ('Musescore_General', 64);
3078+
} elsif ($inam =~ /^Fluid R\d+\b/) {
3079+
($type, $mf) = ('Fluid', 64);
3080+
} else {
3081+
$mf = 64;
3082+
log_warning "$input: Unknown sf2 soundfont, assuming mf=$mf";
3083+
}
3084+
my $descriptor = {'path' => $input, 'priority' => $prio, 'mf' => $mf};
3085+
$descriptor->{'type'} = $type if defined $type;
3086+
push @$soundfonts, $descriptor;
3087+
} else {
3088+
log_warning "$input: INFO chunk not found at beginning of file";
3089+
}
3090+
} else {
3091+
log_warning "$input: Not an sf2 soundfont";
3092+
}
3093+
close INPUT;
3094+
} else {
3095+
log_warning "$input: $!";
30363096
}
3097+
$prio += 1;
30373098
}
3038-
if (defined $sf_path && $key eq 'Timbres Of Heaven') {
3039-
$gain = 0.2; # default gain
3040-
}
3041-
last if defined $sf_path;
30423099
}
3043-
$sf_path = $fluidR3 if !defined $sf_path; # hmm hope this works
3044-
$gain = 1 if !defined $gain;
3100+
die "No usable soundfont found\n" unless defined $soundfonts;
3101+
$master_gain = 0.48; # measured ~0 dB with Aegean in japa
30453102
30463103
log_comment sprintf('Connecting synth %s', $immediate_p? (@sinks? "to $sinks[0]": ''): "for write to $output") if $debug || $verbose_p;
30473104
set_sink $sinks[0] if $immediate_p && @sinks;
3048-
my @cmd = ('fluidsynth', '-g', $gain,
3105+
my @cmd = ('fluidsynth', '-g', $master_gain,
30493106
'--midi-channels', scalar(keys %need_instrument),
30503107
($immediate_p?
30513108
($use_jack_p? '-j': ('-a', 'pulseaudio')):
30523109
('-a', 'file',
30533110
'-o', "audio.file.name=$output")),
3054-
$sf_path);
3111+
map { $_->{'path'} } @$soundfonts);
30553112
30563113
log_command @cmd if $debug;
30573114
$synth = new Expect::Simple({
@@ -3061,55 +3118,70 @@ sub connect_synth ($) {
30613118
});
30623119
30633120
# Figure out what instruments to use
3064-
my $instruments = probe_synth_instruments;
3121+
my $instruments = probe_synth_instruments $soundfonts;
3122+
%instrument_name_to_channel_number = (); # need to reset this for each instance of fluidsynth
3123+
$wood_note = undef; # and this
30653124
for my $instrument ('Gong', 'Bell', 'Woodblock', 'drum kit', sort keys %need_instrument) { # handle hard-coded values 0-3 first
30663125
if (defined $need_instrument{$instrument} && !defined $instrument_name_to_channel_number{$instrument}) {
30673126
my $midi_instrument_id = scalar keys %instrument_name_to_channel_number;
30683127
my $spec;
30693128
if ($instrument eq 'Gong') {
3070-
$spec = find_synth_instrument($instruments, 'Gong!!!', 'Tubular Bells');
3129+
$spec = find_synth_instrument($instruments, $soundfonts, 'Gong!!!', 'Tubular Bells');
30713130
} elsif ($instrument eq 'Bell') {
3072-
# XXX Chimes from Sonatina seems to sound nicer than Carillon, but Sonatina doesn't have standard percussion so we can't really use it
3073-
$spec = find_synth_instrument($instruments, 'Chimes', 'Carillon', 'Tubular Bells');
3131+
$spec = find_synth_instrument($instruments, $soundfonts, 'Carillon', 'Tubular Bells (damp)', 'Tubular Bells');
30743132
} elsif ($instrument eq 'Church Bells') {
3075-
$spec = find_synth_instrument($instruments, 'Church Bell', 'Church Bells', 'Chimes');
3133+
# NOTE: instruments in Sonatina only have a range of [48, 84], Chimes also doesn't sound nicer (though it's a single strike)
3134+
$spec = find_synth_instrument($instruments, $soundfonts, 'Church Bell', 'Church Bells', 'Chimes');
30763135
} elsif ($instrument eq 'Woodblock') {
3077-
$spec = find_synth_instrument($instruments, 'Woodblock'); # 000-115
3136+
$spec = find_synth_instrument($instruments, $soundfonts, 'Woodblock'); # 000-115
30783137
} elsif ($instrument eq 'drum kit') {
3079-
$spec = find_synth_instrument($instruments, 'Jazz Drum Kit', 'Jazz', 'Orchestra Kit');
3138+
$spec = find_synth_instrument($instruments, $soundfonts, 'Jazz Drum Kit', 'Jazz', 'Orchestra Kit');
30803139
} elsif ($instrument eq 'Sine Wave') {
3081-
$spec = find_synth_instrument($instruments, 'Sine Wave', '1 kHz Test', '000-008'); # fallback to any celesta
3140+
$spec = find_synth_instrument($instruments, $soundfonts, 'Sine Wave', '1 kHz Test', '000-008'); # fallback to any celesta
30823141
} elsif ($instrument eq 'Music Box') {
3083-
$spec = find_synth_instrument($instruments, 'Music Box', '000-010'); # XXX this doesn't sound right
3142+
# Glockenspiel from Sonatina sounds the most realistic
3143+
$spec = find_synth_instrument($instruments, $soundfonts, 'Glockenspiel', 'Music Box', '000-010');
30843144
} elsif ($instrument eq 'Organ') {
3085-
$spec = find_synth_instrument($instruments, '000-019'); # church organ
3145+
$spec = find_synth_instrument($instruments, $soundfonts, '000-019'); # church organ
30863146
} elsif ($instrument eq 'Piano') {
3087-
$spec = find_synth_instrument($instruments, '000-000'); # piano
3147+
# NOTE: These are all 000-000 but they sound very different so there's a preferred order
3148+
$spec = find_synth_instrument($instruments, $soundfonts, 'Grand Piano C5', 'Concert Grand', 'Grand Piano', '000-000');
3149+
} elsif ($instrument eq 'Glockenspiel') {
3150+
$spec = find_synth_instrument($instruments, $soundfonts, '000-009'); # glockenspiel
30883151
} elsif ($instrument eq 'Flute') {
3089-
$spec = find_synth_instrument($instruments, '000-073'); # flute
3152+
$spec = find_synth_instrument($instruments, $soundfonts, '000-073'); # flute
30903153
} elsif ($instrument eq 'Recorder') {
3091-
$spec = find_synth_instrument($instruments, '000-074'); # recorder
3154+
$spec = find_synth_instrument($instruments, $soundfonts, '000-074'); # recorder
30923155
} elsif ($instrument eq 'Violin') {
3093-
$spec = find_synth_instrument($instruments, '000-040'); # violin
3156+
$spec = find_synth_instrument($instruments, $soundfonts, '000-040'); # violin
30943157
} elsif ($instrument eq 'Viola') {
3095-
$spec = find_synth_instrument($instruments, '000-041'); # viola
3158+
$spec = find_synth_instrument($instruments, $soundfonts, '000-041'); # viola
30963159
} elsif ($instrument eq 'Cello') {
3097-
$spec = find_synth_instrument($instruments, '000-042'); # cello
3160+
$spec = find_synth_instrument($instruments, $soundfonts, '000-042'); # cello
30983161
} elsif ($instrument eq 'Guitar') {
3099-
$spec = find_synth_instrument($instruments, '000-026'); # jazz guitar
3162+
$spec = find_synth_instrument($instruments, $soundfonts, '000-026'); # jazz guitar
31003163
} elsif ($instrument eq 'Bass Guitar') {
3101-
$spec = find_synth_instrument($instruments, '000-032'); # bass guitar
3164+
$spec = find_synth_instrument($instruments, $soundfonts, '000-032'); # bass guitar
3165+
} elsif ($instrument eq 'Harp') {
3166+
$spec = find_synth_instrument($instruments, $soundfonts, 'Concert Harp', 'Harp (sustain)', 'Clavinova Harp', 'Harp');
3167+
} elsif ($instrument eq 'Harpsichord') {
3168+
$spec = find_synth_instrument($instruments, $soundfonts, 'Harpsichord', "Don's Harpsichord");
31023169
} elsif ($instrument eq 'Guzheng') {
3103-
$spec = find_synth_instrument($instruments, 'Guzheng Harp'); # this is unrealistic; this is in reality a really quiet instrument
3170+
$spec = find_synth_instrument($instruments, $soundfonts, 'Guzheng Harp'); # this is unrealistic; this is in reality a really quiet instrument
31043171
} else {
31053172
die "$instrument: Unhandled instrument\n";
31063173
}
3107-
my($bank, $inst) = @$spec;
3174+
die "$instrument: Instrument not found\n" unless defined $spec;
3175+
# XXX You can't compensate for gain. First, there is no formula. Second, even if you can find one,
3176+
# XXX a loud note with low gain sounds different from a soft note with high gain,
3177+
# XXX probably because loud and soft notes use different samples
3178+
my($font, $bank, $inst, $zero) = @$spec;
31083179
if ($instrument eq 'Woodblock') {
31093180
$wood_note = (synth_instrument_name($instruments, $spec) =~ / Kit\b/ || $bank == 128)? 31: $note_to_midi_number{'d#5'};
31103181
}
3111-
send_midi_command "select $midi_instrument_id 1 $bank $inst";
3182+
send_midi_command "select $midi_instrument_id $font $bank $inst";
31123183
$instrument_name_to_channel_number{$instrument} = $midi_instrument_id;
3184+
$instrument_name_to_mf_velocity{$instrument} = $zero;
31133185
log_debug "instrument $instrument mapped to $midi_instrument_id" if $debug;
31143186
}
31153187
}
@@ -3345,28 +3417,30 @@ sub strike_tune_real ($@) {
33453417
my $semitones = 8;
33463418
my @midi_commands;
33473419
my $percussion_channel = $instrument_name_to_channel_number{'drum kit'};
3420+
my $percussion_mf = $instrument_name_to_mf_velocity{'drum kit'};
33483421
for my $voice (@tune) {
33493422
my $elapsed = 0;
33503423
my $t0 = current_time;
33513424
for my $spec (@$voice) {
33523425
my($note, $time, $velocity, $instrument, $flags) = @$spec;
33533426
$velocity = 127 unless defined $velocity; # NOTE backward compatibility
33543427
$instrument = canonical_instrument $instrument;
3428+
my $velocity_adjustment = defined $instrument_name_to_mf_velocity{$instrument}? $instrument_name_to_mf_velocity{$instrument}/64: 1;
33553429
my $t_i = current_time;
33563430
my $actual_time = $flags->{'actual-duration'};
33573431
my $note_length = $time * $beat_length;
33583432
my $effective_note_length = defined $actual_time? $actual_time * $beat_length: $note_length;
33593433
if (defined $flags && defined $flags->{'directive'}) {
3360-
push @midi_commands, [$elapsed, $flags->{'directive'}, $velocity];
3434+
push @midi_commands, [$elapsed, $flags->{'directive'}, $velocity * $velocity_adjustment];
33613435
} elsif (defined $percussion_note_to_midi_number{$note}) {
3362-
push @midi_commands, [$elapsed, sprintf 'noteon %d %d %d', $percussion_channel, $percussion_note_to_midi_number{$note}, $velocity];
3436+
push @midi_commands, [$elapsed, sprintf 'noteon %d %d %d', $percussion_channel, $percussion_note_to_midi_number{$note}, $velocity * $velocity_adjustment];
33633437
} elsif (exists $percussion_note_to_midi_number{$note}) { # valid percussion note but no MIDI equivalent
33643438
log_debug "percussion note $note has no MIDI equivalent"; # XXX
33653439
} elsif (defined $note_to_midi_number{$note}) {
33663440
my $channel = (defined $instrument_name_to_channel_number{$instrument})? $instrument_name_to_channel_number{$instrument}: 1;
33673441
my $note_number = $note_to_midi_number{$note};
33683442
my $laisser_vibrer_p = 1 if (defined $flags && defined $flags->{'laisser-vibrer'}) || (defined $laisser_vibrer_p{$instrument} && !defined $actual_time);
3369-
push @midi_commands, [$elapsed, sprintf 'noteon %d %d %d', $channel, $note_number, $velocity];
3443+
push @midi_commands, [$elapsed, sprintf 'noteon %d %d %d', $channel, $note_number, $velocity * $velocity_adjustment];
33703444
push @midi_commands, [$elapsed + $effective_note_length, sprintf 'noteoff %d %d', $channel, $note_number] unless $laisser_vibrer_p;
33713445
} elsif ($note ne 'r') {
33723446
log_debug "strike_tune: $note: \"else\" case reached";

doc/chimerrc.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ mode = carillon
2727
method = synth
2828

2929
; to have synth working correctly you MUST have Timbres Of Heaven and tell the script where to find it
30-
Timbres Of Heaven = /usr/local/share/sounds/sf2/Timbres Of Heaven (XGM) 3.94.sf2
30+
soundfont = /usr/local/share/sounds/sf2/Timbres Of Heaven (XGM) 3.94.sf2
3131

3232
; use Whittington chimes (8-bell version) instead of Westminster
3333
; you can get a list of valid options by running chimer with the -l (or --list) option

0 commit comments

Comments
 (0)