sfzer 0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +674 -0
- data/History.txt +39 -0
- data/Manifest.txt +14 -0
- data/README.txt +67 -0
- data/Rakefile +18 -0
- data/bin/sfzer +13 -0
- data/lib/DirectoryProcessor.rb +174 -0
- data/lib/Multisample.rb +320 -0
- data/lib/NamedSample.rb +70 -0
- data/lib/Sample.rb +83 -0
- data/lib/sfzer.rb +128 -0
- data/test/MultisampleTest.rb +115 -0
- data/test/NamedSampleTest.rb +55 -0
- data/test/test_sfzer.rb +12 -0
- metadata +79 -0
data/History.txt
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
=== 0.4.0 / 2009-02-08
|
|
2
|
+
|
|
3
|
+
* 4 Improvements:
|
|
4
|
+
|
|
5
|
+
* Smarter Multisample.get_xfade method: xfade in/outs are only calculated when needed (i.e., for gaps between Samples that are over a semitone).
|
|
6
|
+
* Introduced a better system for unique filenames when using the --top-dir option.
|
|
7
|
+
* Text entered via --message now wraps every 80 lines.
|
|
8
|
+
* Upgraded license to GPLv3.
|
|
9
|
+
|
|
10
|
+
* 3 Fixes:
|
|
11
|
+
|
|
12
|
+
* Corrected @padding bug: @padding now only applies to the edge samples.
|
|
13
|
+
* Fixed file separator bug that created incompatible SFZ files when run from unix shells (Test added).
|
|
14
|
+
* Added non-intrusive path declarations to the sfzer executable and the project Rakefile for maximum compatibility.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
=== 0.3.0 / 2008-03-09
|
|
18
|
+
|
|
19
|
+
* 2 Features:
|
|
20
|
+
|
|
21
|
+
* Added @padding to extend the edges of keycentered Multisample regions.
|
|
22
|
+
* Supports relative paths to samples (so all SFZ files can be generated in the same, top-level directory.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
=== 0.2.0 / 2008-02-18
|
|
26
|
+
|
|
27
|
+
* 3 Features:
|
|
28
|
+
|
|
29
|
+
* xfade support.
|
|
30
|
+
* Introduced cli option handling.
|
|
31
|
+
* Introduced script exit codes.
|
|
32
|
+
|
|
33
|
+
* 1 Fix:
|
|
34
|
+
* Corrected note/octave mapping: c1 up through b1 == octave 1 (Test added).
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
=== 0.1.0 / 2008-02-16
|
|
38
|
+
|
|
39
|
+
* First working version.
|
data/Manifest.txt
ADDED
data/README.txt
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
= SFZer
|
|
2
|
+
|
|
3
|
+
* http://sfzer.rubyforge.org
|
|
4
|
+
* http://christessmer.com
|
|
5
|
+
|
|
6
|
+
== DESCRIPTION:
|
|
7
|
+
|
|
8
|
+
SFZer recursively scans through directories of Multisamples (.wav, .aiff, .ogg,
|
|
9
|
+
etc) and automagically generates SFZ soundfonts from what it finds.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
=== Related Projects:
|
|
13
|
+
* The *SFZ* format: http://www.cakewalk.com/DevXchange/sfz.asp
|
|
14
|
+
* The *makesfz.pl* perl scripts , by Peter L. Jones: http://www.drealm.info/sfz/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
== FEATURES/PROBLEMS:
|
|
18
|
+
|
|
19
|
+
* Quickly converts entire directory trees of named samples into SFZ instruments.
|
|
20
|
+
* Creates quick keyboard mapping from sample file names.
|
|
21
|
+
* Automatically computes xfades between adjacent samples over a semitone apart.
|
|
22
|
+
|
|
23
|
+
=== Future Plans:
|
|
24
|
+
* Future Music Sample name format
|
|
25
|
+
* Numeric Notes
|
|
26
|
+
* loop_mode=one-shot option for percussion
|
|
27
|
+
|
|
28
|
+
=== Ideas without much substance yet:
|
|
29
|
+
* Option to force SFZv1 and SFZv2 output (nothing requires this yet)
|
|
30
|
+
* Default effect/ CC controller options
|
|
31
|
+
* (possibly) 2-n pass (as opposed to 1-pass) processing?
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
== SYNOPSIS:
|
|
36
|
+
|
|
37
|
+
% sfzer [options] directorie(s)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
== REQUIREMENTS:
|
|
41
|
+
|
|
42
|
+
* Ruby
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
== INSTALL:
|
|
46
|
+
|
|
47
|
+
* sudo gem install sfzer
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
== LICENSE:
|
|
51
|
+
|
|
52
|
+
(The GPL License, version 3)
|
|
53
|
+
|
|
54
|
+
Copyright (c) 2008 Chris Tessmer
|
|
55
|
+
|
|
56
|
+
This program is free software: you can redistribute it and/or modify
|
|
57
|
+
it under the terms of the GNU General Public License as published by
|
|
58
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
59
|
+
(at your option) any later version.
|
|
60
|
+
|
|
61
|
+
This program is distributed in the hope that it will be useful,
|
|
62
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
63
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
64
|
+
GNU General Public License for more details.
|
|
65
|
+
|
|
66
|
+
You should have received a copy of the GNU General Public License
|
|
67
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- ruby -*-
|
|
2
|
+
|
|
3
|
+
# Add lib/ and test/ to the load paths
|
|
4
|
+
$: << "./lib"
|
|
5
|
+
$: << "./test"
|
|
6
|
+
|
|
7
|
+
require 'rubygems'
|
|
8
|
+
require 'hoe'
|
|
9
|
+
require './lib/sfzer.rb'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Hoe.new('sfzer', SFZer::VERSION) do |p|
|
|
13
|
+
# p.rubyforge_name = 'sfzerx' # if different than lowercase project name
|
|
14
|
+
p.developer('Chris Tessmer', 'http://christessmer.com')
|
|
15
|
+
p.remote_rdoc_dir = '' # Release to root
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# vim: syntax=Ruby
|
data/bin/sfzer
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/ruby
|
|
2
|
+
#
|
|
3
|
+
# SFZer, by Chris Tessmer ( http://christessmer.com )
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
#--
|
|
7
|
+
# Ensure that the classpath loads regardless of environment
|
|
8
|
+
$: << File.dirname(__FILE__) + '/../lib/'
|
|
9
|
+
#++
|
|
10
|
+
|
|
11
|
+
require 'sfzer.rb'
|
|
12
|
+
s = SFZer.new
|
|
13
|
+
s.do( ARGV )
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# This file is part of SFZer.
|
|
2
|
+
#
|
|
3
|
+
# SFZer is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# SFZer is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with SFZer. If not, see <http://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Processes one or more directories
|
|
18
|
+
#
|
|
19
|
+
# * Scan for Multisamples
|
|
20
|
+
# *
|
|
21
|
+
#
|
|
22
|
+
# TODO: Refactor this into an application class
|
|
23
|
+
class DirectoryProcessor
|
|
24
|
+
|
|
25
|
+
attr_accessor :dirs, :message, :mapping, :generate_in_top_dir
|
|
26
|
+
|
|
27
|
+
# Instantiate the DirectoryProcessor
|
|
28
|
+
def initialize
|
|
29
|
+
# root directories to look for samples under
|
|
30
|
+
@dirs = []
|
|
31
|
+
|
|
32
|
+
# optional message to place in the header of each SFZ file
|
|
33
|
+
@message = false
|
|
34
|
+
|
|
35
|
+
# keymapping setting, set to a Multisample contant.
|
|
36
|
+
# The currently supported values are:
|
|
37
|
+
# * Multisample::XFADE (default )
|
|
38
|
+
# * Multisample::STRICTKEYS
|
|
39
|
+
@mapping = Multisample::XFADE
|
|
40
|
+
|
|
41
|
+
# If true (which is the default), all SFZ files are generated in the
|
|
42
|
+
# directory the script was called from.
|
|
43
|
+
@generate_in_top_dir = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# starting from directory,
|
|
48
|
+
def process_multisample_dir( path, top_path )
|
|
49
|
+
named_hash = {}
|
|
50
|
+
numbered_hash = Hash.new
|
|
51
|
+
|
|
52
|
+
puts
|
|
53
|
+
|
|
54
|
+
Dir.chdir( path )
|
|
55
|
+
Dir.glob( "*#{Sample.suffix_glob}" ).each{ |file|
|
|
56
|
+
|
|
57
|
+
# Start tracking potential instrument names
|
|
58
|
+
if NamedSample.sample?( file )
|
|
59
|
+
# Sanitize hashkeys
|
|
60
|
+
file_basename = File.basename( file )
|
|
61
|
+
|
|
62
|
+
# FIXME: Potential bug: this assumes the first portion of the string is the most
|
|
63
|
+
# significant indicator
|
|
64
|
+
(file_hashkey, ) = file_basename.gsub( /\.#{Sample.suffix_regexp}$/, '' ).sub( NamedSample.regexp, '?' ).split('?')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# if there is no label for the multisample, name it after the directory it's in.
|
|
68
|
+
if file_hashkey.nil?
|
|
69
|
+
file_hashkey = File.basename( path )
|
|
70
|
+
else
|
|
71
|
+
# otherwise, strip it of common paired symbols
|
|
72
|
+
file_hashkey.sub!( /_\d\d(_)$/, '' )
|
|
73
|
+
file_hashkey.sub!( /[(\[{_]$/, '' )
|
|
74
|
+
file_hashkey.sub!( /\s*$/, '' )
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#add Sample to a hashed Multisample
|
|
78
|
+
multi = get_multisample( file_hashkey, path, named_hash )
|
|
79
|
+
multi.add( NamedSample.new( file_basename ) )
|
|
80
|
+
named_hash[ file_hashkey ] = multi
|
|
81
|
+
end
|
|
82
|
+
}
|
|
83
|
+
puts
|
|
84
|
+
|
|
85
|
+
# for each hash entry, make an SFZ
|
|
86
|
+
for key in named_hash.keys.sort
|
|
87
|
+
puts "#{key}: "
|
|
88
|
+
puts named_hash[ key ].to_sfz
|
|
89
|
+
|
|
90
|
+
# generate the SFZ file from the Multisample
|
|
91
|
+
extra_name = ""
|
|
92
|
+
if @generate_in_top_dir
|
|
93
|
+
Dir.chdir( top_path )
|
|
94
|
+
relative_path = path.gsub( /^\\/, '' ).gsub( /^\//, '' )
|
|
95
|
+
named_hash[ key ].default_path = true
|
|
96
|
+
|
|
97
|
+
# create a name based on subfolders to ensure uniqueness
|
|
98
|
+
extra_name = relative_path.gsub( /\\/, "_" ).gsub( /\//, "_" ) + "_"
|
|
99
|
+
extra_name.gsub!(/^(\.|_)*/, '')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
f = File.new( "#{extra_name}#{key}.sfz", "w" )
|
|
104
|
+
f.puts named_hash[ key ].to_sfz
|
|
105
|
+
f.close
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Scan each directory for multisamples
|
|
113
|
+
def scan_dirs
|
|
114
|
+
# tidy this junk up
|
|
115
|
+
multidirs = []
|
|
116
|
+
|
|
117
|
+
for dir in @dirs
|
|
118
|
+
if File.exists?( dir ) and File.stat( dir ).directory? \
|
|
119
|
+
and Multisample.dir_contains_multisamples?( dir )
|
|
120
|
+
|
|
121
|
+
puts "\"#{ dir }\" has multisamples!"
|
|
122
|
+
multidirs.push( dir )
|
|
123
|
+
|
|
124
|
+
elsif File.exists?( dir ) and File.stat( dir ).directory?
|
|
125
|
+
|
|
126
|
+
Dir.foreach( dir ){ |file|
|
|
127
|
+
|
|
128
|
+
current_dir = dir + File::SEPARATOR + file
|
|
129
|
+
if File.stat( current_dir ).directory? and file !~ /^\./
|
|
130
|
+
puts "pushing \"#{current_dir}\""
|
|
131
|
+
@dirs.push( current_dir )
|
|
132
|
+
end
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
puts "pushed \"#{ dir }\""
|
|
136
|
+
else
|
|
137
|
+
puts "\"#{ dir }\" is unusable."
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
puts
|
|
142
|
+
pwd = Dir.getwd
|
|
143
|
+
if multidirs.size > 0
|
|
144
|
+
puts "converting..."
|
|
145
|
+
for dir in multidirs
|
|
146
|
+
Dir.chdir( pwd )
|
|
147
|
+
process_multisample_dir( dir, pwd )
|
|
148
|
+
end
|
|
149
|
+
else
|
|
150
|
+
STDERR.print "ERROR: No multisample directories were found in the given "
|
|
151
|
+
STDERR.puts "arguments."
|
|
152
|
+
STDERR.puts "Quitting."
|
|
153
|
+
exit SFZer::EXITSTATUS_NO_MULTISAMPLES_FOUND
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
protected
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# Returns the Multisample for the given hashkey.
|
|
162
|
+
# If no Multisample exists under that hashkey, a new Multisample is created.
|
|
163
|
+
def get_multisample( file_hashkey, path, named_hash )
|
|
164
|
+
multi = false
|
|
165
|
+
if named_hash.has_key?( file_hashkey )
|
|
166
|
+
multi = named_hash[ file_hashkey ]
|
|
167
|
+
else
|
|
168
|
+
multi = Multisample.new( path, file_hashkey )
|
|
169
|
+
multi.mapping = @mapping
|
|
170
|
+
multi.message = @message if( @message )
|
|
171
|
+
end
|
|
172
|
+
multi
|
|
173
|
+
end
|
|
174
|
+
end
|
data/lib/Multisample.rb
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# This file is part of SFZer.
|
|
2
|
+
#
|
|
3
|
+
# SFZer is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# SFZer is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with SFZer. If not, see <http://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# A Multisample is a collection of related Samples that can form an intrument.
|
|
19
|
+
class Multisample
|
|
20
|
+
# Region-mapping constants
|
|
21
|
+
|
|
22
|
+
# auto-xfade (usually best)
|
|
23
|
+
XFADE = 0
|
|
24
|
+
|
|
25
|
+
# 1:1 key map (usually worst)
|
|
26
|
+
STRICTKEYS = 1
|
|
27
|
+
|
|
28
|
+
# Keycurve constants
|
|
29
|
+
|
|
30
|
+
# Gain keycurve
|
|
31
|
+
KEYCURVE_GAIN = 3
|
|
32
|
+
|
|
33
|
+
# Power keycurve
|
|
34
|
+
KEYCURVE_POWER = 4
|
|
35
|
+
|
|
36
|
+
# Maximum number of columns in SFZ comments before a line wrap
|
|
37
|
+
LINE_LIMIT = 80
|
|
38
|
+
|
|
39
|
+
attr_accessor :message, :mapping, :default_path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# instantiate the Multisample
|
|
44
|
+
def initialize( path, name=false )
|
|
45
|
+
#TODO: Raise Exception if path not valid
|
|
46
|
+
@path=path
|
|
47
|
+
@samples=Array.new
|
|
48
|
+
@name = "Unnamed Multisample"
|
|
49
|
+
if name
|
|
50
|
+
@name=name.chomp.gsub(/^\s*/, '').gsub(/\s*$/, '')
|
|
51
|
+
end
|
|
52
|
+
@mapping = XFADE
|
|
53
|
+
@message = false # SFZ header message
|
|
54
|
+
@padding = 12 # padding in semitones
|
|
55
|
+
|
|
56
|
+
# include a default path?
|
|
57
|
+
@default_path = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# add a Sample to the Multisample
|
|
63
|
+
def add( sample )
|
|
64
|
+
@samples.push( sample )
|
|
65
|
+
@samples.sort!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Sreturn a String representation of the Multisample
|
|
71
|
+
def to_s
|
|
72
|
+
"Multisample \"#{@name}\" [#{@samples.size}]: " + @samples.sort.to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Returns a string of SFZ elements
|
|
78
|
+
def to_sfz
|
|
79
|
+
result = sfz_banner
|
|
80
|
+
|
|
81
|
+
if @default_path
|
|
82
|
+
sanitized_path = sanitize_file_path( @path )
|
|
83
|
+
result = result + "\n<control>\ndefault_path=#{sanitized_path}\\\n\n"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
maxfilenamesize = max_sample_filename_size
|
|
87
|
+
|
|
88
|
+
#result = result + "// Keyboard/Sample mappings:\n"
|
|
89
|
+
@samples.each_index{ |i|
|
|
90
|
+
sample=@samples[i]
|
|
91
|
+
prev_sample = get_previous_sample( i )
|
|
92
|
+
next_sample = get_next_sample( i )
|
|
93
|
+
result = result + "<region> sample=" + sanitize_file_path( sample.sample.ljust( maxfilenamesize + 1 ) )
|
|
94
|
+
|
|
95
|
+
if @mapping == XFADE
|
|
96
|
+
result = result + get_xfade( sample, prev_sample, next_sample )
|
|
97
|
+
elsif @mapping == STRICTKEYS
|
|
98
|
+
result = result + "key=#{sample.value} "
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# *****************************************
|
|
102
|
+
# TODO: PLACE FUTURE PER-SAMPLE LOGIC HERE
|
|
103
|
+
# *****************************************
|
|
104
|
+
|
|
105
|
+
result = result + "\n"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
result
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Sanitizes file path separators.
|
|
114
|
+
#
|
|
115
|
+
# Whilst every path separator within Ruby must be "/" (Unix), evey path
|
|
116
|
+
# separator within SFZ must be "\" (DOS).
|
|
117
|
+
def sanitize_file_path( f )
|
|
118
|
+
f.gsub( File::SEPARATOR, "\\" )
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Returns SFZ opcodes to specify pitch_keycenter and xfade parameters for a
|
|
124
|
+
# region.
|
|
125
|
+
def get_xfade( sample, prev_sample, next_sample, keycurve=false )
|
|
126
|
+
result = "pitch_keycenter=#{sample.value} "
|
|
127
|
+
xfin_lo = false
|
|
128
|
+
xfin_hi = false
|
|
129
|
+
xfout_lo = false
|
|
130
|
+
xfout_hi = false
|
|
131
|
+
lokey = false
|
|
132
|
+
hikey = false
|
|
133
|
+
|
|
134
|
+
# calculate xfade ins from previous sample
|
|
135
|
+
if prev_sample and ( prev_sample.value < (sample.value-1) )
|
|
136
|
+
mid = ( sample.value + prev_sample.value ) / 2
|
|
137
|
+
diff = sample.value - mid
|
|
138
|
+
xfin_lo = mid - (diff/2)
|
|
139
|
+
xfin_hi = mid + (diff/2)
|
|
140
|
+
lokey = xfin_lo
|
|
141
|
+
elsif prev_sample and ( sample.value - prev_sample.value == 1 )
|
|
142
|
+
lokey = sample.value
|
|
143
|
+
else
|
|
144
|
+
lokey = sample.value - @padding
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
#calculate xfade outs into next sample
|
|
148
|
+
if next_sample and ( next_sample.value > (sample.value+1) )
|
|
149
|
+
mid = ( sample.value + next_sample.value ) / 2
|
|
150
|
+
diff = sample.value - mid
|
|
151
|
+
xfout_hi = mid - (diff/2)
|
|
152
|
+
xfout_lo = mid + (diff/2)
|
|
153
|
+
hikey = xfout_hi
|
|
154
|
+
elsif next_sample and ( next_sample.value - sample.value == 1 )
|
|
155
|
+
hikey = sample.value
|
|
156
|
+
else
|
|
157
|
+
hikey = sample.value + @padding
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# depending on the distance between lokey and hikey, apply xfades
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if lokey == hikey and lokey == sample.value
|
|
164
|
+
result = "key=#{sample.value}"
|
|
165
|
+
else
|
|
166
|
+
|
|
167
|
+
if lokey
|
|
168
|
+
result = result + "lokey=#{lokey} "
|
|
169
|
+
if xfin_lo and xfin_hi
|
|
170
|
+
result = result + "xfin_lokey=#{xfin_lo} xfin_hikey=#{xfin_hi} "
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if hikey
|
|
175
|
+
if xfout_lo and xfout_hi
|
|
176
|
+
result = result + "xfout_lokey=#{xfout_lo} xfout_hikey=#{xfout_hi} "
|
|
177
|
+
end
|
|
178
|
+
result = result + "hikey=#{hikey} "
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# add xf_keycurve, if applicable
|
|
183
|
+
if keycurve == KEYCURVE_GAIN
|
|
184
|
+
result = result + "xf_keycurve=gain"
|
|
185
|
+
elsif keycurve == KEYCURVE_POWER
|
|
186
|
+
result = result + "xf_keycurve=power"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
result
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# returns true if a filename can be understood as a multisample element
|
|
195
|
+
def Multisample.multisample_filename?( name )
|
|
196
|
+
types = Sample.suffix_regexp
|
|
197
|
+
|
|
198
|
+
# test for A(#)1.wav type
|
|
199
|
+
#return true if name =~ /^.*([ABGCDEFG](#)?\d).*\.#{types}$/i
|
|
200
|
+
return true if NamedSample.sample?( name )
|
|
201
|
+
|
|
202
|
+
# test for Sample (0)00.wav type
|
|
203
|
+
if name =~ /^.*((0|1)?\d\d)\.#{types}$/i
|
|
204
|
+
return true if (0..127).include?( $1.to_i )
|
|
205
|
+
end
|
|
206
|
+
false
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# returns true if the directory contains files that might be multisamples
|
|
212
|
+
def Multisample.dir_contains_multisamples?( dir )
|
|
213
|
+
result = false
|
|
214
|
+
Dir.foreach( dir ) do |file|
|
|
215
|
+
if Multisample.multisample_filename?( file )
|
|
216
|
+
result = true
|
|
217
|
+
break
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
result
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
protected
|
|
226
|
+
|
|
227
|
+
# returns the maximum filename length of all Samples within the Multisample
|
|
228
|
+
def max_sample_filename_size
|
|
229
|
+
# Find the maximum length of sample filenames
|
|
230
|
+
# NOTE: this seems inefficient
|
|
231
|
+
maxfilenamesize = 0
|
|
232
|
+
@samples.each{ |sample|
|
|
233
|
+
if sample.sample.size > maxfilenamesize
|
|
234
|
+
maxfilenamesize = sample.sample.size
|
|
235
|
+
end
|
|
236
|
+
}
|
|
237
|
+
maxfilenamesize
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Returns the previous Sample in the Multisample (relative to a given Sample)
|
|
243
|
+
def get_previous_sample( i )
|
|
244
|
+
prev_sample=false
|
|
245
|
+
if i>0 and (not @samples[ i-1 ].nil?)
|
|
246
|
+
prev_sample = @samples[ i-1 ]
|
|
247
|
+
end
|
|
248
|
+
prev_sample
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Returns the next Sample in the Multisample (relative to a given Sample)
|
|
254
|
+
def get_next_sample( i )
|
|
255
|
+
next_sample=false
|
|
256
|
+
if not @samples[ i+1 ].nil?
|
|
257
|
+
next_sample = @samples[ i+1 ]
|
|
258
|
+
end
|
|
259
|
+
next_sample
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
public
|
|
264
|
+
|
|
265
|
+
# Returns the Multisample's SFZ header as a string.
|
|
266
|
+
def sfz_banner
|
|
267
|
+
separator = "//----------------------------------------------------------------------------"
|
|
268
|
+
result = ""
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if @name
|
|
272
|
+
result = result + "//\n"
|
|
273
|
+
result = result + "// "+ "*** SFZ Intrument: #{@name} ***".center(76) + "\n"
|
|
274
|
+
result = result + "//\n"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# add optional message to SFZ header
|
|
278
|
+
if @message and @message.chomp.size > 0
|
|
279
|
+
result = result + separator + "\n"
|
|
280
|
+
result = result + sfz_banner_message( @message )
|
|
281
|
+
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# add generator message to SFZ header
|
|
285
|
+
result = result + separator + "\n"
|
|
286
|
+
result = result + "// Generated on #{Time.now.strftime("%Y/%m/%d %I:%M%p") } "
|
|
287
|
+
result = result + "by #{SFZer::NAME} v#{SFZer::VERSION} (#{SFZer::URL})\n"
|
|
288
|
+
result = result + "#{separator}\n"
|
|
289
|
+
result
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Returns a formatted message formatted to the SFZ
|
|
293
|
+
def sfz_banner_message( fulltext )
|
|
294
|
+
limit = 76
|
|
295
|
+
result = ""
|
|
296
|
+
result = result + "//\n"
|
|
297
|
+
|
|
298
|
+
fulltext.each_line{ |text|
|
|
299
|
+
while text.length > limit
|
|
300
|
+
subtext = text.slice( 0 .. ( limit - 1 ) )
|
|
301
|
+
text = text.slice( limit .. text.length )
|
|
302
|
+
|
|
303
|
+
# don't cut off words; break at spaces
|
|
304
|
+
i = subtext.rindex( ' ' )
|
|
305
|
+
if i > ( limit / 2 )
|
|
306
|
+
nub = subtext.slice( i+1, subtext.length )
|
|
307
|
+
subtext = subtext.slice( 0..i )
|
|
308
|
+
text = nub + text
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
result = result + "// " + subtext.center( limit ) + "\n"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
result = result + "// " + text.center( limit ) + "\n"
|
|
315
|
+
}
|
|
316
|
+
result = result + "//\n"
|
|
317
|
+
|
|
318
|
+
result
|
|
319
|
+
end
|
|
320
|
+
end
|