Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
407 lines (352 sloc) 15.3 KB
# 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?