sfzer 0.4 → 0.5.0

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/History.txt CHANGED
@@ -1,8 +1,33 @@
1
+ === 0.5 / 2009-02-17
2
+ * 3 Features:
3
+
4
+ * Added --padding flag to pad the ends of multisamples (default: 12).
5
+ * Added --transpose N flag to transpose sample values by N semitones.
6
+ (for fog: -t-12)
7
+ * Added rudimentary unique filename support.
8
+
9
+ * 1 Improvement:
10
+
11
+ * Smarter detection: It takes at least three regions to make a Multisample.
12
+
13
+ * 5 Fixes:
14
+
15
+ * Fixed a bug that prevented recursion below any directory with Multisamples.
16
+ * Corrected bug that would cause a crash on Multisamples with no names (test
17
+ added).
18
+ * Corrected bug that allowed "-" and "_" at the end of Multisample names
19
+ (test added).
20
+ * Corrected bug that did not recognize lowercase note names (test added).
21
+ * Removed premature numeric note detection, as numeric notes are currently not
22
+ supported.
23
+
24
+
1
25
  === 0.4.0 / 2009-02-08
2
26
 
3
27
  * 4 Improvements:
4
28
 
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).
29
+ * Smarter Multisample.get_xfade method: xfade in/outs are only calculated when
30
+ needed (i.e., for gaps between Samples that are over a semitone).
6
31
  * Introduced a better system for unique filenames when using the --top-dir option.
7
32
  * Text entered via --message now wraps every 80 lines.
8
33
  * Upgraded license to GPLv3.
data/Manifest.txt CHANGED
@@ -1,5 +1,6 @@
1
1
  History.txt
2
2
  Manifest.txt
3
+ Notes.txt
3
4
  README.txt
4
5
  COPYING
5
6
  Rakefile
@@ -12,3 +13,4 @@ lib/Multisample.rb
12
13
  test/test_sfzer.rb
13
14
  test/NamedSampleTest.rb
14
15
  test/MultisampleTest.rb
16
+ test/DirectoryProcessorTest.rb
data/Notes.txt ADDED
@@ -0,0 +1,35 @@
1
+ = Notes
2
+
3
+ == SFZ Client Errata
4
+ === Dimension Pro
5
+ When reading SFZ, Dimension Pro in Windows cannot load samples referenced by
6
+ absolute path:
7
+
8
+ ==== SFZ snippet:
9
+ <control>
10
+ default_path=C:\snd\samples\prog_bass
11
+ <region> sample=ProgK1Bass E0.wav pitch_keycenter=16
12
+ <region> sample=ProgK1Bass G0.wav pitch_keycenter=19
13
+
14
+ ==== Dimension Pro 1.2 sfzlog.txt error snippet:
15
+ c:\dev\sfzer\trunk\c:\snd\samples\prog_bass\progk1bass e0.wav not found or couldn't be loaded.
16
+ c:\dev\sfzer\trunk\c:\snd\samples\prog_bass\progk1bass g0.wav not found or couldn't be loaded.
17
+
18
+
19
+
20
+
21
+ === Rapture
22
+ Rapture in Windows has the opposite problem from Dimension. It can load via
23
+ absolute paths (the Dimension Pro snippet above will pass without
24
+ a murmur), but it wont't load samples referenced by relative paths:
25
+
26
+ ==== SFZ snippet:
27
+ <control>
28
+ default_path=..\..\..\snd\samples\prog_bass
29
+ <region> sample=ProgK1Bass E0.wav pitch_keycenter=16
30
+ <region> sample=ProgK1Bass G0.wav pitch_keycenter=19
31
+
32
+ ==== Rapture 1.1 sfzlog.txt error snippet:
33
+ File c:\dev\sfzer\trunk\\snd\samples\prog_bass\progk1bass e0.wav not found or couldn't be loaded.
34
+ File c:\dev\sfzer\trunk\\snd\samples\prog_bass\progk1bass g0.wav not found or couldn't be loaded.
35
+
data/README.txt CHANGED
@@ -1,8 +1,10 @@
1
1
  = SFZer
2
2
 
3
3
  * http://sfzer.rubyforge.org
4
+ * http://rubyforge.org/projects/sfzer/
4
5
  * http://christessmer.com
5
6
 
7
+
6
8
  == DESCRIPTION:
7
9
 
8
10
  SFZer recursively scans through directories of Multisamples (.wav, .aiff, .ogg,
@@ -16,13 +18,15 @@ etc) and automagically generates SFZ soundfonts from what it finds.
16
18
 
17
19
  == FEATURES/PROBLEMS:
18
20
 
19
- * Quickly converts entire directory trees of named samples into SFZ instruments.
21
+ * Quickly converts entire directories of named samples into SFZ instruments.
20
22
  * Creates quick keyboard mapping from sample file names.
21
23
  * Automatically computes xfades between adjacent samples over a semitone apart.
22
24
 
23
25
  === Future Plans:
24
- * Future Music Sample name format
25
- * Numeric Notes
26
+ * Numeric Notes (some filenames use the numeric instead of chromatic key value)
27
+ * Future Music sample name format (sample order number + chromatic value)
28
+ * Specify multisamples to include or exclude by name or pattern.
29
+ * Specify minimum number of regions/samples in a multisample.
26
30
  * loop_mode=one-shot option for percussion
27
31
 
28
32
  === Ideas without much substance yet:
@@ -31,7 +35,6 @@ etc) and automagically generates SFZ soundfonts from what it finds.
31
35
  * (possibly) 2-n pass (as opposed to 1-pass) processing?
32
36
 
33
37
 
34
-
35
38
  == SYNOPSIS:
36
39
 
37
40
  % sfzer [options] directorie(s)
@@ -44,14 +47,14 @@ etc) and automagically generates SFZ soundfonts from what it finds.
44
47
 
45
48
  == INSTALL:
46
49
 
47
- * sudo gem install sfzer
50
+ % sudo gem install sfzer
48
51
 
49
52
 
50
53
  == LICENSE:
51
54
 
52
55
  (The GPL License, version 3)
53
56
 
54
- Copyright (c) 2008 Chris Tessmer
57
+ Copyright (c) 2008, 2009 Chris Tessmer
55
58
 
56
59
  This program is free software: you can redistribute it and/or modify
57
60
  it under the terms of the GNU General Public License as published by
data/bin/sfzer CHANGED
@@ -6,6 +6,7 @@
6
6
  #--
7
7
  # Ensure that the classpath loads regardless of environment
8
8
  $: << File.dirname(__FILE__) + '/../lib/'
9
+ puts $:
9
10
  #++
10
11
 
11
12
  require 'sfzer.rb'
@@ -14,6 +14,7 @@
14
14
  # along with SFZer. If not, see <http://www.gnu.org/licenses/>.
15
15
 
16
16
 
17
+
17
18
  # Processes one or more directories
18
19
  #
19
20
  # * Scan for Multisamples
@@ -22,7 +23,8 @@
22
23
  # TODO: Refactor this into an application class
23
24
  class DirectoryProcessor
24
25
 
25
- attr_accessor :dirs, :message, :mapping, :generate_in_top_dir
26
+ attr_accessor :dirs, :message, :mapping, :generate_in_top_dir,
27
+ :transpose, :padding
26
28
 
27
29
  # Instantiate the DirectoryProcessor
28
30
  def initialize
@@ -41,38 +43,50 @@ class DirectoryProcessor
41
43
  # If true (which is the default), all SFZ files are generated in the
42
44
  # directory the script was called from.
43
45
  @generate_in_top_dir = true
46
+
47
+ @transpose = 0
48
+ @padding = 12
44
49
  end
45
-
46
50
 
51
+
52
+
53
+ # Scan each directory for multisamples
54
+ def scan_dirs
55
+ multidirs = get_multisample_directories
56
+
57
+ # convert each multisample directory
58
+ pwd = Dir.getwd
59
+ if multidirs.size > 0
60
+
61
+ puts "converting..."
62
+ for dir in multidirs
63
+ Dir.chdir( pwd )
64
+ process_multisample_dir( dir, pwd )
65
+ end
66
+
67
+ else
68
+ raise NoMultisamplesInDirectoryException
69
+ end
70
+ end
71
+
72
+
73
+
47
74
  # starting from directory,
48
75
  def process_multisample_dir( path, top_path )
49
76
  named_hash = {}
50
- numbered_hash = Hash.new
77
+ #numbered_hash = Hash.new
51
78
 
52
- puts
79
+ puts
53
80
 
54
81
  Dir.chdir( path )
55
82
  Dir.glob( "*#{Sample.suffix_glob}" ).each{ |file|
56
83
 
57
84
  # Start tracking potential instrument names
58
85
  if NamedSample.sample?( file )
86
+
59
87
  # Sanitize hashkeys
60
88
  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
89
+ file_hashkey = sanitize_multisample_name( file_basename, path )
76
90
 
77
91
  #add Sample to a hashed Multisample
78
92
  multi = get_multisample( file_hashkey, path, named_hash )
@@ -80,82 +94,123 @@ class DirectoryProcessor
80
94
  named_hash[ file_hashkey ] = multi
81
95
  end
82
96
  }
83
- puts
84
97
 
85
98
  # for each hash entry, make an SFZ
86
99
  for key in named_hash.keys.sort
87
- puts "#{key}: "
88
- puts named_hash[ key ].to_sfz
89
-
100
+
101
+ filename = key
90
102
  # generate the SFZ file from the Multisample
103
+
91
104
  extra_name = ""
92
105
  if @generate_in_top_dir
93
106
  Dir.chdir( top_path )
94
107
  relative_path = path.gsub( /^\\/, '' ).gsub( /^\//, '' )
95
108
  named_hash[ key ].default_path = true
96
-
109
+
110
+ #filename = "#{top_path}/#{key}.sfz"
97
111
  # create a name based on subfolders to ensure uniqueness
98
112
  extra_name = relative_path.gsub( /\\/, "_" ).gsub( /\//, "_" ) + "_"
99
- extra_name.gsub!(/^(\.|_)*/, '')
113
+ extra_name.gsub!(/^(\.|-|_)+/, '')
114
+
115
+ filename = "#{extra_name}#{filename}"
116
+
117
+ end
118
+ filename = filename.gsub( ' ', '_')
119
+ filename = get_unique_filename( filename, path )
120
+
121
+ puts "#{key}: #{filename}"
122
+ multi = named_hash[ key ]
123
+ #puts multi.to_sfz
124
+ if( multi.valid? )
125
+ f = File.new( filename, "w" )
126
+ f.puts multi.to_sfz
127
+ f.close
128
+ else
129
+ puts "NOT VALID\n\n\n"
100
130
  end
101
131
 
132
+ end
133
+ end
134
+
135
+
136
+ def get_unique_filename( name, path )
137
+ result = "#{name}.sfz"
138
+ count = 0
139
+ while( FileTest.exists?( result ) )
140
+ result = "#{name}-#{count}.sfz"
141
+ count = count + 1
142
+ end
143
+ result
144
+ end
145
+
102
146
 
103
- f = File.new( "#{extra_name}#{key}.sfz", "w" )
104
- f.puts named_hash[ key ].to_sfz
105
- f.close
147
+ # Santizes a multisample
148
+ #
149
+ #
150
+ #
151
+ def sanitize_multisample_name( string, path )
152
+ # puts "sanitizing '#{string}':"
106
153
 
154
+ # FIXME: Potential bug: this assumes the first portion of the string
155
+ # is the most significant indicator
156
+ result = string.gsub( /\.#{Sample.suffix_regexp}$/, '' )
157
+ result = result.sub( NamedSample.regexp, '?' )
158
+ (result,) = result.split('?')
107
159
 
160
+ # sometimes, sample producers A
161
+ if result.nil?
162
+ result = File.basename( path )
108
163
  end
164
+
165
+ # otherwise, strip it of common paired symbols
166
+ result.sub!( /_\d\d(_)$/, '' ) # _01 + .wav and _01_.wav
167
+ result.sub!( /[(\[{_]$/, '' ) # blahblha( + A#1
168
+ result.sub!( /[-_]+$/, '' )
169
+ #result.sub!( /^[-_]+/, '' )
170
+ result.sub!( /\s*$/, '' )
171
+ result
109
172
  end
110
173
 
111
174
 
112
- # Scan each directory for multisamples
113
- def scan_dirs
114
- # tidy this junk up
115
- multidirs = []
116
175
 
117
- for dir in @dirs
118
- if File.exists?( dir ) and File.stat( dir ).directory? \
119
- and Multisample.dir_contains_multisamples?( dir )
120
176
 
121
- puts "\"#{ dir }\" has multisamples!"
122
- multidirs.push( dir )
123
177
 
124
- elsif File.exists?( dir ) and File.stat( dir ).directory?
178
+ protected
179
+
180
+ def get_multisample_directories
181
+ # tidy this junk up
182
+ multidirs = []
125
183
 
126
- Dir.foreach( dir ){ |file|
127
-
184
+ # push every directory w/multisamples onto the stack
185
+ for dir in @dirs
186
+ puts "scanning \"#{dir}\""
187
+
188
+ if File.exists?( dir ) and File.stat( dir ).directory?
189
+
190
+
191
+ #if File.exists?( dir ) and File.stat( dir ).directory?
192
+
193
+ Dir.foreach( dir ){ |file|
194
+ #puts "... #{file}"
128
195
  current_dir = dir + File::SEPARATOR + file
129
196
  if File.stat( current_dir ).directory? and file !~ /^\./
130
- puts "pushing \"#{current_dir}\""
197
+ #puts "...scanning \"#{current_dir}\""
131
198
  @dirs.push( current_dir )
132
- end
133
- }
134
-
135
- puts "pushed \"#{ dir }\""
199
+ end
200
+ }
201
+
202
+ if Multisample.dir_contains_multisamples?( dir )
203
+ puts "\"#{ dir }\" has multisamples!"
204
+ multidirs.push( dir )
205
+ end
206
+
207
+ # => puts "pushed \"#{ dir }\""
136
208
  else
137
209
  puts "\"#{ dir }\" is unusable."
138
210
  end
139
211
  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
212
+ multidirs
213
+ end
159
214
 
160
215
 
161
216
  # Returns the Multisample for the given hashkey.
@@ -168,7 +223,20 @@ class DirectoryProcessor
168
223
  multi = Multisample.new( path, file_hashkey )
169
224
  multi.mapping = @mapping
170
225
  multi.message = @message if( @message )
226
+ multi.transpose = @transpose
227
+ multi.padding = @padding
171
228
  end
172
229
  multi
173
230
  end
174
231
  end
232
+
233
+
234
+
235
+
236
+ # Exception to be raised in the case that no Multisample directories
237
+ # Have been found by DirectoryProcessor.
238
+ class NoMultisamplesInDirectoryException < Exception
239
+ def message
240
+ "No multisample directories were found in the given directories."
241
+ end
242
+ end
data/lib/Multisample.rb CHANGED
@@ -36,7 +36,7 @@ class Multisample
36
36
  # Maximum number of columns in SFZ comments before a line wrap
37
37
  LINE_LIMIT = 80
38
38
 
39
- attr_accessor :message, :mapping, :default_path
39
+ attr_accessor :message, :mapping, :default_path, :padding, :transpose
40
40
 
41
41
 
42
42
 
@@ -55,6 +55,9 @@ class Multisample
55
55
 
56
56
  # include a default path?
57
57
  @default_path = false
58
+
59
+ # Number of semitones to shift the note on the keyboard
60
+ @transpose = 0
58
61
  end
59
62
 
60
63
 
@@ -94,8 +97,8 @@ class Multisample
94
97
 
95
98
  if @mapping == XFADE
96
99
  result = result + get_xfade( sample, prev_sample, next_sample )
97
- elsif @mapping == STRICTKEYS
98
- result = result + "key=#{sample.value} "
100
+ elsif @mapping == STRICTKEYS
101
+ result = result + "key=#{ _transpose( sample.value )} "
99
102
  end
100
103
 
101
104
  # *****************************************
@@ -109,6 +112,15 @@ class Multisample
109
112
  end
110
113
 
111
114
 
115
+ # Do the samples within the Multisample justify its existence?
116
+ def valid?
117
+ values = {}
118
+ @samples.each{ |s|
119
+ values[s.value] = s
120
+ }
121
+ values.size >= 3
122
+ end
123
+
112
124
 
113
125
  # Sanitizes file path separators.
114
126
  #
@@ -123,7 +135,7 @@ class Multisample
123
135
  # Returns SFZ opcodes to specify pitch_keycenter and xfade parameters for a
124
136
  # region.
125
137
  def get_xfade( sample, prev_sample, next_sample, keycurve=false )
126
- result = "pitch_keycenter=#{sample.value} "
138
+
127
139
  xfin_lo = false
128
140
  xfin_hi = false
129
141
  xfout_lo = false
@@ -152,16 +164,25 @@ class Multisample
152
164
  xfout_lo = mid + (diff/2)
153
165
  hikey = xfout_hi
154
166
  elsif next_sample and ( next_sample.value - sample.value == 1 )
155
- hikey = sample.value
167
+ hikey = sample.value
156
168
  else
157
169
  hikey = sample.value + @padding
158
170
  end
159
171
 
160
172
  # depending on the distance between lokey and hikey, apply xfades
161
173
 
174
+ # Apply tranpose
175
+ value = _transpose( sample.value )
176
+ hikey = _transpose( hikey )
177
+ lokey = _transpose( lokey )
178
+ xfin_lokey = _transpose( xfin_lokey )
179
+ xfin_hikey = _transpose( xfin_hikey )
180
+ xfout_lokey = _transpose( xfout_lokey )
181
+ xfout_hikey = _transpose( xfout_hikey )
162
182
 
183
+ result = "pitch_keycenter=#{value} "
163
184
  if lokey == hikey and lokey == sample.value
164
- result = "key=#{sample.value}"
185
+ result = "key=#{value}"
165
186
  else
166
187
 
167
188
  if lokey
@@ -177,6 +198,7 @@ class Multisample
177
198
  end
178
199
  result = result + "hikey=#{hikey} "
179
200
  end
201
+
180
202
  end
181
203
 
182
204
  # add xf_keycurve, if applicable
@@ -190,19 +212,25 @@ class Multisample
190
212
  end
191
213
 
192
214
 
215
+ def _transpose( note )
216
+ if !note.nil? and note
217
+ note = note + @transpose
218
+ end
219
+ note
220
+ end
221
+
193
222
 
194
223
  # returns true if a filename can be understood as a multisample element
195
224
  def Multisample.multisample_filename?( name )
196
- types = Sample.suffix_regexp
225
+ #types = Sample.suffix_regexp
197
226
 
198
227
  # test for A(#)1.wav type
199
- #return true if name =~ /^.*([ABGCDEFG](#)?\d).*\.#{types}$/i
200
- return true if NamedSample.sample?( name )
228
+ return NamedSample.value( name ) if NamedSample.sample?( name )
201
229
 
202
230
  # 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
231
+ #if name =~ /^.*((0|1)?\d\d)\.#{types}$/i
232
+ # return true if (0..127).include?( $1.to_i )
233
+ #end
206
234
  false
207
235
  end
208
236
 
@@ -211,12 +239,19 @@ class Multisample
211
239
  # returns true if the directory contains files that might be multisamples
212
240
  def Multisample.dir_contains_multisamples?( dir )
213
241
  result = false
242
+ values = {}
243
+ @min_samples_per_multisample = 3
244
+
214
245
  Dir.foreach( dir ) do |file|
215
- if Multisample.multisample_filename?( file )
216
- result = true
217
- break
246
+ if v = Multisample.multisample_filename?( file )
247
+ values[v] = 1
248
+ if( values.size >= @min_samples_per_multisample )
249
+ result = true
250
+ break
251
+ end
218
252
  end
219
253
  end
254
+
220
255
  result
221
256
  end
222
257
 
data/lib/NamedSample.rb CHANGED
@@ -28,13 +28,14 @@ class NamedSample < Sample
28
28
 
29
29
  # returns true if the String name is a NamedSample.
30
30
  def NamedSample.sample?( name )
31
- name =~ /^.*(#{regexp}).*\.#{Sample.suffix_regexp}$/i
31
+ name =~ /^.*(#{NamedSample.regexp})/ &&
32
+ name =~ /.*\.#{Sample.suffix_regexp}$/i
32
33
  end
33
34
 
34
35
 
35
36
  # returns the RegExp used to test for a NamedSample.
36
37
  def NamedSample.regexp
37
- /[ABCDEFG](#)?\d/
38
+ /([A-Ga-g])(#)?(\d)/
38
39
  end
39
40
 
40
41
 
@@ -42,8 +43,9 @@ class NamedSample < Sample
42
43
  def NamedSample.value( string )
43
44
  result = false
44
45
 
45
- if( string =~ /([ABCDEFG])(#)?(\d)/i )
46
- notes = { 'C'=>0, 'D'=>2, 'E'=>4, 'F'=>5, 'G'=>7, 'A'=>9, 'B'=>11}
46
+ if( string =~ regexp )
47
+ notes = { 'C'=>0, 'D'=>2, 'E'=>4, 'F'=>5, 'G'=>7, 'A'=>9, 'B'=>11,
48
+ 'c'=>0, 'd'=>2, 'e'=>4, 'f'=>5, 'g'=>7, 'a'=>9, 'b'=>11}
47
49
  note = $1
48
50
  sharp = 0
49
51
  if $2 == "#" then
data/lib/Sample.rb CHANGED
@@ -13,6 +13,7 @@
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with SFZer. If not, see <http://www.gnu.org/licenses/>.
15
15
 
16
+
16
17
  # An SFZ Sample is a file that represents a single note.
17
18
  class Sample
18
19
  attr_reader :path, :sample
@@ -79,5 +80,5 @@ class Sample
79
80
  "#{@sample}"
80
81
  end
81
82
  end
82
-
83
+
83
84
  end
data/lib/sfzer.rb CHANGED
@@ -1,128 +1,157 @@
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
- # Program Requirements
18
- require 'optparse'
19
-
20
- require 'Sample'
21
- require 'NamedSample'
22
- require 'Multisample'
23
- require 'DirectoryProcessor'
24
-
25
- # Parses the command line options and sets the program into motion.
26
- class SFZer
27
-
28
- # Version information
29
- VERSION = '0.4'
30
- VERSION_DATE = "08 Feb 2009"
31
- URL = "http://sfzer.rubyforge.org"
32
- NAME = "SFZer"
33
- SEPARATOR = "\\"
34
-
35
- #Exit codes
36
- EXITSTATUS_COMPLETE = 0
37
- EXITSTATUS_NO_MULTISAMPLES_FOUND = 1
38
- EXITSTATUS_ARG_ERROR = 2
39
- EXITSTATUS_USER_ABORT = 3
40
-
41
-
42
- # Execute the SFZer program
43
- def do( argv )
44
- sfzer = process_options( argv )
45
- sfzer.scan_dirs
46
- end
47
-
48
-
49
- protected
50
-
51
-
52
- # Processes each command line option and returns a configured Directory Processor.
53
- def process_options( argv )
54
- sfzer = DirectoryProcessor.new
55
- exit_now = false
56
-
57
- begin
58
- argv.options{ |opt|
59
-
60
- # help message banner and usage instructions
61
- opt.banner = "A script by Chris Tessmer that creates SFZ files from multisamples.\n"
62
- opt.banner += "version #{SFZer::VERSION}, #{SFZer::VERSION_DATE}.\n"
63
- opt.banner += "\n"
64
- opt.banner += "Usage: sfzer.rb [options] directorie(s)\n"
65
- opt.banner += "\n"
66
-
67
- # defining options
68
- opt.on( "Options:" )
69
- opt.on( "\n" )
70
- opt.on( "Mapping:" )
71
- opt.on( "--xfade", "-x", "Map crossfades between region keycenters *" ) {
72
- sfzer.mapping = Multisample::XFADE
73
- }
74
-
75
- opt.on( "--strictkeys", "-s", "Map regions on a 1:1 key:sample basis" ) {
76
- sfzer.mapping = Multisample::STRICTKEYS
77
- }
78
-
79
-
80
- opt.on( "\n" )
81
- opt.on( "File placement:" )
82
- opt.on( "--top-dir", "-t", "Create SFZ files in topmost directory *" ){
83
- sfzer.generate_in_top_dir = true
84
- }
85
-
86
- opt.on( "--each-dir", "-e", "Create each SFZ file with its own Samples"){
87
- sfzer.generate_in_top_dir = false
88
- }
89
-
90
- opt.on( "\n" )
91
- opt.on( "Decoration:" )
92
- opt.on( "--message 'TEXT'", "-m", "Add TEXT to each generated SFZ file header" ) {
93
- |text|
94
- sfzer.message = text
95
- }
96
-
97
- opt.on( "--help", "-h", "This text" ) {
98
- puts opt
99
- exit_now = true
100
- }
101
-
102
- opt.parse!
103
-
104
- # Exit with help message if no arguments are given
105
- if( argv.length == 0 )
106
- puts opt
107
- exit_now = true
108
- end
109
- }
110
- rescue Exception => e
111
- STDERR.puts "\n#{e.class}"
112
- STDERR.puts "\t#{e}\n"
113
- STDERR.puts "Since this messed up the arguments, I am ABORTING THE PROGRAM. Sorry.\n\n"
114
- exit EXITSTATUS_ARG_ERROR
115
- end
116
-
117
- # Handle graceful exit (couldn't put in opt block because of rescue clause)
118
- if exit_now
119
- exit EXITSTATUS_COMPLETE
120
- end
121
-
122
- argv.each{ |arg|
123
- sfzer.dirs.push( arg )
124
- }
125
-
126
- sfzer
127
- end
128
- end
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
+ # Program Requirements
18
+ require 'optparse'
19
+
20
+ require 'DirectoryProcessor'
21
+ require 'Sample'
22
+ require 'NamedSample'
23
+ require 'Multisample'
24
+
25
+
26
+ # Parses the command line options and sets the program into motion.
27
+ class SFZer
28
+
29
+ # Version information
30
+ VERSION = '0.5.0'
31
+ VERSION_DATE = "17 Feb 2009"
32
+ URL = "http://sfzer.rubyforge.org"
33
+ NAME = "SFZer"
34
+
35
+ # File separator for SFZ samples
36
+ SEPARATOR = "\\"
37
+
38
+ #Exit codes
39
+ EXITSTATUS_COMPLETE = 0
40
+ EXITSTATUS_NO_MULTISAMPLES_FOUND = 1
41
+ EXITSTATUS_ARG_ERROR = 2
42
+ # EXITSTATUS_USER_ABORT = 3
43
+ MAX_NOTE_SHIFT = 9 * 12
44
+
45
+ # Execute the SFZer program
46
+ def do( argv )
47
+ begin
48
+ sfzer = process_options( argv )
49
+ sfzer.scan_dirs
50
+ rescue NoMultisamplesInDirectoryException => e
51
+ STDERR.puts "ERROR: #{e.message}"
52
+ STDERR.puts " Quitting."
53
+ exit SFZer::EXITSTATUS_NO_MULTISAMPLES_FOUND
54
+ end
55
+ end
56
+
57
+
58
+ protected
59
+
60
+
61
+ # Processes each command line option and returns a configured Directory Processor.
62
+ def process_options( argv )
63
+ sfzer = DirectoryProcessor.new
64
+ exit_now = false
65
+
66
+ begin
67
+ argv.options{ |opt|
68
+
69
+ # help message banner and usage instructions
70
+ opt.banner = "A script by Chris Tessmer that creates SFZ files from multisamples.\n"
71
+ opt.banner += "version #{SFZer::VERSION}, #{SFZer::VERSION_DATE}.\n"
72
+ opt.banner += "\n"
73
+ opt.banner += "Usage: sfzer.rb [options] directorie(s)\n"
74
+ opt.banner += "\n"
75
+
76
+ # defining options
77
+ opt.on( "Options:" )
78
+ opt.on( "\n" )
79
+ opt.on( "Mapping:" )
80
+ opt.on( "--xfade", "-x", "Map crossfades between region keycenters *" ){
81
+ sfzer.mapping = Multisample::XFADE
82
+ }
83
+
84
+ opt.on( "--strictkeys", "-s", "Map regions on a 1:1 key:sample basis" ){
85
+ sfzer.mapping = Multisample::STRICTKEYS
86
+ }
87
+
88
+ opt.on( "--transpose NOTES", "-t", Integer,
89
+ "Remaps regions on the keyboard by NOTES"){ |n|
90
+
91
+ if n > MAX_NOTE_SHIFT or n < -MAX_NOTE_SHIFT
92
+ raise ArgumentError,
93
+ "Cannot shift octaves by greater than #{MAX_NOTE_SHIFT}."
94
+ end
95
+
96
+ sfzer.transpose = n
97
+ }
98
+
99
+ opt.on( "--padding NOTES", "-p", Integer,
100
+ "Pads the ends of each multisample by NOTES "){ |n|
101
+ if n < 0
102
+ raise ArgumentError,
103
+ "Cannot pad by a negative number}."
104
+ end
105
+ sfzer.padding = n
106
+ }
107
+
108
+ opt.on( "" )
109
+ opt.on( "File placement:" )
110
+ opt.on( "--top-dir", "-d", "Create SFZ files in current directory *" ){
111
+ sfzer.generate_in_top_dir = true
112
+ }
113
+
114
+ opt.on( "--each-dir", "-e", "Create each SFZ file with its own Samples"){
115
+ sfzer.generate_in_top_dir = false
116
+ }
117
+
118
+ opt.on( "\n" )
119
+ opt.on( "Decoration:" )
120
+ opt.on( "--message 'TEXT'", "-m",
121
+ "Add TEXT to each generated SFZ file header" ) {
122
+ |text|
123
+ sfzer.message = text
124
+ }
125
+
126
+ opt.on( "--help", "-h", "This text" ) {
127
+ puts opt
128
+ exit_now = true
129
+ }
130
+
131
+ opt.parse!
132
+
133
+ # Exit with help message if no arguments are given
134
+ if( argv.length == 0 )
135
+ puts opt
136
+ exit_now = true
137
+ end
138
+ }
139
+ rescue Exception => e
140
+ STDERR.puts "\n#{e.class}"
141
+ STDERR.puts "\t#{e}\n"
142
+ STDERR.puts "Since this messed up the arguments, I am ABORTING THE PROGRAM. Sorry.\n\n"
143
+ exit EXITSTATUS_ARG_ERROR
144
+ end
145
+
146
+ # Handle graceful exit (couldn't put in opt block because of rescue clause)
147
+ if exit_now
148
+ exit EXITSTATUS_COMPLETE
149
+ end
150
+
151
+ argv.each{ |arg|
152
+ sfzer.dirs.push( arg )
153
+ }
154
+
155
+ sfzer
156
+ end
157
+ end
@@ -0,0 +1,27 @@
1
+ class DirectoryProcessorTest < Test::Unit::TestCase
2
+ def name
3
+ "DirectoryProcessorTest tests"
4
+ end
5
+
6
+
7
+ def setup
8
+ @d = DirectoryProcessor.new
9
+ end
10
+
11
+ def test_for_hashkey_sanitization
12
+ names = [ ['MS_Cavern-C5.wav', 'path'],
13
+ ['A#4.ogg', 'C:\snd\Analog_Pad' ],
14
+ ['D#4.ogg', '/snd/Juno_Lead' ],
15
+ ['d#4.ogg', '/snd/Juno_Lead' ],
16
+ ]
17
+
18
+ names.each{ |name|
19
+ path = name[1]
20
+ name = name[0]
21
+ key = @d.sanitize_multisample_name( name, path )
22
+ assert( key !~ /[-_]$/ ,
23
+ "\"#{key}\" has a minus or underscore at the end.")
24
+ }
25
+ end
26
+
27
+ end
@@ -52,25 +52,25 @@ class MultisampleTest < Test::Unit::TestCase
52
52
 
53
53
 
54
54
 
55
- def test_simple_numeric_note_filenames
56
- (0..127).each do |i|
57
- # Note 001.wav
58
- n = sprintf "%03d", i
59
- Sample.suffixes do |s|
60
- note = "#{n}.#{s}"
61
- assert( Multisample.multisample_filename?( note ) ,
62
- "\"#{note}\" is NOT a valid 3-digit numeric note name.")
63
- end
64
-
65
- # Note 01.wav
66
- n = sprintf "%02d", i
67
- Sample.suffixes do |s|
68
- note = "#{n}.#{s}"
69
- assert( Multisample.multisample_filename?( note ) ,
70
- "\"#{note}\" is NOT a valid 2-to-3-digit numeric note name.")
71
- end
72
- end
73
- end
55
+ # def test_simple_numeric_note_filenames
56
+ # (0..127).each do |i|
57
+ # # Note 001.wav
58
+ # n = sprintf "%03d", i
59
+ # Sample.suffixes do |s|
60
+ # note = "#{n}.#{s}"
61
+ # assert( Multisample.multisample_filename?( note ) ,
62
+ # "\"#{note}\" is NOT a valid 3-digit numeric note name.")
63
+ # end
64
+ #
65
+ # # Note 01.wav
66
+ # n = sprintf "%02d", i
67
+ # Sample.suffixes do |s|
68
+ # note = "#{n}.#{s}"
69
+ # assert( Multisample.multisample_filename?( note ) ,
70
+ # "\"#{note}\" is NOT a valid 2-to-3-digit numeric note name.")
71
+ # end
72
+ # end
73
+ # end
74
74
 
75
75
 
76
76
 
@@ -78,8 +78,9 @@ class MultisampleTest < Test::Unit::TestCase
78
78
  # Names of real files that should succeed
79
79
  names = [ 'NRGK4ObBass F#1.wav',
80
80
  'OBX-SoundTrk_03B2.wav',
81
- 'Bass06_C2.wav',
81
+ 'Bass06_C2.wav',
82
82
  ]
83
+
83
84
  for name in names
84
85
  assert( Multisample.multisample_filename?( name ),
85
86
  "\"#{name}\"is NOT a valid multisample filename" )
@@ -89,7 +90,7 @@ class MultisampleTest < Test::Unit::TestCase
89
90
  #badnames = ['119F#001.wav']
90
91
  #for name in badnames
91
92
  # assert( Multisample.multisample_filename?( name ) == false ,
92
- # "\"#{name}\" IS a valid multisample filename, but shouldn't be!" )
93
+ # "\"#{name}\" IS a valid multisample filename, but shouldn't be!" )
93
94
  #end
94
95
  end
95
96
 
@@ -6,7 +6,7 @@ class NamedSampleTest < Test::Unit::TestCase
6
6
  def test_for_named_samples_files
7
7
  names = [
8
8
  'Bassy01(C3).wav',
9
- 'D#2.wav',
9
+ 'd#2.wav',
10
10
  'Bass10_C1.wav'
11
11
  ]
12
12
 
@@ -14,6 +14,16 @@ class NamedSampleTest < Test::Unit::TestCase
14
14
  assert( NamedSample.sample?( name ) ,
15
15
  "\"#{name}\" is NOT a valid note name.")
16
16
  end
17
+
18
+ badnames = [
19
+ '090C001.wav',
20
+ ]
21
+
22
+ #badnames.each do |name|
23
+ #assert( !NamedSample.sample?( name ) ,
24
+ # "\"#{name}\" IS a valid note name (value = #{NamedSample.value( name )}).")
25
+ #end
26
+
17
27
  end
18
28
 
19
29
 
@@ -27,7 +37,7 @@ class NamedSampleTest < Test::Unit::TestCase
27
37
 
28
38
  sample_values.each do |sample, value|
29
39
  assert_equal( value, NamedSample.value( sample ),
30
- "The value of \"#{sample}\" is NOT \"#{value}\".")
40
+ "The value of \"#{sample}\" did NOT evaluate to \"#{value}\".")
31
41
  end
32
42
  end
33
43
 
@@ -52,4 +62,4 @@ class NamedSampleTest < Test::Unit::TestCase
52
62
  assert_operator c3, :==, c3o, "#{c3} = #{c3o}"
53
63
  end
54
64
 
55
- end
65
+ end
data/test/test_sfzer.rb CHANGED
@@ -9,4 +9,5 @@ require 'test/unit'
9
9
  require 'sfzer'
10
10
 
11
11
  require 'NamedSampleTest'
12
+ require 'DirectoryProcessorTest'
12
13
  require 'MultisampleTest'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sfzer
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.4"
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Tessmer
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-11 00:00:00 +00:00
12
+ date: 2009-02-17 00:00:00 +00:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -32,10 +32,12 @@ extensions: []
32
32
  extra_rdoc_files:
33
33
  - History.txt
34
34
  - Manifest.txt
35
+ - Notes.txt
35
36
  - README.txt
36
37
  files:
37
38
  - History.txt
38
39
  - Manifest.txt
40
+ - Notes.txt
39
41
  - README.txt
40
42
  - COPYING
41
43
  - Rakefile
@@ -48,6 +50,7 @@ files:
48
50
  - test/test_sfzer.rb
49
51
  - test/NamedSampleTest.rb
50
52
  - test/MultisampleTest.rb
53
+ - test/DirectoryProcessorTest.rb
51
54
  has_rdoc: true
52
55
  homepage: http://sfzer.rubyforge.org
53
56
  post_install_message: