parent
43cccc563e
commit
718ba6f4f2
42 changed files with 4980 additions and 0 deletions
@ -0,0 +1 @@ |
||||
clinkster/Clinkster/* linguist-vendored |
@ -0,0 +1 @@ |
||||
*.o |
@ -0,0 +1,205 @@ |
||||
|
||||
CLINKSTER - a software synthesizer for 4k intros by Blueberry / Loonies |
||||
|
||||
|
||||
INTRODUCTION |
||||
|
||||
Clinkster is a software synthesizer (or synth, for short) designed for use in |
||||
extremely size-restricted executables, such a 4k intros. It has been under |
||||
development since 2008 and used in several 4k intros through the years. For |
||||
some background reading on the ideas behind it and its early history, refer |
||||
to the article from Zine #14 "Development Diary of Luminagia": |
||||
http://zine.bitfellas.org/article.php?zine=14&id=24 |
||||
|
||||
|
||||
FEATURES |
||||
|
||||
- VST instrument for Windows and Mac OS X, with source |
||||
- Simple interface: create your instruments using just 18 sliders |
||||
- Multi-layered, delicious, voluminous stereo sound based on phase modulation |
||||
- Unlimited number of tracks and unlimited polyphony per track |
||||
- Player source for Windows - integrate with C++ or asm |
||||
- Excludes unused features from player code to save space |
||||
- Easy-exe setup for creating executable music - no coding or additional |
||||
installation needed |
||||
- Many example songs and instruments included |
||||
|
||||
|
||||
OVERVIEW |
||||
|
||||
The Clinkster toolchain consists of three parts: |
||||
|
||||
1. A VST instrument to use when composing the music. This can in principle be |
||||
used with any VSTi host, but the rest of the toolchain is designed to be |
||||
used with Renoise, so if you want to use your music in an executable, you |
||||
will need to make your music in Renoise. |
||||
|
||||
2. A conversion script - RenoiseConvert.py - to convert a Renoise song using |
||||
Clinkster instruments into a .asm file containing the music data in the |
||||
format needed by the executable player. To run the conversion script, you |
||||
will need to install Python 2.x (where x >= 5). |
||||
|
||||
3. Player code to include in your intro. Two versions are provided: |
||||
clinkster.asm and clinkster_multithreaded.asm. The multithreaded version |
||||
is bigger but computes the music twice as fast (provided you have at least |
||||
two CPU cores). Both of these assume the converted music in music.asm. |
||||
The clinkster.h and clinkster.inc files contain definitions for using the |
||||
synth from C/C++ or asm, respectively. Refer to these files for detailed |
||||
usage information. |
||||
|
||||
|
||||
PARAMETERS |
||||
|
||||
Clinkster is designed to be used with the built-in VST parameter adjustment |
||||
GUI in Renoise. |
||||
|
||||
The numbers in parentheses after the value of each parameter indicates the |
||||
byte value that will be used to represent that parameter value in the |
||||
executable version of the music. Using the same byte values across parameters |
||||
and across instruments generally leads to a more compact representation of the |
||||
music. |
||||
|
||||
The sliders in Renoise have 101 different positions, and for most of the |
||||
parameters, these correspond to successive byte values. The exceptions are: |
||||
- B PitchD, M PitchD, IndexD, Attack, Decay, Release, Gain: One position for |
||||
every two values. |
||||
- B Pitch and M Pitch: 5 positions for every 12 values (one octave). |
||||
- RandomSeed: 128 values in total. |
||||
|
||||
If you want a value in between two slider-accessible values, click on the |
||||
value to access the internal representation (0% for minimum, 100% for maximum) |
||||
and modify it slightly. |
||||
|
||||
As for the meaning of the individual parameters, experimentation is the key. |
||||
Be sure to test your instrument in many different octaves. To get you started, |
||||
here is a brief description of the parameters: |
||||
|
||||
BaseWave, ModWave: Waveforms for the two oscillators. The ModWave modulates |
||||
the phase of the BaseWave. |
||||
B Detune, M Detune: Randomly varies the frequencies of the two oscillators |
||||
across layers and across the left and right channel. |
||||
B Pitch, M Pitch: Pitch the oscillator up or down relative to the played note. |
||||
B PitchD, M PitchD (pitch decay): Decay the pitch towards (or away from) the |
||||
played note. |
||||
Index: Strength of the phase modulation. |
||||
IndexSpr (index spread): Randomly varies the modulation strength across layers |
||||
and across the left and right channel. |
||||
IndexD (index decay): Decay the modulation strength towards (or away from) |
||||
zero. |
||||
Layers: Number of layers of sound to compute, with individual, random |
||||
variations controlled by the detune and index spread parameters. More |
||||
layers give a fatter, more chorus-like sound but also increases the |
||||
computational load of the instrument considerably. |
||||
RandomSeed: Seed used for the random variations across layers and channels. |
||||
Adjust this until you get a balanced, in-key sound. |
||||
Attack, Decay, Sustain, Release: Control the amplitude envelope of the sound. |
||||
Setting Sustain to a negative value creates a "double attack" where the |
||||
amplitude crosses zero during the decay. |
||||
Gain: Amplify the sound after the amplitude envelope and apply soft clipping. |
||||
Results in distorted or compressed sounds. |
||||
|
||||
Also take a look at the included example instruments and example songs for |
||||
inspiration. |
||||
|
||||
|
||||
USING THE VST |
||||
|
||||
In order to use the music in an executable, there are a couple of guidelines |
||||
you need to follow when creating your music in Renoise. |
||||
|
||||
You can use per-note velocity, but no other in-track effect commands. |
||||
|
||||
You can use any number of tracks and any number of note columns per track. |
||||
|
||||
You can adjust the volume and panning of tracks using the Volume/Panning Track |
||||
DSP, the post-DSP volume/panning, the mixer and the master volume. |
||||
|
||||
You can use the Delay effect (under Track DSPs) with these restrictions: |
||||
- The "L Feedb.", "R Feedb." and "Send" sliders must be at the same position. |
||||
- "Mute Src." must be off. |
||||
- No L/R Output Pan. |
||||
- If you use the Delay effect on multiple tracks, you must use the exact same |
||||
parameters on each track. |
||||
|
||||
To make it easier to control the volume and panning of a group of tracks, or |
||||
to use the same delay on multiple tracks, you can use a #Send DSP set to "Mute |
||||
Source" to route the sound from the track to a Send track. You can then set |
||||
the desired volume, panning and delay on the Send track. Only the final track |
||||
of a Send chain can use delay. |
||||
|
||||
The VST is designed to run in one instance per instrument and supports |
||||
unlimited polyphony and unlimited reuse of instruments across tracks. Note |
||||
however, that if you play notes using the same instrument in multiple tracks |
||||
at the same time, Renoise will play the notes using a single VST instance and |
||||
output the result in the track in which the last note was triggered. This |
||||
works fine as long as the tracks use the same volume, panning and delay. If |
||||
this is not the case, you will need to use separate copies of the instrument |
||||
for each different track. Otherwise, you will get clicks and generally a |
||||
different result from what you expected. |
||||
|
||||
In the pattern sequence matrix (available to the left of the pattern view), |
||||
you can mute individual tracks at specific positions. This muting is taken |
||||
into account by the converter. |
||||
|
||||
The VST works with any sample rate. To match the sound produced by the player, |
||||
use a sample rate of 44100Hz. If your music is too computationally heavy for |
||||
your CPU to cope with, you can lighten the load by lowering the sample rate |
||||
while composing, though that will of course have a detrimental effect on the |
||||
sound quality. |
||||
|
||||
|
||||
USING THE CONVERTER |
||||
|
||||
The RenoiseConvert.py script in the converter directory (or, equivalently, |
||||
RenoiseConvert.exe in easy_exe/tools) will convert a Renoise song using |
||||
Clinkster into a .asm file to use with the supplied player source. |
||||
|
||||
During conversion, each note column in a track will become a separate track in |
||||
the converted music. If you use more than one instrument inside a single note |
||||
column, that column will be split by the converter into one track per |
||||
instrument. Also, if you have used the delay effect, all tracks using delay |
||||
will be put before the tracks not using delay. |
||||
|
||||
The converter will print a list of the resulting tracks, along with the |
||||
original track / instrument combinations they correspond to. |
||||
|
||||
For each track, it will print a list of tone / length / velocity combinations |
||||
used in that track, in the form NOTE/VELOCITY:LENGTH(NUMBER) or |
||||
NOTE:LENGTH(NUMBER). For instance, C-4/6A:4(32) means that C-4 notes with |
||||
velocity 6A (hex) and length 4 occur 32 times in the track. If the velocity is |
||||
omitted, it is 127 (7F hex - maximum). The number of different combinations |
||||
used in the track has influence on the size of the resulting music (as well as |
||||
its precalculation time), so this list can be useful as a guide for optimizing |
||||
your music to take up less space. |
||||
|
||||
At the end, the converter prints a list of the optional features used in the |
||||
music. Each of these features has some cost in terms of the code size of the |
||||
player. The options are: |
||||
|
||||
SINE, SAWTOOTH, SQUARE, PARABOLA, TRIANGLE, NOISE: |
||||
The corresponding waveform is used in some instrument. |
||||
VELOCITY: One or more notes have velocity less than 127. |
||||
LONG_NOTES: One or more notes are longer than 127 rows. |
||||
DELAY: The delay device is used. |
||||
PANNING: One or more tracks have non-center panning. |
||||
INDEXDECAY: Some instrument has IndexDecay different from 0. |
||||
GAIN: Some instrument has Gain different from 1. |
||||
|
||||
|
||||
FEEDBACK |
||||
|
||||
I am always very interested in hearing about your adventures with Clinkster, |
||||
and to help out if you encounter problems. |
||||
|
||||
Send stories, comments, bug reports and questions to blueberry@loonies.dk or |
||||
post them to the Pouet forum at http://pouet.net/prod.php?which=61592 |
||||
|
||||
|
||||
ACKNOWLEDGEMENTS |
||||
|
||||
Thanks to all the people who have tried out this synth (as musician and/or |
||||
coder) and given good feedback for its development: Bod, Bstrr, Curt Cool, |
||||
Eladamri, El Blanco, Farfar, Garfferen, Hardy, Lemmus, Loaderror, Maytz, |
||||
Neoman, Psycho, Punqtured, Response, Seven, TheT, Xerxes, and the ones I |
||||
have forgotten. |
||||
|
@ -0,0 +1,665 @@ |
||||
#!/usr/bin/env python |
||||
|
||||
import sys |
||||
import zipfile |
||||
import XML |
||||
import struct |
||||
import ctypes |
||||
import math |
||||
import datetime |
||||
|
||||
class InputException(Exception): |
||||
def __init__(self, message): |
||||
Exception.__init__(self) |
||||
self.message = message |
||||
|
||||
|
||||
class Volume: |
||||
def __init__(self, left, right): |
||||
self.left = left |
||||
self.right = right |
||||
|
||||
def __mul__(self, other): |
||||
return Volume(self.left * other.left, self.right * other.right) |
||||
|
||||
def isPanned(self): |
||||
return self.left != self.right |
||||
|
||||
def makeVolume(xvolume): |
||||
v = float(xvolume) |
||||
return Volume(v,v) |
||||
|
||||
def makePanning(xpanning): |
||||
p = float(xpanning) |
||||
return Volume(math.sqrt(2.0 * (1.0 - p)), math.sqrt(2.0 * p)) |
||||
|
||||
|
||||
|
||||
class Instrument: |
||||
NAMES = ["bwave","mwave","bdetune","mdetune", |
||||
"bpitchs","bpitchd","mpitchs","mpitchd", |
||||
"index","indexspr","indexd","layers","randomseed", |
||||
"attack","decay","sustain","release","gain"] |
||||
QUAN = [(6,0),(6,0),(101,0),(101,0), |
||||
(241,120),(201,128),(241,120),(201,128), |
||||
(101,0),(101,0),(201,128),(51,0),(128,0), |
||||
(201,128),(201,128),(65,32),(201,128),(201,80)] |
||||
|
||||
def __init__(self, number, name, params): |
||||
names,quan = Instrument.NAMES,Instrument.QUAN |
||||
self.number = number |
||||
self.name = name |
||||
self.params = params |
||||
self.param_data = [int(p * (quan[i][0]-1) + 0.5) - quan[i][1] for i,p in enumerate(params)] |
||||
for i,p in enumerate(self.param_data): |
||||
self.__dict__[names[i]] = p |
||||
for mp,m in [("layers", 1), ("attack", -120), ("decay", -120), ("release", -120)]: |
||||
if self.__dict__[mp] < m: |
||||
self.__dict__[mp] = m |
||||
print " * Instrument '%s': %s clamped to %d" % (name, mp, m) |
||||
self.chopped = self.sustain == 0 |
||||
self.volume = Volume(1.0, 1.0) |
||||
|
||||
class Note: |
||||
NOTEBASES = { |
||||
"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11 |
||||
} |
||||
|
||||
NOTENAMES = { |
||||
0: "C-", 1: "C#", 2: "D-", 3: "D#", 4: "E-", 5: "F-", |
||||
6: "F#", 7: "G-", 8: "G#", 9: "A-", 10: "A#", 11: "B-" |
||||
} |
||||
|
||||
def __init__(self, line, songpos, pat, note, instr, velocity): |
||||
note = str(note) |
||||
self.line = int(line) |
||||
self.songpos = int(songpos) |
||||
self.pat = int(pat) |
||||
self.velocity = 127 if str(velocity) == "" or str(velocity) == ".." else int(str(velocity), 16) |
||||
if note == "OFF": |
||||
self.off = True |
||||
self.tone = None |
||||
self.instr = 0 |
||||
else: |
||||
self.off = False |
||||
octave = int(note[2]) |
||||
notebase = Note.NOTEBASES[note[0]] |
||||
sharp = int(note[1] == "#") |
||||
self.tone = octave * 12 + notebase + sharp |
||||
self.instr = int(str(instr), 16) |
||||
|
||||
|
||||
def instplugins(xinst): |
||||
xplugins = xinst.PluginProperties |
||||
if xplugins: |
||||
return xplugins |
||||
return xinst.PluginGenerator |
||||
|
||||
def isactive(xdevice): |
||||
if not xdevice: |
||||
return False |
||||
if xdevice.IsActive.Value: |
||||
return float(xdevice.IsActive.Value) != 0.0 |
||||
else: |
||||
return str(xdevice.IsActive) == "true" |
||||
|
||||
def notename(tone): |
||||
return Note.NOTENAMES[tone%12] + str(tone/12) |
||||
|
||||
def multibyte(v): |
||||
return [-1 - (v >> 8), v & 255] if v > 127 else [v] |
||||
|
||||
|
||||
class Track: |
||||
def __init__(self, number, name, notes, volume, instr, instruments): |
||||
self.number = number |
||||
self.name = name |
||||
self.notes = notes |
||||
self.volume = volume * instruments[instr].volume |
||||
self.instr = instr |
||||
self.notemap = dict() |
||||
self.tal_repr = dict() |
||||
|
||||
prev = None |
||||
for note in notes: |
||||
if prev is not None and not prev.off and prev.instr == instr: |
||||
length = 1 if instruments[self.instr].chopped else note.line - prev.line |
||||
if length < 0: |
||||
raise InputException("Track '%s' has reversed note order from %d to %d" % (name, prev.line, note.line)) |
||||
if prev.tone is None: |
||||
raise InputException("Track '%s' has a toneless note at %d" % (name, prev.line)) |
||||
tal = (prev.tone, length, prev.velocity) |
||||
self.notemap[prev] = tal |
||||
|
||||
prev = note |
||||
|
||||
if not prev.off and prev.instr == instr: |
||||
if instr.chopped: |
||||
tal = (prev.tone, 1) |
||||
self.notemap[prev] = tal |
||||
elif not prev.off: |
||||
raise InputException("Track '%s' is not terminated." % name) |
||||
|
||||
self.tals = sorted(set(self.notemap.values()), key = (lambda (t,l,v) : (t,v,-l))) |
||||
for i,tal in enumerate(self.tals): |
||||
if tal[0] is None: |
||||
raise InputException("Track '%s' has a toneless note" % name) |
||||
self.tal_repr[tal] = i |
||||
|
||||
self.longest_sample = None |
||||
self.sample_length_sum = None |
||||
|
||||
|
||||
class Music: |
||||
def __init__(self, version, tracks, instruments, length, ticklength, n_delay_tracks, delay_lengths, delay_strength, master_volume): |
||||
self.version = version |
||||
self.tracks = tracks |
||||
self.instruments = instruments |
||||
self.length = length |
||||
self.ticklength = ticklength |
||||
self.n_delay_tracks = n_delay_tracks |
||||
self.delay_lengths = delay_lengths |
||||
self.delay_strength = delay_strength |
||||
self.master_volume = master_volume |
||||
|
||||
self.uses_waveform = [False] * 6 |
||||
for t in self.tracks: |
||||
inst = self.instruments[t.instr] |
||||
self.uses_waveform[inst.bwave] = True |
||||
self.uses_waveform[inst.mwave] = True |
||||
self.uses_velocity = any(any(tal[2] != 127 for tal in t.tals) for t in self.tracks) |
||||
self.uses_long_notes = any(any(tal[1] > 127 for tal in t.tals) for t in self.tracks) |
||||
self.uses_delay = (self.n_delay_tracks > 0) |
||||
self.uses_panning = any(t.volume.isPanned() for t in self.tracks) |
||||
self.uses_indexdecay = any(self.instruments[t.instr].indexd != 0 for t in self.tracks) |
||||
self.uses_gain = any(self.instruments[t.instr].gain != 0 for t in self.tracks) |
||||
|
||||
# Calculate longest sample |
||||
self.max_longest_sample = 0.0 |
||||
self.max_sample_length_sum = 0.0 |
||||
self.max_release_tail = 0.0 |
||||
for ti,track in enumerate(self.tracks): |
||||
track.longest_sample = 0.0 |
||||
track.sample_length_sum = 0.0 |
||||
track.max_release_tail = 0.0 |
||||
|
||||
def envelope(v): |
||||
return pow(2.0, v * 0.125) * 32767.0 / (44100.0 * 4) |
||||
instr = instruments[track.instr] |
||||
attack_length = envelope(instr.attack) |
||||
decay_length = envelope(instr.decay) |
||||
release_length = envelope(instr.release) |
||||
|
||||
for tal in track.tals: |
||||
note_length = tal[1] * ticklength |
||||
sustain_length = max(note_length, attack_length + decay_length) |
||||
sample_length = sustain_length + release_length + 32767.0 / (44100.0 * 4) + 0.01 |
||||
release_tail = sample_length - note_length |
||||
track.longest_sample = max(track.longest_sample, sample_length) |
||||
track.sample_length_sum += sample_length |
||||
track.max_release_tail = max(track.max_release_tail, release_tail) |
||||
self.max_longest_sample = max(self.max_longest_sample, track.longest_sample) |
||||
self.max_sample_length_sum = max(self.max_sample_length_sum, track.sample_length_sum) |
||||
self.max_release_tail = max(self.max_release_tail, track.max_release_tail) |
||||
|
||||
# Track title, specifying resulting track index, track name and instrument number / name |
||||
instr = self.instruments[track.instr] |
||||
track.title = "%02d: %s / %02X|%s" % (ti, track.name, instr.number, instr.name) |
||||
|
||||
self.datainit = None |
||||
self.out = None |
||||
|
||||
def dataline(self, data): |
||||
if len(data) > 0: |
||||
line = self.datainit |
||||
first = True |
||||
for d in data: |
||||
if not first: |
||||
line += "," |
||||
line += str(d) |
||||
first = False |
||||
line += "\n" |
||||
self.out += line |
||||
|
||||
def instrparams(self, inst, fields): |
||||
return [inst.__dict__[f] if f in inst.__dict__ else f for f in fields] |
||||
|
||||
def comment(self, c): |
||||
self.out += "\t; %s\n" % c |
||||
|
||||
def notelist(self, datafunc, trackterm): |
||||
for t in self.tracks: |
||||
self.comment(t.title) |
||||
prev_n = None |
||||
pat_data = [] |
||||
for n in [note for note in t.notes if not note.off and note.instr == t.instr]: |
||||
if prev_n is None or n.songpos != prev_n.songpos: |
||||
self.dataline(pat_data) |
||||
pat_data = [] |
||||
self.comment("position %d - pattern %d" % (n.songpos, n.pat)) |
||||
pat_data += datafunc(t,prev_n,n) |
||||
prev_n = n |
||||
self.dataline(pat_data) |
||||
self.dataline(trackterm) |
||||
self.out += "\n" |
||||
|
||||
def notebitmask(self): |
||||
for t in self.tracks: |
||||
self.comment(t.name) |
||||
prev_n = None |
||||
pat_data = [] |
||||
pos = 0 |
||||
data_byte = 0 |
||||
dummy_note = Note(self.length, 0, 0, "C-0", 0, 127) |
||||
for n in [note for note in t.notes if not note.off and note.instr == t.instr] + [dummy_note]: |
||||
while pos <= n.line: |
||||
data_byte = (data_byte << 1) + (1 if pos == n.line else 0) |
||||
pos += 1 |
||||
if (pos & 7) == 0: |
||||
if prev_n is None or n.songpos != prev_n.songpos: |
||||
self.dataline(pat_data) |
||||
pat_data = [] |
||||
self.comment("position %d - pattern %d" % (n.songpos, n.pat)) |
||||
prev_n = n |
||||
pat_data.append(data_byte) |
||||
data_byte = 0 |
||||
prev_n = n |
||||
self.dataline(pat_data) |
||||
self.out += "\n" |
||||
|
||||
def posdata(self, t, pn, n): |
||||
step = n.line-pn.line if pn is not None else n.line |
||||
return multibyte(step) |
||||
|
||||
def samdata(self, t, pn, n): |
||||
return [t.tal_repr[t.notemap[n]]] |
||||
|
||||
def exportPC(self, sample_rate): |
||||
self.datainit = "\tdb\t" |
||||
self.out = "" |
||||
|
||||
sspt = int(self.ticklength * sample_rate)*4 |
||||
|
||||
def roundup(v): |
||||
return (int(v) & -0x10000) + 0x10000 |
||||
|
||||
feature_flags = self.uses_waveform + [self.uses_velocity, self.uses_long_notes, self.uses_delay, self.uses_panning, self.uses_indexdecay, self.uses_gain] |
||||
feature_names = ["SINE", "SAWTOOTH", "SQUARE", "PARABOLA", "TRIANGLE", "NOISE", |
||||
"VELOCITY", "LONG_NOTES", "DELAY", "PANNING", "INDEXDECAY", "GAIN"] |
||||
print "Features used: " + " ".join(n for f,n in zip(feature_flags, feature_names) if f) |
||||
print |
||||
|
||||
global infile |
||||
self.out += "; Clinkster music converted from %s %s\n" % (infile, str(datetime.datetime.now())[:-7]) |
||||
self.out += "\n" |
||||
for f,fname in zip(feature_flags, feature_names): |
||||
self.out += "%%define USES_%s %d\n" % (fname, int(f)) |
||||
self.out += "\n" |
||||
self.out += "%%define SUBSAMPLES_PER_TICK %d\n" % sspt |
||||
self.out += "%%define MAX_INSTRUMENT_SUBSAMPLES %d\n" % roundup((self.max_longest_sample + self.max_release_tail) * (sample_rate * 4.0)) |
||||
self.out += "%%define MAX_TOTAL_INSTRUMENT_SAMPLES %d\n" % roundup(self.max_sample_length_sum * sample_rate) |
||||
self.out += "%%define MAX_RELEASE_SUBSAMPLES %d\n" % roundup(self.max_release_tail * (sample_rate * 4.0)) |
||||
self.out += "%%define TOTAL_SAMPLES %d\n" % roundup((self.length * self.ticklength + self.max_release_tail) * sample_rate) |
||||
self.out += "%%define MAX_TRACK_INSTRUMENT_RENDERS %d\n" % max(len(t.tals) for t in self.tracks) |
||||
self.out += "\n" |
||||
self.out += "%%define MAX_DELAY_LENGTH %d\n" % int(max(self.delay_lengths) * sample_rate) |
||||
self.out += "%%define LEFT_DELAY_LENGTH %d\n" % int(self.delay_lengths[0] * sample_rate) |
||||
self.out += "%%define RIGHT_DELAY_LENGTH %d\n" % int(self.delay_lengths[1] * sample_rate) |
||||
self.out += "%%define DELAY_STRENGTH %0.8f\n" % self.delay_strength |
||||
self.out += "\n" |
||||
self.out += "%%define NUMTRACKS %d\n" % len(self.tracks) |
||||
self.out += "%%define LOGNUMTICKS %d\n" % int(math.log(self.length, 2) + 1) |
||||
self.out += "%%define MUSIC_LENGTH %d\n" % self.length |
||||
self.out += "%%define TICKS_PER_SECOND %0.8f\n" % (1.0 / self.ticklength) |
||||
|
||||
# Remap used instruments |
||||
wmap = [] |
||||
j = 0 |
||||
for i in range(6): |
||||
wmap.append(j) |
||||
if self.uses_waveform[i]: |
||||
j += 1 |
||||
for inst in self.instruments: |
||||
if inst is not None: |
||||
for wave in ["bwave", "mwave"]: |
||||
inst.__dict__[wave] = wmap[inst.__dict__[wave]] |
||||
|
||||
# Instrument data |
||||
self.out += "\n\n\tsection instdata data align=1\n" |
||||
self.out += "\n_InstrumentData:\n" |
||||
for ti,track in enumerate(self.tracks): |
||||
track_volume = track.volume * self.master_volume * makeVolume(32.0) |
||||
if self.n_delay_tracks > 0 and ti == self.n_delay_tracks: |
||||
self.dataline([-1]) |
||||
self.comment(track.title) |
||||
params = self.instrparams( |
||||
self.instruments[track.instr], |
||||
["bwave","mwave","bdetune","mdetune", |
||||
"indexspr","index","layers","randomseed", |
||||
"sustain"] + |
||||
([int(track_volume.left), int(track_volume.right)] |
||||
if self.uses_panning else |
||||
[int(track_volume.left)]) + |
||||
["bpitchs","mpitchs","bpitchd","mpitchd"] + |
||||
(["indexd"] if self.uses_indexdecay else []) + |
||||
(["gain"] if self.uses_gain else []) + |
||||
["attack","decay","release"]) |
||||
self.dataline(params) |
||||
|
||||
# List tones and lengths |
||||
taldata = [] |
||||
prev_t = -1 |
||||
first = True |
||||
for t,l,v in track.tals: |
||||
if t > prev_t: |
||||
if not first: |
||||
taldata += [0] |
||||
taldata += [t-prev_t-1] |
||||
prev_t = t |
||||
if self.uses_velocity: |
||||
taldata += [v] |
||||
taldata += multibyte(l) |
||||
prev_v = v |
||||
first = False |
||||
taldata += [0,-1] |
||||
self.dataline(taldata) |
||||
if self.uses_delay: |
||||
self.dataline([-1,-1]) |
||||
else: |
||||
self.dataline([-1]) |
||||
|
||||
# Positions of notes |
||||
self.out += "\n\tsection notepos data align=1\n" |
||||
self.out += "\n_NotePositions:\n" |
||||
self.notelist(self.posdata, []) |
||||
|
||||
# Samples for notes |
||||
self.out += "\n\tsection notesamp data align=1\n" |
||||
self.out += "\n_NoteSamples:\n" |
||||
self.notelist(self.samdata, [-1]) |
||||
|
||||
return self.out |
||||
|
||||
def makeDeltas(self, init_delta, lines_per_beat): |
||||
beats_per_line = 1.0/lines_per_beat |
||||
deltas = [] |
||||
for t in self.tracks: |
||||
tdeltas = [] |
||||
delta = init_delta |
||||
note_i = 0 |
||||
for p in range(0, self.length): |
||||
while t.notes[note_i].line <= p: |
||||
if not t.notes[note_i].off: |
||||
delta = p * beats_per_line |
||||
note_i += 1 |
||||
tdeltas.append(delta) |
||||
deltas.append(tdeltas) |
||||
return deltas |
||||
|
||||
|
||||
def extractTrackNotes(xsong, tr, col): |
||||
outside_pattern = 0 |
||||
xsequence = xsong.PatternSequence.PatternSequence |
||||
if not xsequence: |
||||
xsequence = xsong.PatternSequence.SequenceEntries.SequenceEntry |
||||
xpatterns = xsong.PatternPool.Patterns.Pattern |
||||
tname = str(xsong.Tracks.SequencerTrack[tr].Name) |
||||
|
||||
notes = [] |
||||
|
||||
pattern_top = 0 |
||||
prev_instr = None |
||||
for posn,xseq in enumerate(xsequence): |
||||
patn = int(xseq.Pattern) |
||||
xpat = xpatterns[patn] |
||||
nlines = int(xpat.NumberOfLines) |
||||
if tr in [int(xmt) for xmt in xseq.MutedTracks.MutedTrack]: |
||||
off = Note(pattern_top, posn, patn, "OFF", None, 127) |
||||
notes.append(off) |
||||
else: |
||||
xtrack = xpat.Tracks.PatternTrack[tr] |
||||
for xline in xtrack.Lines.Line: |
||||
index = int(xline("index")) |
||||
if index < nlines: |
||||
line = pattern_top + index |
||||
xcol = xline.NoteColumns.NoteColumn[col] |
||||
if xcol.Note and str(xcol.Note) != "---": |
||||
instr = str(xcol.Instrument) |
||||
if instr == "..": |
||||
if prev_instr is None and str(xcol.Note) != "OFF": |
||||
raise InputException("Track '%s' pattern %d position %d: Unspecified instrument" % (tname, patn, index)) |
||||
instr = prev_instr |
||||
prev_instr = instr |
||||
|
||||
note = Note(line, posn, patn, xcol.Note, instr, xcol.Volume) |
||||
notes.append(note) |
||||
|
||||
if note.velocity == 0 or note.velocity > 127: |
||||
raise InputException("Track '%s' pattern %d position %d: Illegal velocity value" % (tname, patn, index)) |
||||
|
||||
# Check for illegal uses of panning, delay and effect columns |
||||
def checkColumn(x, msg): |
||||
if x and not str(x) in ["", "..", "00"]: |
||||
raise InputException("Track '%s' pattern %d position %d: %s" % (tname, patn, index, msg)) |
||||
checkColumn(xcol.Delay, "Delay column used") |
||||
for xeff in xline.EffectColumns.EffectColumn.Number: |
||||
checkColumn(xeff, "Effect column used") |
||||
else: |
||||
outside_pattern += 1 |
||||
pattern_top += nlines |
||||
notes.append(Note(pattern_top, len(xsequence), len(xpatterns), "OFF", 0, 127)) |
||||
|
||||
if outside_pattern > 0: |
||||
print " * Track '%s': %d note%s outside patterns ignored" % (tname, outside_pattern, "s" * (outside_pattern > 1)) |
||||
|
||||
return notes |
||||
|
||||
def pickupDelay(xdevices, delay_lengths, delay_strength, tname, ticklength): |
||||
if isactive(xdevices.DelayDevice): |
||||
send = float(xdevices.DelayDevice.TrackSend.Value) / 127.0 |
||||
lfeedback = float(xdevices.DelayDevice.LFeedback.Value) |
||||
rfeedback = float(xdevices.DelayDevice.RFeedback.Value) |
||||
if float(xdevices.DelayDevice.LineSync.Value): |
||||
lsynctime = float(xdevices.DelayDevice.LSyncTime.Value) |
||||
lsyncoffset = float(xdevices.DelayDevice.LSyncOffset.Value) |
||||
ldelay = (lsynctime + lsyncoffset) * ticklength |
||||
rsynctime = float(xdevices.DelayDevice.RSyncTime.Value) |
||||
rsyncoffset = float(xdevices.DelayDevice.RSyncOffset.Value) |
||||
rdelay = (rsynctime + rsyncoffset) * ticklength |
||||
else: |
||||
ldelay = float(xdevices.DelayDevice.LDelay.Value) / 1000.0 |
||||
rdelay = float(xdevices.DelayDevice.RDelay.Value) / 1000.0 |
||||
if abs(lfeedback - send) > 0.05: |
||||
print " * Track '%s': Left feedback (%0.2f) is different from send value (%0.2f)" % (tname, lfeedback, send) |
||||
if abs(rfeedback - send) > 0.05: |
||||
print " * Track '%s': Right feedback (%0.2f) is different from send value (%0.2f)" % (tname, rfeedback, send) |
||||
if delay_lengths != [0.0, 0.0] and ([ldelay,rdelay] != delay_lengths or send != delay_strength): |
||||
print " * Track '%s' has different delay parameters from earlier track" % tname |
||||
return [ldelay,rdelay],send |
||||
return delay_lengths,delay_strength |
||||
|
||||
def makeTracks(version, xsong, ticklength): |
||||
instruments = [] |
||||
delay_tracks = [] |
||||
non_delay_tracks = [] |
||||
delay_lengths = [0.0, 0.0] |
||||
delay_strength = 0.0 |
||||
|
||||
for ii,xinst in enumerate(xsong.Instruments.Instrument): |
||||
params = [float(v) for v in instplugins(xinst).PluginDevice.Parameters.Parameter.Value] |
||||
if params: |
||||
instrument = Instrument(ii, str(xinst.Name), params) |
||||
instrument.volume = makeVolume(instplugins(xinst).Volume) |
||||
instruments.append(instrument) |
||||
|
||||
else: |
||||
instruments.append(None) |
||||
|
||||
for tr,xtrack in enumerate(xsong.Tracks.SequencerTrack): |
||||
tname = str(xtrack.Name) |
||||
ncols = int(xtrack.NumberOfVisibleNoteColumns) |
||||
xdevices = xtrack.FilterDevices.Devices |
||||
xdevice = xdevices.SequencerTrackDevice |
||||
if not xdevice: |
||||
xdevice = xdevices.TrackMixerDevice |
||||
volume = makeVolume(xdevice.Volume.Value) |
||||
volume *= makePanning(xdevice.Panning.Value) |
||||
while isactive(xdevices.SendDevice): |
||||
if isactive(xdevices.DelayDevice): |
||||
raise InputException("Track '%s' uses both delay and send" % tname); |
||||
if str(xdevices.SendDevice.MuteSource) != "true": |
||||
raise InputException("Track '%s' uses send without Mute Source" % tname); |
||||
volume *= makeVolume(xdevices.SendDevice.SendAmount.Value) |
||||
volume *= makePanning(xdevices.SendDevice.SendPan.Value) |
||||
dest = int(float(xdevices.SendDevice.DestSendTrack.Value)) |
||||
xdevices = xsong.Tracks.SequencerSendTrack[dest].FilterDevices.Devices |
||||
xdevice = xdevices.SequencerSendTrackDevice |
||||
if not xdevice: |
||||
xdevice = xdevices.SendTrackMixerDevice |
||||
volume *= makeVolume(xdevice.Volume.Value) |
||||
volume *= makePanning(xdevice.Panning.Value) |
||||
volume *= makeVolume(xdevice.PostVolume.Value) |
||||
volume *= makePanning(xdevice.PostPanning.Value) |
||||
|
||||
for col in range(0,ncols): |
||||
notes = extractTrackNotes(xsong, tr, col) |
||||
|
||||
track_instrs = [] |
||||
for note in notes: |
||||
if not note.off: |
||||
instr = instruments[note.instr] |
||||
if instr is None: |
||||
raise InputException("Track '%s' uses undefined instrument (%d)" % (tname, note.instr)); |
||||
if note.instr not in track_instrs: |
||||
track_instrs.append(note.instr) |
||||
|
||||
for instr in track_instrs: |
||||
track = Track(tr, tname, notes, volume, instr, instruments) |
||||
if isactive(xdevices.DelayDevice): |
||||
delay_tracks.append(track) |
||||
else: |
||||
non_delay_tracks.append(track) |
||||
|
||||
delay_lengths,delay_strength = pickupDelay(xdevices, delay_lengths, delay_strength, tname, ticklength) |
||||
|
||||
for xtrack in xsong.Tracks.SequencerSendTrack: |
||||
xdevices = xtrack.FilterDevices.Devices |
||||
if xdevices.DelayDevice: |
||||
delay_lengths,delay_strength = pickupDelay(xdevices, delay_lengths, delay_strength, tname, ticklength) |
||||
|
||||
#delay_tracks = sorted(delay_tracks, key = (lambda t : t.instr)) |
||||
#non_delay_tracks = sorted(non_delay_tracks, key = (lambda t : t.instr)) |
||||
|
||||
return (delay_tracks + non_delay_tracks), len(delay_tracks), delay_lengths, delay_strength, instruments |
||||
|
||||
def makeMusic(xsong): |
||||
vstnames = set(str(v) for v in instplugins(xsong.Instruments.Instrument).PluginDevice.PluginIdentifier) |
||||
if len(vstnames) > 1: |
||||
raise InputException("More than one VST used: %s" % list(vstnames)) |
||||
vstname = list(vstnames)[0] |
||||
vstmap = { "Clinkster" : 1 } |
||||
if vstname not in vstmap: |
||||
raise InputException("Unknown VST used: %s" % vstname) |
||||
vstversion = vstmap[vstname] |
||||
print "VST version: %d" % vstversion |
||||
if vstversion != 1: |
||||
raise InputException("Only Clinkster version 1 supported") |
||||
|
||||
xgsd = xsong.GlobalSongData |
||||
if xgsd.PlaybackEngineVersion and int(xgsd.PlaybackEngineVersion) >= 4: |
||||
lines_per_minute = float(xgsd.BeatsPerMin) * float(xgsd.LinesPerBeat) |
||||
print "New timing format: %d ticks per minute" % lines_per_minute |
||||
else: |
||||
lines_per_minute = float(xgsd.BeatsPerMin) * 24.0 / float(xgsd.TicksPerLine) |
||||
print "Old timing format: %d ticks per minute" % lines_per_minute |
||||
ticklength = 60.0 / lines_per_minute |
||||
print |
||||
|
||||
tracks,n_delay_tracks,delay_lengths,delay_strength,instruments = makeTracks(vstversion, xsong, ticklength) |
||||
|
||||
xpositions = xsong.PatternSequence.PatternSequence.Pattern |
||||
if not xpositions: |
||||
xpositions = xsong.PatternSequence.SequenceEntries.SequenceEntry.Pattern |
||||
xpatterns = xsong.PatternPool.Patterns.Pattern |
||||
length = 0 |
||||
for xpos in xpositions: |
||||
patn = int(xpos) |
||||
xpat = xpatterns[patn] |
||||
nlines = int(xpat.NumberOfLines) |
||||
length += nlines |
||||
|
||||
xmstdev = xsong.Tracks.SequencerMasterTrack.FilterDevices.Devices.SequencerMasterTrackDevice |
||||
if not xmstdev: |
||||
xmstdev = xsong.Tracks.SequencerMasterTrack.FilterDevices.Devices.MasterTrackMixerDevice |
||||
master_volume = makeVolume(xmstdev.Volume.Value) |
||||
master_volume *= makePanning(xmstdev.Panning.Value) |
||||
master_volume *= makeVolume(xmstdev.PostVolume.Value) |
||||
master_volume *= makePanning(xmstdev.PostPanning.Value) |
||||
|
||||
return Music(vstversion, tracks, instruments, length, ticklength, n_delay_tracks, delay_lengths, delay_strength, master_volume) |
||||
|
||||
|
||||
def printMusicStats(music): |
||||
print "Music length: %d ticks at %0.2f ticks per minute" % (music.length, 60.0 / music.ticklength) |
||||
print |
||||
for ti,track in enumerate(music.tracks): |
||||
tnotes = "" |
||||
for t,l,v in track.tals: |
||||
num_notes = 0 |
||||
for n in [note for note in track.notes if not note.off and note.instr == track.instr]: |
||||
if track.notemap[n] == (t,l,v): |
||||
num_notes += 1 |
||||
if v < 127: |
||||
tnotes += " %s/%02X:%d(%d)" % (notename(t), v, l, num_notes) |
||||
else: |
||||
tnotes += " %s:%d(%d)" % (notename(t), l, num_notes) |
||||
if music.n_delay_tracks > 0 and ti == 0: |
||||
print "Tracks with delay:" |
||||
print |
||||
if music.n_delay_tracks > 0 and ti == music.n_delay_tracks: |
||||
print |
||||
print "Tracks without delay:" |
||||
print |
||||
print track.title |
||||
print tnotes |
||||
#print "Max: longest %f, total %f" % (music.max_longest_sample, music.max_sample_length_sum) |
||||
|
||||
def writefile(filename, s): |
||||
f = open(filename, "wb") |
||||
f.write(s) |
||||
f.close() |
||||
print "Wrote file %s" % filename |
||||
|
||||
|
||||
if len(sys.argv) < 3: |
||||
print "Usage: %s <input xrns file> <output asm file>" % sys.argv[0] |
||||
sys.exit(1) |
||||
|
||||
infile = sys.argv[1] |
||||
outfile = sys.argv[2] |
||||
|
||||
x = XML.makeXML(zipfile.ZipFile(infile).read("Song.xml")) |
||||
try: |
||||
music = makeMusic(x.RenoiseSong) |
||||
print |
||||
printMusicStats(music) |
||||
print |
||||
|
||||
writefile(outfile, music.exportPC(44100.0)) |
||||
|
||||
if len(sys.argv) > 3: |
||||
deltas = music.makeDeltas(0.0, 1.0) |
||||
syncfile = sys.argv[3] |
||||
header = "" |
||||
header += struct.pack('I', 1) |
||||
header += struct.pack('I', music.length*4) |
||||
header += struct.pack('I', len(music.tracks)*music.length*4) |
||||
body = "" |
||||
for t,tdeltas in enumerate(deltas): |
||||
body += struct.pack("%df" % len(tdeltas), *tdeltas) |
||||
data = header + body |
||||
writefile(syncfile, data) |
||||
|
||||
except InputException, e: |
||||
print "Error in input song: %s" % e.message |
||||
|
@ -0,0 +1,91 @@ |
||||
#!/usr/bin/env python |
||||
|
||||
import xml.dom |
||||
import xml.dom.minidom |
||||
|
||||
class XML(object): |
||||
def __init__(self, domlist): |
||||
self.domlist = list(domlist) |
||||
|
||||
def __getattr__(self, name): |
||||
l = [] |
||||
for d in self.domlist: |
||||
for c in d.childNodes: |
||||
if c.nodeName == name: |
||||
l.append(c) |
||||
return XML(l) |
||||
|
||||
def __len__(self): |
||||
return len(self.domlist) |
||||
|
||||
def __getitem__(self, i): |
||||
if i >= len(self.domlist): |
||||
return XML([]) |
||||
return XML([self.domlist[i]]) |
||||
|
||||
def __iter__(self): |
||||
for d in self.domlist: |
||||
yield XML([d]) |
||||
|
||||
def __call__(self, attrname): |
||||
s = "" |
||||
for d in self.domlist: |
||||
if d.nodeType == xml.dom.Node.ELEMENT_NODE and d.hasAttribute(attrname): |
||||
s += d.getAttribute(attrname) |
||||
return s |
||||
|
||||
def __str__(self): |
||||
def collect(dl): |
||||
s = "" |
||||
for d in dl: |
||||
if d.nodeType == xml.dom.Node.TEXT_NODE: |
||||
s += d.data |
||||
else: |
||||
s += collect(d.childNodes) |
||||
return s |
||||
return collect(self.domlist) |
||||
|
||||
def __int__(self): |
||||
return int(str(self)) |
||||
|
||||
def __float__(self): |
||||
return float(str(self)) |
||||
|
||||
def __nonzero__(self): |
||||
return len(self.domlist) != 0 |
||||
|
||||
def replaceText(self, fun): |
||||
def collect(dl): |
||||
for d in dl: |
||||
if d.nodeType == xml.dom.Node.TEXT_NODE: |
||||
d.data = fun(d.data) |
||||
else: |
||||
collect(d.childNodes) |
||||
collect(self.domlist) |
||||
|
||||
def setData(self, data): |
||||
sdata = str(data) |
||||
for d in self.domlist: |
||||
for c in d.childNodes: |
||||
c.data = sdata |
||||
|
||||
def removeChild(self, child): |
||||
if len(self.domlist) != len(child.domlist): |
||||
raise ValueError |
||||
for p,c in zip(self.domlist, child.domlist): |
||||
p.removeChild(c) |
||||
|
||||
def insertBefore(self, newChild, refChild): |
||||
if len(self.domlist) != len(newChild.domlist) or len(newChild.domlist) != len(refChild.domlist): |
||||
raise ValueError |
||||
for p,nc,rc in zip(self.domlist, newChild.domlist, refChild.domlist): |
||||
p.insertBefore(nc.childNodes[0],rc) |
||||
|
||||
def export(self): |
||||
return "".join(x.toxml("utf-8") for x in self.domlist) |
||||
|
||||
def readXML(filename): |
||||
return XML([xml.dom.minidom.parse(filename)]) |
||||
|
||||
def makeXML(xstring): |
||||
return XML([xml.dom.minidom.parseString(xstring)]) |
Binary file not shown.
@ -0,0 +1,9 @@ |
||||
del /q temp\* |
||||
del music.exe |
||||
|
||||
tools\RenoiseConvert.exe music.xrns temp\music.asm |
||||
tools\nasmw -f win32 src\clinkster_multithreaded.asm -o temp\clinkster_multithreaded.obj |
||||
tools\nasmw -f win32 src\play.asm -o temp\play.obj |
||||
tools\crinkler20\crinkler temp\clinkster_multithreaded.obj temp\play.obj /OUT:music.exe /ENTRY:main tools\kernel32.lib tools\user32.lib tools\winmm.lib tools\msvcrt_old.lib @crinkler_options.txt |
||||
|
||||
pause |
@ -0,0 +1,9 @@ |
||||
del /q temp\* |
||||
del music_wav.exe |
||||
|
||||
tools\RenoiseConvert.exe music.xrns temp\music.asm |
||||
tools\nasmw -f win32 src\clinkster_multithreaded.asm -o temp\clinkster_multithreaded.obj |
||||
tools\nasmw -f win32 -dWRITE_WAV src\play.asm -o temp\play.obj |
||||
tools\crinkler20\crinkler temp\clinkster_multithreaded.obj temp\play.obj /OUT:music_wav.exe /ENTRY:main tools\kernel32.lib tools\user32.lib tools\winmm.lib tools\msvcrt_old.lib @crinkler_options.txt |
||||
|
||||
pause |
@ -0,0 +1 @@ |
||||
/UNSAFEIMPORT /COMPMODE:INSTANT /HASHSIZE:100 |
@ -0,0 +1,4 @@ |
||||
Music composed using 4k synth "Clinkster" by Blueberry / Loonies |
||||
|
||||
Generating music... |
||||
|
Binary file not shown.
@ -0,0 +1,16 @@ |
||||
|
||||
This setup is for easily building an executable version of a piece of music |
||||
created using Clinkster. |
||||
|
||||
Proceed as follows: |
||||
|
||||
1. Place your music here, named music.xrns. |
||||
2. Place a text file containing the text you would like the executable to |
||||
print at startup, named music.txt. |
||||
3. Optionally modify the Crinkler options in crinkler_options.txt |
||||
(read the Crinkler manual for details). |
||||
4. Run build.bat to get an executable that plays the music, or |
||||
build_wav.bat to get one that writes the music in WAV format to |
||||
music.wav and then plays it. |
||||
|
||||
Enjoy! |
@ -0,0 +1,12 @@ |
||||
|
||||
extern Clinkster_GenerateMusic |
||||
extern Clinkster_StartMusic |
||||
extern Clinkster_GetPosition |
||||
extern Clinkster_GetInstrumentTrigger |
||||
|
||||
extern Clinkster_MusicBuffer |
||||
extern Clinkster_NoteTiming |
||||
extern Clinkster_TicksPerSecond |
||||
extern Clinkster_MusicLength |
||||
extern Clinkster_NumTracks |
||||
extern Clinkster_WavFileHeader |
@ -0,0 +1,962 @@ |
||||
|
||||
; If set to 1, timing information is generated during music generation |
||||
; which is needed for Clinkster_GetInstrumentTrigger. |
||||
; Set it to 0 if you don't need this functionality. |
||||
%define CLINKSTER_GENERATE_TIMING_DATA 0 |
||||
|
||||
; Offset applied by Clinkster_GetPosition to compensate for graphics latency. |
||||
; Measured in samples (44100ths of a second). |
||||
; The default value of 2048 (corresponding to about 46 milliseconds) is |
||||
; appropriate for typical display latencies for high-framerate effects. |
||||
%define CLINKSTER_TIMER_OFFSET 0 |
||||
|
||||
%include "temp/music.asm" |
||||
|
||||
|
||||
;; ********** Definitions ********** |
||||
|
||||
global Clinkster_GenerateMusic |
||||
global _Clinkster_GenerateMusic@0 |
||||
global Clinkster_StartMusic |
||||
global _Clinkster_StartMusic@0 |
||||
global Clinkster_GetPosition |
||||
global _Clinkster_GetPosition@0 |
||||
global Clinkster_GetInstrumentTrigger |
||||
global _Clinkster_GetInstrumentTrigger@8 |
||||
|
||||
global Clinkster_MusicBuffer |
||||
global _Clinkster_MusicBuffer |
||||
global Clinkster_TicksPerSecond |
||||
global _Clinkster_TicksPerSecond |
||||
global Clinkster_MusicLength |
||||
global _Clinkster_MusicLength |
||||
global Clinkster_NumTracks |
||||
global _Clinkster_NumTracks |
||||
%if CLINKSTER_GENERATE_TIMING_DATA |
||||
global Clinkster_NoteTiming |
||||
global _Clinkster_NoteTiming |
||||
%endif |
||||
global Clinkster_WavFileHeader |
||||
global _Clinkster_WavFileHeader |
||||
|
||||
extern __imp__waveOutOpen@24 |
||||
extern __imp__waveOutPrepareHeader@12 |
||||
extern __imp__waveOutWrite@12 |
||||
extern __imp__waveOutGetPosition@12 |
||||
|
||||
extern __imp__CreateThread@24 |
||||
extern __imp__WaitForSingleObject@8 |
||||
|
||||
%define SAMPLE_RATE 44100 |
||||
%define WAVE_SIZE 65536 |
||||
|
||||
|
||||
;; ********** Public variables ********** |
||||
|
||||
section MusBuf bss align=4 |
||||
Clinkster_MusicBuffer: |
||||
_Clinkster_MusicBuffer: |
||||
.align24 |
||||
resw (TOTAL_SAMPLES*2) |
||||
resw 2 ; padding to catch extra write in conversion |
||||
|
||||
section tps rdata align=4 |
||||
Clinkster_TicksPerSecond: |
||||
_Clinkster_TicksPerSecond: |
||||
dd TICKS_PER_SECOND |
||||
|
||||
section muslen rdata align=4 |
||||
Clinkster_MusicLength: |
||||
_Clinkster_MusicLength: |
||||
dd MUSIC_LENGTH |
||||
|
||||
section numtr rdata align=4 |
||||
Clinkster_NumTracks: |
||||
_Clinkster_NumTracks: |
||||
dd NUMTRACKS |
||||
|
||||
%if CLINKSTER_GENERATE_TIMING_DATA |
||||
section musdat bss align=4 |
||||
Clinkster_NoteTiming: |
||||
_Clinkster_NoteTiming: |
||||
.align16 |
||||
resd 2*(NUMTRACKS<<LOGNUMTICKS) |
||||
|
||||
section timing data align=4 |
||||
timing_ptr: dd Clinkster_NoteTiming |
||||
%endif |
||||
|
||||
section WavFile rdata align=4 |
||||
Clinkster_WavFileHeader: |
||||
_Clinkster_WavFileHeader: |
||||
db "RIFF" |
||||
dd 36+TOTAL_SAMPLES*4 |
||||
db "WAVE" |
||||
db "fmt " |
||||
dd 16 |
||||
dw 1,2 |
||||
dd SAMPLE_RATE |
||||
dd SAMPLE_RATE*4 |
||||
dw 4,16 |
||||
db "data" |
||||
dd TOTAL_SAMPLES*4 |
||||
|
||||
|
||||
;; ********** System structures ********** |
||||
|
||||
section WaveForm rdata align=1 |
||||
_WaveFormat: |
||||
dw 1,2 |
||||
dd SAMPLE_RATE |
||||
dd SAMPLE_RATE*4 |
||||
dw 4,16,0 |
||||
|
||||
section WaveHdr data align=4 |
||||
_WaveHdr: |
||||
dd Clinkster_MusicBuffer |
||||
dd (TOTAL_SAMPLES*4) |
||||
dd 0,0,0,0,0,0 |
||||
|
||||
section wavehand bss align=4 |
||||
_WaveOutHandle: |
||||
.align16 |
||||
resd 1 |
||||
|
||||
section WaveTime data align=4 |
||||
_WaveTime: |
||||
dd 4,0,0,0,0,0,0,0 |
||||
|
||||
|
||||
;; ********** Internal buffers ********** |
||||
|
||||
section wforms bss align=4 |
||||
waveforms: |
||||
.align16 |
||||
resd 6*WAVE_SIZE |
||||
|
||||
|
||||
;; ********** Instrument parameter access ********** |
||||
|
||||
section paramw rdata align=4 |
||||
param_weights: |
||||
dd 0.125 ; Release |
||||
dd 0.125 ; Decay |
||||
dd 0.125 ; Attack |
||||
%if USES_GAIN |
||||
dd 0.125 ; Gain |
||||
%endif |
||||
%if USES_INDEXDECAY |
||||
dd 0.0009765625 ; IndexDecay |
||||
%endif |
||||
dd 0.0009765625 ; M PitchDecay |
||||
dd 0.0009765625 ; B PitchDecay |
||||
dd 0.083333333333 ; M Pitch |
||||
dd 0.083333333333 ; B Pitch |
||||
dd 0.0000152587890625 ; Volume |
||||
%if USES_PANNING |
||||
dd 0.0000152587890625 ; Volume |
||||
%endif |
||||
dd 0.03125 ; Sustain |
||||
dd 16307 ; RandomSeed |
||||
dd 1 ; Layers |
||||
dd 4096.0 ; Index |
||||
dd 0.125 ; Index Spread |
||||
dd 0.0009765625 ; M Detune |
||||
dd 0.0009765625 ; B Detune |
||||
dd 65536 ; ModWave |
||||
dd 65536 ; BaseWave |
||||
|
||||
struc instr_params |
||||
ip_basewave: resd 1 |
||||
ip_modwave: resd 1 |
||||
ip_bdetune: resd 1 |
||||
ip_mdetune: resd 1 |
||||
ip_indexspr: resd 1 |
||||
ip_index: resd 1 |
||||
ip_layers: resd 1 |
||||
ip_randomseed: resd 1 |
||||
ip_sustain: resd 1 |
||||
ip_volume: resd 1+USES_PANNING |
||||
ip_bpitch: resd 1 |
||||
ip_mpitch: resd 1 |
||||
ip_bpitchd: resd 1 |
||||
ip_mpitchd: resd 1 |
||||
%if USES_INDEXDECAY |
||||
ip_indexd: resd 1 |
||||
%endif |
||||
%if USES_GAIN |
||||
ip_gain: resd 1 |
||||
%endif |
||||
ip_attack: resd 1 |
||||
ip_decay: resd 1 |
||||
ip_release: resd 1 |
||||
endstruc |
||||
|
||||
%define ip_INT 0 |
||||
%define ip_FLOAT 0 |
||||
%define IP(f,t) dword [dword ebx + g_instrparams + ip_ %+ f + ip_ %+ t] |
||||
%define IPI(f,i,t) dword [dword ebx + g_instrparams + ip_ %+ f + ip_ %+ t + i] |
||||
|
||||
|
||||
;; ********** Internal constants and tables ********** |
||||
|
||||
section resamp rdata align=4 |
||||
resamplefilter: |
||||
db -1,-2,-4,-4,-2,3,14,30,51,98,116,126 |
||||
db 126,116,98,51,30,14,3,-2,-4,-4,-2,-1 |
||||
resamplefilter_end: |
||||
FILTER_SIZE equ (resamplefilter_end-resamplefilter) |
||||
|
||||
section wavestep rdata align=4 |
||||
c_wavestep: dd 0.000030517578125 |
||||
section basefreq rdata align=4 |
||||
c_basefreq: dd 2.86698696365342 |
||||
section halfnote rdata align=4 |
||||
c_halfnote: dd 1.05946309435929 |
||||
section finalamp rdata align=4 |
||||
c_finalamp: dd 32767 |
||||
section velfac rdata align=4 |
||||
c_velocityfac: dd 0.007874015748031496 |
||||
section delaystr rdata align=4 |
||||
c_delaystr: dd DELAY_STRENGTH |
||||
section offset rdata align=4 |
||||
c_timeoffset: dd CLINKSTER_TIMER_OFFSET*4 |
||||
section tempo rdata align=4 |
||||
c_ticklength: dd SUBSAMPLES_PER_TICK/4*4 |
||||
section half rdata align=4 |
||||
c_onehalf: dd 0.5 |
||||
|
||||
|
||||
;; ********** Internal global variables ********** |
||||
|
||||
struc globalvars |
||||
g_phasetemp: resd 1 |
||||
g_layer_random: resd 1 |
||||
g_stereo: resd 1 ; 0 for left channel, 2 for right channel |
||||
g_noteposptr: resd 1 |
||||
g_notesamptr: resd 1 |
||||
g_instrparams: resb instr_params_size |
||||
g_layerparams: resq 0 |
||||
g_layer_bfreq: resq 1 |
||||
g_layer_mfreq: resq 1 |
||||
g_layer_index: resq 1 |
||||
g_layer_bpitch: resq 1 |
||||
g_layer_mpitch: resq 1 |
||||
g_layer_bpitchd: resq 1 |
||||
g_layer_mpitchd: resq 1 |
||||
%if USES_INDEXDECAY |
||||
g_layer_indexd: resq 1 |
||||
%endif |
||||
%if USES_GAIN |
||||
g_layer_gain: resq 1 |
||||
%endif |
||||
g_layer_attack: resq 1 |
||||
g_layer_decay: resq 1 |
||||
g_layer_release: resq 1 |
||||
|
||||
alignb 256 |
||||
g_InstrumentPointers: |
||||
resd MAX_TRACK_INSTRUMENT_RENDERS+1 |
||||
|
||||
resd MAX_DELAY_LENGTH |
||||
alignb 16777216 |
||||
g_MixingBuffer: |
||||
resd TOTAL_SAMPLES |
||||
|
||||
alignb 16777216 |
||||
g_InstrumentBuffer: |
||||
resd MAX_INSTRUMENT_SUBSAMPLES |
||||
|
||||
resd 256 |
||||
alignb 16777216 |
||||
g_InstrumentRender: |
||||
resd MAX_INSTRUMENT_SUBSAMPLES |
||||
|
||||
alignb 16777216 |
||||
g_InstrumentStore: |
||||
resd MAX_TOTAL_INSTRUMENT_SAMPLES |
||||
|
||||
endstruc |
||||
|
||||
section vars bss align=8 |
||||
vars_align16 |
||||
globals: |
||||
resb globalvars_size |
||||
resb globalvars_size |
||||
|
||||
|
||||
;; ********** Generate the sound for one layer ********** |
||||
|
||||
section mklayer text align=1 |
||||
makelayer: |
||||
lea edx, [dword ebx + g_layerparams] |
||||
|
||||
; Init random variables for layer |
||||
fild word [dword ebx + g_layer_random] |
||||
mov ecx, dword [dword ebx + g_layer_random] |
||||
ror ecx, cl |
||||
dec ecx |
||||
mov dword [dword ebx + g_layer_random], ecx |
||||
|
||||
fld IP(bdetune, FLOAT) |
||||
fmul st0, st0 |
||||
fmulp st1, st0 |
||||
fadd st0, st1 |
||||
fstp qword [edx] |
||||
add edx, byte 8 |
||||
|
||||
fild word [dword ebx + g_layer_random] |
||||
mov ecx, dword [dword ebx + g_layer_random] |
||||
ror ecx, cl |
||||
dec ecx |
||||
mov dword [dword ebx + g_layer_random], ecx |
||||
|
||||
fld IP(mdetune, FLOAT) |
||||
fmul st0, st0 |
||||
fmulp st1, st0 |
||||
fadd st0, st1 |
||||
fstp qword [edx] |
||||
add edx, byte 8 |
||||
|
||||
fild word [dword ebx + g_layer_random] |
||||
mov ecx, dword [dword ebx + g_layer_random] |
||||
ror ecx, cl |
||||
dec ecx |
||||
mov dword [dword ebx + g_layer_random], ecx |
||||
|
||||
fmul IP(indexspr, FLOAT) |
||||
fadd IP(index, FLOAT) |
||||
fstp qword [edx] |
||||
add edx, byte 8 |
||||
|
||||
; Init exponentiated variables for layer |
||||
lea edi, [dword ebx + g_instrparams+ip_bpitch] |
||||
mov ecx, 7+USES_INDEXDECAY+USES_GAIN |
||||
.powloop: |
||||
fld dword [edi] |
||||
|
||||
fld1 |
||||
fld st1 |
||||
fprem |
||||
fstp st1 |
||||
f2xm1 |
||||