Task 643: .SCALA File Format
Task 643: .SCALA File Format
1. List of All Properties of the .SCALA File Format Intrinsic to Its File System
The .SCALA file format (also known as .SCL in standard implementations) is a plain-text, human-readable format for defining musical scales and tunings, primarily used in microtonal music software. It adheres to a line-based structure with specific rules for comments, whitespace, and data representation. The intrinsic properties, derived from the official specification, are as follows:
| Property | Type | Description |
|---|---|---|
| File Extension | String | .scala or .scl (case-insensitive; one scale per file). |
| Encoding | Character Set | ISO 8859-1 (Latin-1) or ASCII subset; 8-bit clean. |
| Line Terminator | String | Standard newline (LF or CRLF); lines are processed sequentially. |
| Comment Lines | Optional, Multiple | Lines starting with '!' (exclamation mark) are ignored entirely; may appear anywhere, including inline after data on pitch lines (post-value content is ignored). |
| Description | String, Single Line | First non-comment, non-empty line after trimming whitespace; provides a human-readable title or note about the scale (e.g., "1/4-comma meantone scale"). |
| Note Count | Integer | Second non-comment, non-empty line; specifies the number of scale degrees (N) following the base note (degree 0, implicitly 1/1 or 0 cents); range: 0 to theoretically unlimited (practical limit based on file size); parsed after trimming. |
| Pitch Values | Array of Strings (N elements) | Exactly N non-comment, non-empty lines following the note count; each represents a scale degree interval from the base: - Ratio Format: Numerator/denominator (e.g., "81/64", "5/1"); integers without '/' treated as "/1" (e.g., "5" = "5/1"). - Cents Format: Decimal with '.' (e.g., "76.04900"); distinguishes from ratios. - Trimmed; content after first whitespace ignored (e.g., labels like "E" or "cents"). - Negative values invalid (should trigger read error). - Supports up to 2^31-1 for numerators/denominators. |
| Base Interval | Implicit (Not Stored) | Degree 0 is always 1/1 (ratio) or 0.0 cents; not explicitly listed. |
| Whitespace Handling | Rule | Leading/trailing spaces/tabs ignored on all lines; no structural impact. |
| Error Conditions | Validation Rules | Invalid formats (e.g., malformed ratios, negative ratios, mismatched line count) trigger read errors; long lines or large N tolerated but may be practically limited. |
These properties ensure portability and simplicity, with no binary elements or complex headers.
2. Two Direct Download Links for Files of Format .SCALA
- Pythagorean tuning scale: https://raw.githubusercontent.com/cuthbertLab/music21/master/music21/scale/scala/scl/pythagorean.scl
- Just intonation scale: https://raw.githubusercontent.com/cuthbertLab/music21/master/music21/scale/scala/scl/just_intonation.scl
These are raw text files in the standard .SCL format, compatible with .SCALA usage.
3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop Parsing
The following is a self-contained HTML snippet with embedded JavaScript, suitable for embedding in a Ghost blog post (e.g., via the HTML card). It creates a drag-and-drop zone for .SCALA/.SCL files, parses the properties per the specification, and displays them in a formatted output div. No external dependencies are required.
Drag and drop a .SCALA or .SCL file here to parse its properties.
4. Python Class for .SCALA File Handling
The following Python class handles reading, writing, and printing .SCALA properties. It uses standard library functions for file I/O.
import os
class ScalaFile:
def __init__(self):
self.description = ""
self.note_count = 0
self.pitch_values = [] # List of dicts: {'type': 'ratio' or 'cents', 'value': str}
def read(self, filepath):
if not os.path.exists(filepath):
raise FileNotFoundError(f"File {filepath} not found.")
with open(filepath, 'r', encoding='iso-8859-1') as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
non_comments = [line for line in lines if not line.startswith('!')]
if len(non_comments) < 2:
raise ValueError("Invalid file: Insufficient non-comment lines.")
self.description = non_comments[0]
try:
self.note_count = int(non_comments[1])
except ValueError:
raise ValueError("Invalid note count.")
if self.note_count < 0 or len(non_comments) < 2 + self.note_count:
raise ValueError("Invalid pitch values or count.")
self.pitch_values = []
for i in range(2, 2 + self.note_count):
pitch_line = non_comments[i].split()[0] # First token
if '/' in pitch_line:
self.pitch_values.append({'type': 'ratio', 'value': pitch_line})
elif '.' in pitch_line:
self.pitch_values.append({'type': 'cents', 'value': pitch_line})
else:
self.pitch_values.append({'type': 'ratio', 'value': pitch_line + '/1'})
print("File read successfully.")
def write(self, filepath):
with open(filepath, 'w', encoding='iso-8859-1') as f:
f.write('! Generated .SCALA file\n')
f.write(f"{self.description}\n")
f.write(f"{self.note_count}\n")
for pitch in self.pitch_values:
f.write(f"{pitch['value']}\n")
print(f"File written to {filepath}.")
def print_properties(self):
print(f"Description: {self.description}")
print(f"Number of Notes: {self.note_count}")
print("Pitch Values:")
for i, pitch in enumerate(self.pitch_values, 1):
print(f" Degree {i}: {pitch['value']} ({pitch['type']})")
# Example usage:
# sf = ScalaFile()
# sf.read('example.scl')
# sf.print_properties()
# sf.write('output.scl')
5. Java Class for .SCALA File Handling
The following Java class uses java.io and java.util for file parsing, reading, writing, and console output. Compile with javac ScalaFile.java and run with java ScalaFile.
import java.io.*;
import java.util.*;
public class ScalaFile {
private String description = "";
private int noteCount = 0;
private List<Map<String, String>> pitchValues = new ArrayList<>(); // type -> value
public void read(String filepath) throws IOException {
File file = new File(filepath);
if (!file.exists()) {
throw new FileNotFoundException("File " + filepath + " not found.");
}
List<String> nonComments = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "ISO-8859-1"))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.length() > 0 && !line.startsWith("!")) {
nonComments.add(line);
}
}
}
if (nonComments.size() < 2) {
throw new IllegalArgumentException("Invalid file: Insufficient non-comment lines.");
}
description = nonComments.get(0);
try {
noteCount = Integer.parseInt(nonComments.get(1));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid note count.");
}
if (noteCount < 0 || nonComments.size() < 2 + noteCount) {
throw new IllegalArgumentException("Invalid pitch values or count.");
}
pitchValues.clear();
for (int i = 2; i < 2 + noteCount; i++) {
String pitchLine = nonComments.get(i).split("\\s+")[0];
Map<String, String> pitch = new HashMap<>();
if (pitchLine.contains("/")) {
pitch.put("type", "ratio");
pitch.put("value", pitchLine);
} else if (pitchLine.contains(".")) {
pitch.put("type", "cents");
pitch.put("value", pitchLine);
} else {
pitch.put("type", "ratio");
pitch.put("value", pitchLine + "/1");
}
pitchValues.add(pitch);
}
System.out.println("File read successfully.");
}
public void write(String filepath) throws IOException {
try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(filepath), "ISO-8859-1"))) {
pw.println("! Generated .SCALA file");
pw.println(description);
pw.println(noteCount);
for (Map<String, String> pitch : pitchValues) {
pw.println(pitch.get("value"));
}
}
System.out.println("File written to " + filepath + ".");
}
public void printProperties() {
System.out.println("Description: " + description);
System.out.println("Number of Notes: " + noteCount);
System.out.println("Pitch Values:");
for (int i = 0; i < pitchValues.size(); i++) {
Map<String, String> pitch = pitchValues.get(i);
System.out.println(" Degree " + (i + 1) + ": " + pitch.get("value") + " (" + pitch.get("type") + ")");
}
}
public static void main(String[] args) throws IOException {
if (args.length < 1) {
System.out.println("Usage: java ScalaFile <input.scl> [output.scl]");
return;
}
ScalaFile sf = new ScalaFile();
sf.read(args[0]);
sf.printProperties();
if (args.length > 1) {
sf.write(args[1]);
}
}
}
6. JavaScript Class for .SCALA File Handling (Node.js)
This Node.js class uses the fs module for synchronous file I/O. Run with node scalaFile.js example.scl.
const fs = require('fs');
class ScalaFile {
constructor() {
this.description = '';
this.noteCount = 0;
this.pitchValues = []; // [{type: 'ratio' or 'cents', value: str}]
}
read(filepath) {
if (!fs.existsSync(filepath)) {
throw new Error(`File ${filepath} not found.`);
}
const content = fs.readFileSync(filepath, 'iso-8859-1');
const lines = content.toString().split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
const nonComments = lines.filter(line => !line.startsWith('!'));
if (nonComments.length < 2) {
throw new Error('Invalid file: Insufficient non-comment lines.');
}
this.description = nonComments[0];
this.noteCount = parseInt(nonComments[1], 10);
if (isNaN(this.noteCount) || this.noteCount < 0 || nonComments.length < 2 + this.noteCount) {
throw new Error('Invalid pitch values or count.');
}
this.pitchValues = [];
for (let i = 2; i < 2 + this.noteCount; i++) {
const pitchLine = nonComments[i].split(/\s+/)[0];
let type, value;
if (pitchLine.includes('/')) {
type = 'ratio';
value = pitchLine;
} else if (pitchLine.includes('.')) {
type = 'cents';
value = pitchLine;
} else {
type = 'ratio';
value = pitchLine + '/1';
}
this.pitchValues.push({ type, value });
}
console.log('File read successfully.');
}
write(filepath) {
let output = '! Generated .SCALA file\n';
output += `${this.description}\n`;
output += `${this.noteCount}\n`;
this.pitchValues.forEach(pitch => {
output += `${pitch.value}\n`;
});
fs.writeFileSync(filepath, output, 'iso-8859-1');
console.log(`File written to ${filepath}.`);
}
printProperties() {
console.log(`Description: ${this.description}`);
console.log(`Number of Notes: ${this.noteCount}`);
console.log('Pitch Values:');
this.pitchValues.forEach((pitch, i) => {
console.log(` Degree ${i + 1}: ${pitch.value} (${pitch.type})`);
});
}
}
// Example usage:
// const sf = new ScalaFile();
// sf.read('example.scl');
// sf.printProperties();
// sf.write('output.scl');
7. C Implementation for .SCALA File Handling
C does not support classes natively; the following provides a struct with functions for reading, writing, and printing. Compile with gcc -o scala_file scala_file.c and run with ./scala_file example.scl [output.scl]. Uses standard C libraries.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MAX_LINES 10000
#define MAX_LINE_LEN 256
typedef struct {
char description[MAX_LINE_LEN];
int note_count;
char** pitch_values; // Array of strings; type inferred in print
int* pitch_types; // 0: ratio, 1: cents
int capacity;
} ScalaFile;
ScalaFile* scala_new() {
ScalaFile* sf = malloc(sizeof(ScalaFile));
sf->description[0] = '\0';
sf->note_count = 0;
sf->pitch_values = NULL;
sf->pitch_types = NULL;
sf->capacity = 0;
return sf;
}
void scala_free(ScalaFile* sf) {
if (sf) {
for (int i = 0; i < sf->note_count; i++) {
free(sf->pitch_values[i]);
}
free(sf->pitch_values);
free(sf->pitch_types);
free(sf);
}
}
int read_line(FILE* fp, char* line, int max_len) {
if (!fgets(line, max_len, fp)) return 0;
// Trim trailing newline and whitespace
int len = strlen(line);
if (len > 0 && line[len-1] == '\n') line[--len] = '\0';
while (len > 0 && isspace(line[len-1])) line[--len] = '\0';
// Trim leading whitespace
char* start = line;
while (isspace(*start)) start++;
memmove(line, start, strlen(start) + 1);
return strlen(line) > 0;
}
void scala_read(ScalaFile* sf, const char* filepath) {
FILE* fp = fopen(filepath, "r");
if (!fp) {
fprintf(stderr, "File %s not found.\n", filepath);
return;
}
char line[MAX_LINE_LEN];
char** non_comments = malloc(MAX_LINES * sizeof(char*));
int nc_count = 0;
while (read_line(fp, line, MAX_LINE_LEN)) {
if (!line[0] || line[0] == '!') continue;
non_comments[nc_count] = strdup(line);
nc_count++;
if (nc_count >= MAX_LINES) break;
}
fclose(fp);
if (nc_count < 2) {
fprintf(stderr, "Invalid file: Insufficient non-comment lines.\n");
goto cleanup;
}
strncpy(sf->description, non_comments[0], MAX_LINE_LEN - 1);
sf->description[MAX_LINE_LEN - 1] = '\0';
sf->note_count = atoi(non_comments[1]);
if (sf->note_count < 0 || nc_count < 2 + sf->note_count) {
fprintf(stderr, "Invalid pitch values or count.\n");
goto cleanup;
}
sf->capacity = sf->note_count;
sf->pitch_values = malloc(sf->note_count * sizeof(char*));
sf->pitch_types = malloc(sf->note_count * sizeof(int));
for (int i = 0; i < sf->note_count; i++) {
char* pitch_line = non_comments[2 + i];
char* space_pos = strchr(pitch_line, ' ');
if (space_pos) *space_pos = '\0'; // Truncate to first token
sf->pitch_values[i] = strdup(pitch_line);
if (strchr(pitch_line, '/')) {
sf->pitch_types[i] = 0; // ratio
} else if (strchr(pitch_line, '.')) {
sf->pitch_types[i] = 1; // cents
} else {
sf->pitch_types[i] = 0;
// Append /1 if needed
size_t len = strlen(pitch_line);
char* new_val = malloc(len + 3);
strcpy(new_val, pitch_line);
strcat(new_val, "/1");
free(sf->pitch_values[i]);
sf->pitch_values[i] = new_val;
}
}
printf("File read successfully.\n");
cleanup:
for (int i = 0; i < nc_count; i++) free(non_comments[i]);
free(non_comments);
}
void scala_write(ScalaFile* sf, const char* filepath) {
FILE* fp = fopen(filepath, "w");
if (!fp) {
fprintf(stderr, "Failed to write to %s.\n", filepath);
return;
}
fprintf(fp, "! Generated .SCALA file\n");
fprintf(fp, "%s\n", sf->description);
fprintf(fp, "%d\n", sf->note_count);
for (int i = 0; i < sf->note_count; i++) {
fprintf(fp, "%s\n", sf->pitch_values[i]);
}
fclose(fp);
printf("File written to %s.\n", filepath);
}
void scala_print_properties(ScalaFile* sf) {
printf("Description: %s\n", sf->description);
printf("Number of Notes: %d\n", sf->note_count);
printf("Pitch Values:\n");
for (int i = 0; i < sf->note_count; i++) {
const char* type_str = sf->pitch_types[i] ? "cents" : "ratio";
printf(" Degree %d: %s (%s)\n", i + 1, sf->pitch_values[i], type_str);
}
}
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s <input.scl> [output.scl]\n", argv[0]);
return 1;
}
ScalaFile* sf = scala_new();
scala_read(sf, argv[1]);
scala_print_properties(sf);
if (argc > 2) {
scala_write(sf, argv[2]);
}
scala_free(sf);
return 0;
}