Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Libraries/src/stdlib.coffee
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
406 lines (352 sloc)
15.3 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Provides a shorthand for musical notes. Usage: | |
# Frequency.A_4 // Defined to be 440.0 | |
class Frequency | |
# MIDI Note number -> Hz value | |
@noteNumber = (note) -> | |
note = parseInt(note) | |
(440 / 32) * Math.pow(2, (note - 9) / 12) | |
C_0 = Frequency.noteNumber(12) | |
C_SHARP_0 = D_FLAT_0 = Frequency.noteNumber(13) | |
D_0 = Frequency.noteNumber(14) | |
D_SHARP_0 = E_FLAT_0 = Frequency.noteNumber(15) | |
E_0 = Frequency.noteNumber(16) | |
F_0 = Frequency.noteNumber(17) | |
F_SHARP_0 = G_FLAT_0 = Frequency.noteNumber(18) | |
G_0 = Frequency.noteNumber(19) | |
G_SHARP_0 = A_FLAT_0 = Frequency.noteNumber(20) | |
A_0 = Frequency.noteNumber(21) | |
A_SHARP_0 = B_FLAT_0 = Frequency.noteNumber(22) | |
B_0 = Frequency.noteNumber(23) | |
C_1 = Frequency.noteNumber(24) | |
C_SHARP_1 = D_FLAT_1 = Frequency.noteNumber(25) | |
D_1 = Frequency.noteNumber(26) | |
D_SHARP_1 = E_FLAT_1 = Frequency.noteNumber(27) | |
E_1 = Frequency.noteNumber(28) | |
F_1 = Frequency.noteNumber(29) | |
F_SHARP_1 = G_FLAT_1 = Frequency.noteNumber(30) | |
G_1 = Frequency.noteNumber(31) | |
G_SHARP_1 = A_FLAT_1 = Frequency.noteNumber(32) | |
A_1 = Frequency.noteNumber(33) | |
A_SHARP_1 = B_FLAT_1 = Frequency.noteNumber(34) | |
B_1 = Frequency.noteNumber(35) | |
DEEP_C = C_2 = Frequency.noteNumber(36) | |
C_SHARP_2 = D_FLAT_2 = Frequency.noteNumber(37) | |
D_2 = Frequency.noteNumber(38) | |
D_SHARP_2 = E_FLAT_2 = Frequency.noteNumber(39) | |
E_2 = Frequency.noteNumber(40) | |
F_2 = Frequency.noteNumber(41) | |
F_SHARP_2 = G_FLAT_2 = Frequency.noteNumber(42) | |
G_2 = Frequency.noteNumber(43) | |
G_SHARP_2 = A_FLAT_2 = Frequency.noteNumber(44) | |
A_2 = Frequency.noteNumber(45) | |
A_SHARP_2 = B_FLAT_2 = Frequency.noteNumber(46) | |
B_2 = Frequency.noteNumber(47) | |
TENOR_C = C_3 = Frequency.noteNumber(48) | |
C_SHARP_3 = D_FLAT_3 = Frequency.noteNumber(49) | |
D_3 = Frequency.noteNumber(50) | |
D_SHARP_3 = E_FLAT_3 = Frequency.noteNumber(51) | |
E_3 = Frequency.noteNumber(52) | |
F_3 = Frequency.noteNumber(53) | |
F_SHARP_3 = G_FLAT_3 = Frequency.noteNumber(54) | |
G_3 = Frequency.noteNumber(55) | |
G_SHARP_3 = A_FLAT_3 = Frequency.noteNumber(56) | |
A_3 = Frequency.noteNumber(57) | |
A_SHARP_3 = B_FLAT_3 = Frequency.noteNumber(58) | |
B_3 = Frequency.noteNumber(59) | |
MIDDLE_C = C_4 = Frequency.noteNumber(60) | |
C_SHARP_4 = D_FLAT_4 = Frequency.noteNumber(61) | |
D_4 = Frequency.noteNumber(62) | |
D_SHARP_4 = E_FLAT_4 = Frequency.noteNumber(63) | |
E_4 = Frequency.noteNumber(64) | |
F_4 = Frequency.noteNumber(65) | |
F_SHARP_4 = G_FLAT_4 = Frequency.noteNumber(66) | |
G_4 = Frequency.noteNumber(67) | |
G_SHARP_4 = A_FLAT_4 = Frequency.noteNumber(68) | |
A440 = A_4 = Frequency.noteNumber(69) | |
A_SHARP_4 = B_FLAT_4 = Frequency.noteNumber(70) | |
B_4 = Frequency.noteNumber(71) | |
C_5 = Frequency.noteNumber(72) | |
C_SHARP_5 = D_FLAT_5 = Frequency.noteNumber(73) | |
D_5 = Frequency.noteNumber(74) | |
D_SHARP_5 = E_FLAT_5 = Frequency.noteNumber(75) | |
E_5 = Frequency.noteNumber(76) | |
F_5 = Frequency.noteNumber(77) | |
F_SHARP_5 = G_FLAT_5 = Frequency.noteNumber(78) | |
G_5 = Frequency.noteNumber(79) | |
G_SHARP_5 = A_FLAT_5 = Frequency.noteNumber(80) | |
A_5 = Frequency.noteNumber(81) | |
A_SHARP_5 = B_FLAT_5 = Frequency.noteNumber(82) | |
B_5 = Frequency.noteNumber(83) | |
SOPRANO_C = HIGH_C = C_6 = Frequency.noteNumber(84) | |
C_SHARP_6 = D_FLAT_6 = Frequency.noteNumber(85) | |
D_6 = Frequency.noteNumber(86) | |
D_SHARP_6 = E_FLAT_6 = Frequency.noteNumber(87) | |
E_6 = Frequency.noteNumber(88) | |
F_6 = Frequency.noteNumber(89) | |
F_SHARP_6 = G_FLAT_6 = Frequency.noteNumber(90) | |
G_6 = Frequency.noteNumber(91) | |
G_SHARP_6 = A_FLAT_6 = Frequency.noteNumber(92) | |
A_6 = Frequency.noteNumber(93) | |
A_SHARP_6 = B_FLAT_6 = Frequency.noteNumber(94) | |
B_6 = Frequency.noteNumber(95) | |
DOUBLE_HIGH_C = C_7 = Frequency.noteNumber(96) | |
C_SHARP_7 = D_FLAT_7 = Frequency.noteNumber(97) | |
D_7 = Frequency.noteNumber(98) | |
D_SHARP_7 = E_FLAT_7 = Frequency.noteNumber(99) | |
E_7 = Frequency.noteNumber(100) | |
F_7 = Frequency.noteNumber(101) | |
F_SHARP_7 = G_FLAT_7 = Frequency.noteNumber(102) | |
G_7 = Frequency.noteNumber(103) | |
G_SHARP_7 = A_FLAT_7 = Frequency.noteNumber(104) | |
A_7 = Frequency.noteNumber(105) | |
A_SHARP_7 = B_FLAT_7 = Frequency.noteNumber(106) | |
B_7 = Frequency.noteNumber(107) | |
C_8 = Frequency.noteNumber(108) | |
C_SHARP_8 = D_FLAT_8 = Frequency.noteNumber(109) | |
D_8 = Frequency.noteNumber(110) | |
D_SHARP_8 = E_FLAT_8 = Frequency.noteNumber(111) | |
E_8 = Frequency.noteNumber(112) | |
F_8 = Frequency.noteNumber(113) | |
F_SHARP_8 = G_FLAT_8 = Frequency.noteNumber(114) | |
G_8 = Frequency.noteNumber(115) | |
G_SHARP_8 = A_FLAT_8 = Frequency.noteNumber(116) | |
A_8 = Frequency.noteNumber(117) | |
A_SHARP_8 = B_FLAT_8 = Frequency.noteNumber(118) | |
B_8 = Frequency.noteNumber(119) | |
### | |
TinyRaveTimer | |
-------------------------- | |
The TinyRave library provides a custom sample accurate implementation of | |
setInterval / setTimeout / clearTimeout. Any specified callbacks will preempt | |
audio rendering allowing you to modify your environment with sample-level | |
time resolution. | |
I recommend using the DSL provided by buildTrack(), which atomatically manages | |
callback registration and unregistration to simplify the process of creating | |
short-lived loops. | |
See the `@every()`, `@after()` and `@until()` methods here: | |
https://emcmanus.gitbooks.io/tinyrave-libraries/content/timers.html | |
This timer is optimized to handle 1000's of callbacks, which is useful for | |
tracks that front-load the scheduling of notes, like the MIDI adapter. | |
### | |
class TinyRaveTimer | |
constructor: -> | |
@callbackDescriptors = [] | |
@lastId = 1 | |
@time = 0 # Initialize to 0 so any callers using getTime() can correctly | |
# perform offset math. | |
# This is an optimization that allows us to skip most calls to | |
# fireCallbacks(). Maintain the lowest time threshold of our descriptors and | |
# sleep until that time is reached. | |
@nextThreshold = 0 | |
getTime: -> | |
@time | |
setTime: (time) -> | |
# Time only advances | |
if time > @time || time == 0 | |
@time = time | |
if @time >= @nextThreshold | |
@fireCallbacks() | |
@updateThreshold() # B/C descriptors reset their registration time when isLoop = true | |
else | |
throw new Error "Time invalid." | |
time | |
# Callbacks should fire in the order the timers were created. | |
registerCallback: (callback, interval, isLoop=false) -> | |
id = @lastId++ | |
@callbackDescriptors.push { id: id, callback: callback, interval: interval, registrationTime: @time, isLoop: isLoop } | |
@invalidateThreshold() | |
id | |
unregisterCallback: (id) -> | |
for descriptor, i in @callbackDescriptors | |
if descriptor.id == id | |
@callbackDescriptors.splice i, 1 | |
@invalidateThreshold() | |
break | |
# Find next elegible timer. If a loop, re-queue after firing. | |
dequeueNextDescriptor: -> | |
for descriptor, i in @callbackDescriptors | |
fireThreshold = descriptor.registrationTime + descriptor.interval | |
if @time >= fireThreshold | |
if descriptor.isLoop | |
descriptor.registrationTime = fireThreshold | |
else | |
@callbackDescriptors.splice(i, 1) | |
return descriptor | |
# By design callbacks can clear timers scheduled to run in the current tick. | |
# We need to iterate over the full array, from the beginning, since we don't | |
# know how the state of the array has changed after firing each callback. | |
fireCallbacks: -> | |
while descriptor = @dequeueNextDescriptor() | |
descriptor.callback.apply(undefined) | |
invalidateThreshold: -> | |
@nextThreshold = 0 | |
updateThreshold: -> | |
@nextThreshold = Number.POSITIVE_INFINITY | |
for callback in @callbackDescriptors | |
@nextThreshold = Math.min( @nextThreshold, callback.registrationTime + callback.interval ) | |
invalidateBeatLength: -> | |
for descriptor in @callbackDescriptors | |
if descriptor.interval.hasValueInBeats() | |
descriptor.interval = descriptor.interval.beats() | |
@invalidateThreshold() | |
# All timer DSL functions (every, until, after) are called with an instance of | |
# TopLevelScope or ShadowScope as `this.` Initially, we create a TopLevelScope | |
# with an expiration set to now() + delay. Any calls to setInterval / setTimeout | |
# from inside the functions will be cleared at the expiration time. The special | |
# case is `until`, which executes its callback in a new instance of | |
# ShadowScope, which is chained to the parent scope (an instance of | |
# TopLevelScope or ShadowScope [since `until` calls can be nested]). | |
# | |
# Doing this gets us two things: | |
# | |
# 1) An `expiration` shadow variable. When the timer methods run in an instance | |
# of ShadowScope, they will reference the most-local shadow copy of | |
# @expiration. This allows us to adjust the block expiration in nested | |
# calls and "unwind" the value as we exit nested scopes. | |
# | |
# 2) A version of `this` that will still resolve instance variables. If you | |
# define any variables using `this` they will be accessible in other timer | |
# callbacks. | |
class TopLevelScope | |
constructor: (duration) -> | |
@expiration = TinyRave.timer.getTime() + duration | |
every: (delay, callback) -> | |
callback.displayName ||= "Every Block" | |
@until(delay, callback) | |
@withExpiration( | |
setInterval((=> @until(delay, callback)), delay) | |
) | |
after: (delay, callback) -> | |
callback.displayName ||= "After Block" | |
@withExpiration( | |
setTimeout((=> callback.apply(@)), delay) | |
) | |
until: (delay, callback) -> | |
callback.displayName ||= "Until Block" | |
newScope = @createUntilScope(delay) | |
callback.apply(newScope) | |
# You can chain until() calls and they'll run sequentially | |
class Chain | |
constructor: (@reference, @delay) -> | |
until: (_delay, _callback) => | |
@reference.after(@delay, => | |
@reference.until(_delay, _callback) | |
) | |
new Chain(@reference, @delay + _delay) | |
new Chain(@, delay) | |
# | |
# Internal API: | |
withExpiration: (id) -> | |
expirationCallback = => clearInterval(id) | |
setTimeout(expirationCallback, @expiration - TinyRave.timer.getTime()) | |
createUntilScope: (delay) -> | |
# Delay cannot exceed parent (existing) scope expiration | |
expiration = Math.min(TinyRave.timer.getTime() + delay, @expiration) | |
# The new scope creates a shadow var expiration, so timer functions will | |
# see the local scope's value and behave apporopriately in nested calls | |
ShadowScope.prototype = @ | |
new ShadowScope(expiration) | |
# For expiration shadow variable | |
class ShadowScope | |
constructor: (@expiration) -> | |
# buildTrack() provides an instance of BuildTrackEnvironment as `this`. Since it | |
# extends TopLevelScope, you also get the `every` / `after` / `until` functions. | |
class BuildTrackEnvironment extends TopLevelScope | |
constructor: -> | |
@setBPM(120) | |
@mixer = new GlobalMixer | |
super(60 * 60 * 24 * 365 * 10) # 10 yrs | |
# - | |
setBPM: (bpm) -> | |
TinyRave.setBPM(bpm) | |
getBPM: -> | |
TinyRave.getBPM() | |
# - | |
getMixer: -> | |
@mixer | |
# - | |
getMasterGain: -> | |
@mixer.getGain() | |
setMasterGain: (gain) -> | |
@mixer.setGain(gain) | |
# - | |
play: (buildSampleClosure) -> | |
duration = buildSampleClosure.duration || @expiration - TinyRave.timer.getTime() | |
@mixer.mixFor duration, buildSampleClosure | |
# GlobalMixer is a Mixer instance that exists for the life of the track when | |
# the track defines a `buildTrack` function. This mixer instance maintains | |
# an array of all sound generating functions, and every 100ms iterates the array | |
# to remove any functions that have stopped generating audio (as determined by | |
# the function's `duration` property). It's strongly recommended that any | |
# functions passed into mixFor provide a duration, since this allows us to | |
# optimize the mixer. Note: if you use the @play method of `buildTrack` we can | |
# do a reasonable job inferring a function's duration from the current scope. | |
class GlobalMixer | |
constructor: -> | |
@pruneInterval = 0.100 | |
@lastPruneAt = 0 | |
@mixableDescriptors = [] | |
@time = 0 # This assumes the mixer will start at time 0! | |
@setGain(-7) | |
getGain: -> @gain | |
setGain: (@gain=-7.0) -> | |
@multiplier = Math.pow(10, @gain / 20) | |
prune: -> | |
i = @mixableDescriptors.length - 1 | |
while (i >= 0) | |
mixable = @mixableDescriptors[i] | |
@mixableDescriptors.splice(i, 1) if mixable.expiresAt < @time | |
i-- | |
@lastPruneAt = @time | |
buildSample: (@time) -> | |
@prune() if @time >= @lastPruneAt + @pruneInterval | |
sample = 0 | |
for descriptor in @mixableDescriptors when descriptor.expiresAt >= @time | |
sample += @multiplier * descriptor.buildSample(@time - descriptor.createdAt, @time) | |
sample | |
mixFor: (duration, buildSampleClosure) -> | |
console.error "Must specify duration in push() call" unless duration? | |
console.error "Must specify function in push() call" unless buildSampleClosure? | |
@mixableDescriptors.push { | |
createdAt: @time | |
expiresAt: @time + duration, | |
buildSample: buildSampleClosure | |
} | |
# | |
# TinyRave Namespace | |
TinyRave = { | |
timer: new TinyRaveTimer() | |
setBPM: (@BPM) -> | |
@timer.invalidateBeatLength() | |
getBPM: -> | |
@BPM | |
initializeBuildTrack: -> | |
# Called when the adapter detects buildTrack but no buildSample | |
environment = new BuildTrackEnvironment | |
mixer = environment.getMixer() | |
buildTrack.apply(environment) | |
self.buildSample = (time) -> | |
mixer.buildSample(time) | |
} | |
# Sample-accurate replacements for setInterval / setTimeout / clearInterval | |
setInterval = (callback, delay) -> | |
TinyRave.timer.registerCallback(callback, delay, true) | |
setTimeout = (callback, delay) -> | |
TinyRave.timer.registerCallback(callback, delay, false) | |
# Accepts any ID returned by setInterval or setTimeout. | |
clearInterval = (id) -> | |
TinyRave.timer.unregisterCallback(id) | |
# Core Extensions | |
# We can treat 5.beats() as a value in seconds and recover the correct duration | |
# if BPM changes. After `setBPM()`, call `number.beats()` if | |
# `number.hasValueInBeats()`. See `invalidateBeatLength()` implementation for | |
# usage. | |
Number.prototype.beats = Number.prototype.beat = -> | |
valueInBeats = this.valueInBeats || this | |
seconds = new Number(valueInBeats / (TinyRave.BPM / 60)) | |
seconds.valueInBeats = valueInBeats | |
seconds | |
# Whether this number instance was ever generated as the result of a call to | |
# beat or beats(). | |
Number.prototype.hasValueInBeats = -> | |
this.valueInBeats? |