-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathScala.sc
executable file
·382 lines (294 loc) · 10.3 KB
/
Scala.sc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// By Jascha Narveson and Charles Celeste Hutchins
Scala : Tuning {
/*@
shortDesc: open Scala files
longDesc: Reads Scala files and creates a Tuning basedon them. It can also generate a Scale that includes every interval in the Tuning. See Tuning for the principle methods.
More information about the Scala file format, and a link to the scale library, can be found at:
http://www.huygens-fokker.org/scala/scl_format.html
@*/
//
classvar <>defaultDir;
var <pitchesPerOctave;
*initClass {
defaultDir = PathName( Scala.filenameSymbol.asString ).pathOnly +/+ "data/";
}
*new { arg path;
/*@
desc: open a Scala file
path: path to the Scala file
ex:
a = Scala("slendro.scl");
@*/
^super.new.initOpen(pathname: path);
}
*open { arg path;
/*@
desc: a more intiutive syntax for opening a Scala file
@*/
^super.new.initOpen(pathname: path);
}
*at { arg name;
try {
^super.new.initOpen(name);
} {|e| ^Tuning.at(name) }
}
*forceUpdate{|dir,lastModifiedFile|
var file, data;
data=dir?defaultDir;
File.exists(data).not.if({
File.mkdir(data)
});
lastModifiedFile = lastModifiedFile ? (data +/+ "Last-Modified.txt");
//"forceUpdate".postln;
"cd % ; rm scales.zip ; curl -L https://huygens-fokker.org/docs/scales.zip --output scales.zip && unzip -aa -u scales.zip".format(data).unixCmd({
file = File.new(lastModifiedFile, "w");
file.write("%".format(Date.getDate.rawSeconds));
file.close;
});
}
*update {|dir|
var tmp, data, lastModifiedFile, lastModified, propFile, properties, propArr,
dateStr, dateArr, month, time, archiveModified, shouldUpdate, doUpdate, key;
tmp = Platform.defaultTempDir;
data=dir?defaultDir;
File.exists(data).not.if({
File.mkdir(data)
});
shouldUpdate = true;
doUpdate = {
Scala.forceUpdate(data, lastModifiedFile);
// var file;
// "doUpdate".postln;
// "curl http://huygens-fokker.org/docs/scales.zip > % && cd % && unzip -aa scales.zip".format(data+/+"scales.zip", data).unixCmd({
// file = File.new(lastModifiedFile, "w");
// file.write(Date.getDate.rawSeconds);
// file.close;
// });
};
doUpdate.();
// Checking if we should update doesn't work because of a 301 responce, so just update
// check if we should update
lastModifiedFile = data +/+ "Last-Modified.txt";
File.exists(lastModifiedFile).if({
lastModified = File.mtime(lastModifiedFile);
propFile = tmp +/+ "scala.txt";
// get just the header
//("curl -I -L https://huygens-fokker.org/docs/scales.zip -D %".format(propFile)).unixCmd({
("curl --get https://huygens-fokker.org/docs/scales.zip --output % -v -I".
format(propFile)).unixCmd({
properties = IdentityDictionary.new;
File.exists(propFile).if ({
propArr = FileReader.read(propFile, true, true, delimiter: $:);
propArr.do({|pair|
properties.put(pair[0].stripWhiteSpace.asSymbol, pair[1..]);
});
// now we've read the propFile, delete it
File.delete(propFile);
//properties.keys.postln;
key = "Last-Modified";
properties.keys.includes(key.asSymbol).not.if({
key = key.toLower;
});
// check the date in the header
dateArr = properties.at(key.asSymbol);
dateArr = dateArr.collect({|i| i.stripWhiteSpace});
dateArr = dateArr.collect({|i| i.split($ )}).flatten;
month = case
{ dateArr[2].compare("Jan", true)==0 } { 1 } // Jan is 1
{ dateArr[2].compare("Feb", true)==0 } { 2 } // Jan is 1
{ dateArr[2].compare("Mar", true)==0 } { 3 } // Jan is 1
{ dateArr[2].compare("Apr", true)==0 } { 4 } // Jan is 1
{ dateArr[2].compare("May", true)==0 } { 5 } // Jan is 1
{ dateArr[2].compare("Jun", true)==0 } { 6 } // Jan is 1
{ dateArr[2].compare("Jul", true)==0 } { 7 } // Jan is 1
{ dateArr[2].compare("Aug", true)==0 } { 8 } // Jan is 1
{ dateArr[2].compare("Sep", true)==0 } { 9 } // Jan is 1
{ dateArr[2].compare("Oct", true)==0 } { 10 } // Jan is 1
{ dateArr[2].compare("Nov", true)==0 } { 11 } // Jan is 1
{ dateArr[2].compare("Dec", true)==0 } { 12 };
//time = dateArr[4].split($:
//Thu, 07 Mar 2019 13:28:36 GMT
//year, month, day, hour, minute, second, dayOfWeek, rawSeconds;
archiveModified = Date(dateArr[3], month, dateArr[1], dateArr[4], dateArr[5], dateArr[6], 0, 0);
shouldUpdate = (archiveModified.rawSeconds > lastModified);
shouldUpdate.if({
File.delete(lastModifiedFile);
doUpdate.();
}, {
"Up to date.".postln;
});
})
});
}, { doUpdate.();})
}
*install {|dir|
Scala.update(dir);
}
initOpen{ arg pathname;
var path, file,lines,line, ratios, num, numerator, denominator, clean_line, line_finished;
tuning = [];
ratios = [];
path = pathname;
// First try to find the file
File.exists(pathname).not.if({
pathname.endsWith("scl").not.if({
pathname = pathname++".scl";
});
});
File.exists(pathname).not.if({
pathname = defaultDir +/+ pathname;
});
File.exists(pathname).not.if({
pathname = defaultDir +/+ "scl" +/+ path;
});
File.exists(pathname).not.if({
pathname.endsWith("scl").not.if({
pathname = pathname++".scl";
});
});
pathname.postln;
File.exists(pathname).not.if({
MethodError("File % not found.".format(path)).throw;
});
// read the file
// ok to read all the data into memory because it's small
file = File.open(pathname,"r"); // open the .scl file
lines = []; // start an array for the relevant lines of the file
line = 0;
while ({line != nil},
{
line=file.getLine;
if(line.isNil==false,
{
if(line.beginsWith("!").not,
// ignore commented .scl file lines (starting with "!")
{
lines = lines.add(line);
//line.postln;
} // look at non-commented lines
);
}
);
}
);
file.close;
// parse the results
name = lines.removeAt(0);
name = name.asString; // the first line will the the name
pitchesPerOctave = lines.removeAt(0).asInteger;
lines.do({|line| // each scale pitch will be either in ratio or cents notation
//line.post;
clean_line = String.new;
line_finished = false;
line.do({|char|
// only take numbers
(((char >= $0) && (char <= $9)) || ( char==$.) || (char==$/)).if({
line_finished.not.if({
clean_line = clean_line.add(char);
});
}, {
((char == $ )||(char == $\t )).if({
// ignore whitespace
}, {
// other stuff (letters, etc)
line_finished = true
});
});
});
clean_line.endsWith(".").if({
clean_line = clean_line.add($0); // 433. is a legal number
});
(clean_line.contains(".").not).if({
// the ratio case
//num = clean_line.interpret.ratiomidi;
#numerator,denominator = clean_line.split($/);
numerator.notNil.if({
// watch out for blank lines
denominator.isNil.if({ denominator =1 });
num = (numerator.asInteger / denominator.asInteger);
// get the ratio
ratios = ratios ++ num;
// put the midi stuff in the tuning
num = num.ratiomidi;
tuning = tuning ++ num;
});
},
{// the cents case
//"as float: ".post; i.asFloat.postln;
num = clean_line.asFloat;
num = num / 100;
tuning = tuning ++ num;
// also stuff this into ratios
ratios = ratios ++ num.midiratio;
});
});
tuning = tuning.addFirst(0); // the interval 1/1 is not explicitly stated in the .scl file
(tuning.size > pitchesPerOctave).if({
// 12.midiratio has rounding error, should be 2, is 1.999etc
num = tuning.pop;
(num == 12).if ({ // octave
octaveRatio = 2;
}, {
octaveRatio = ratios.pop;
});
}, {
octaveRatio=2
});
path = PathName.new(pathname);
all.put(path.fileNameWithoutExtension.asSymbol, this);
}
scale {
/*@
desc: Generates a Scale which contains as a degree every step in the tuning
ex:
a = Scala("slendro.scl");
b = a.scale;
@*/
var degrees;
degrees = Array.series(pitchesPerOctave, 0, 1);
^Scale(degrees, pitchesPerOctave, this, name);
}
}
/*
The scale archive, and rules for formatting .scl files, are described at
<http://www.xs4all.nl/~huygensf/scala/scl_format.html>
as of Nov.11, 2005:
� The files are human readable ASCII or 8-bit character text-files.
� The file type is .scl .
� There is one scale per file.
� Lines beginning with an exclamation mark are regarded as comments and are to be ignored.
� The first (non comment) line contains a short description of the scale, preferably not exceeding 80 characters, but longer lines are possible and should not give a read error. The description is only one line. If there is no description, there should be an empty line.
� The second line contains the number of notes. This number indicates the number of lines with pitch values that follow. In principle there is no upper limit to this, but it is allowed to reject files exceeding a certain size. The lower limit is 0, which is possible since degree 0 of 1/1 is implicit. Spaces before or after the number are allowed.
� After that come the pitch values, each on a separate line, either as a ratio or as a value in cents. If the value contains a period, it is a cents value, otherwise a ratio. Ratios are written with a slash, and only one. Integer values with no period or slash should be regarded as such, for example "2" should be taken as "2/1". The highest allowed numerator or denominator is 231-1 = 2147483647. Anything after a valid pitch value should be ignored. Space or horizontal tab characters are allowed and should be ignored. Negative ratios are meaningless and should give a read error. For a description of cents, go here.
� The first note of 1/1 or 0.0 cents is implicit and not in the files.
� Files for which Scala gives Error in file format are incorrectly formatted. They should give a read error and be rejected.
So these lines are all valid pitch lines:
81/64
408.0
408.
5
-5.0
10/20
100.0 cents
100.0 C#
5/4 E\
Here is an example of a valid file:
! meanquar.scl
!
1/4-comma meantone scale. Pietro Aaron's temperament (1523)
12
!
76.04900
193.15686
310.26471
5/4
503.42157
579.47057
696.57843
25/16
889.73529
1006.84314
1082.89214
2/1
An advise for writing a scale file: put the filename on the first line behind an exclamation mark. Then someone receiving the file and reading it knows a name under which to save it.
*/