Last active
March 13, 2017 03:41
-
-
Save Guevara-chan/d9df1202e0e884c93c8b9ba7a291d8c1 to your computer and use it in GitHub Desktop.
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
EventEmitter = require('events') | |
require('clr').init {assemblies: ['CSCore.dll', 'System', 'mscorlib']} | |
Δimport = (namespace) -> Object.assign global, namespace | |
Δexport = (entity) -> module.exports[entity.name] = entity | |
Function::getter = (name, proc) -> Object.defineProperty @prototype, name, {get: proc, configurable: true} | |
Function::setter = (name, proc) -> Object.defineProperty @prototype, name, {set: proc, configurable: true} | |
#- Requires CSCore v.1.2.0+ (https://github.com/filoe/cscore) | |
Δimport CSCore | |
Δimport CSCore.Codecs | |
Δimport CSCore.SoundOut | |
Δimport CSCore.Tags.ID3 | |
#.{ [Classes] | |
Δexport class Record | |
# --Methods goes here. | |
constructor: (@origin) -> | |
if @origin instanceof Record then @origin.destroy(); @origin = @origin.origin | |
@origin = normalize_path @origin | |
@audio = Record.load_audio(@origin) | |
@meta = Object.assign Record.guess_title(@origin, @is_finite), (try Record.extract_metadata @origin) | |
Object.freeze @ | |
destroy: () -> | |
@audio.Dispose() | |
rewind: (progress = 0) -> | |
if @can_rewind | |
if progress.GetType?().Equals(System.TimeSpan) | |
Extensions.SetPosition(@audio, progress) | |
else if @is_finite or not progress | |
@offset = @audio.Length * progress / 100 | |
else throw new Error('unable to find position in infinite record') | |
return @ | |
normalize_path = (path) -> | |
if typeof path is 'string' then new System.Uri path else path | |
@load_audio: (origin) -> | |
# Service objects preparation. | |
origin = normalize_path origin | |
# Consumption phase: | |
if origin.IsFile | |
return CodecFactory.Instance.GetCodec origin | |
else | |
return new MP3.Mp3WebStream origin | |
@extract_metadata: (origin) -> | |
# Service objects preparation. | |
streamer = new System.Net.WebClient() | |
stream = streamer.OpenRead origin | |
# Actual metadata extraction (ID3). | |
if id3 = [ID3v2.FromStream(stream)?.QuickInfo, stream.Close(), streamer.Dispose()][0] | |
{ | |
title: id3.Title; album: id3.Album; artist: id3.Artist; lead_performers: id3.LeadPerformers | |
comments: id3.Comments; image: id3.Image; year: id3.Year; track_number: id3.TrackNumber | |
original_release: id3.OriginalReleaseYear; genre: id3.Genre | |
} | |
@guess_title: (origin, finity = true) -> | |
# Service objects preparation. | |
origin = normalize_path origin | |
# Guessing switch. | |
unless finity # (infinite stream -> probably an internet radio) | |
guess: "/" + [origin.Host | |
":" + origin.Port if origin.Port isnt -1 | |
origin.LocalPath if origin.LocalPath isnt '/'].filter(Boolean).join('') + "/" | |
heur: 'stream' | |
else if (local = origin.LocalPath) isnt '/' # (localizable path -> probably remote file) | |
guess: System.IO.Path.GetFileNameWithoutExtension local | |
heur: 'file' | |
else # (unable to to determine source -> no guesses) | |
guess: origin.ToString() | |
heur: "???" | |
# --Getters/setters. | |
@getter 'position', -> Extensions.GetPosition(@audio) | |
@getter 'length', -> Extensions.GetLength(@audio) if @is_finite | |
@getter 'progress', -> @audio.Position / @audio.Length * 100 if @is_finite | |
@getter 'title', -> if @meta.title then @meta.title else @meta.guess | |
@getter 'is_finished', -> if @is_finite then @position is @length else false | |
@getter 'is_finite', -> @audio.Length > 0 | |
@getter 'can_rewind', -> @audio.CanSeek | |
@getter 'offset', -> @audio.Position | |
@setter 'offset', (val) -> @audio.Position = val | |
# -------------------- # | |
Δexport class PlayList | |
# --Methods goes here. | |
constructor: (initial_list) -> | |
@listing = [] | |
@merge initial_list | |
Object.freeze @ | |
destroy: () -> | |
@clear() | |
move: (src_idx = @selected, dest_idx = 0) -> | |
if src_idx isnt dest_idx | |
@check_index dest_idx, 1 | |
if @selected is src_idx then @selected = dest_idx | |
else if dest_idx <= @selected then @selected = @selected + 1 | |
@listing.splice dest_dix, 0, @remove(src_idx) | |
add: (source, dest_idx = @length) -> | |
@listing.push {Δ: source} | |
@move @length-1, dest_idx | |
merge: (source_list) -> | |
if source_list instanceof PlayList then source_list = source_list.checkout() | |
else unless Array.isArray source_list then source_list = [source_list] | |
@add(src) for src in source_list | |
remove: (idx = @length-1) -> | |
@check_index idx | |
if idx <= @selected then @selected = Math.max(@selected - 1, 0) | |
return @listing.splice(idx, 1) | |
clear: () -> | |
@remove() while not @is_empty | |
sublink: (data, channel = '', idx = @selected) -> | |
@check_index idx | |
@listing[idx]["Δ#{channel}"] = data | |
request: (idx = @selected, channel = '') -> | |
@check_index idx | |
@listing[idx]["Δ#{channel}"] | |
checkout: () -> | |
return @listing | |
next: (shuffle = false) -> | |
@selected = | |
if shuffle then Math.random() * @length // 1 | |
else (@selected+1) % @length | |
check_index: (idx, edge = 0) -> | |
if idx < 0 then throw new Error "playlist index underflow" | |
if idx >= @length + edge then throw new Error "playlist index overflow" | |
# --Accumulators. | |
Δselected = 0 | |
# --Getters/setters. | |
@getter 'length', -> @listing.length | |
@getter 'is_empty', -> @length is 0 | |
@getter 'selected', -> Δselected | |
@setter 'selected', (val) -> @check_index val; Δselected = val | |
# -------------------- # | |
Δexport class MusicBox extends EventEmitter | |
# --Methods goes here. | |
constructor: (source_list, @random = false, @pulse_interval = 1000, @emitter = true) -> | |
@output = new WasapiOut() | |
@repertory = new PlayList | |
@load source_list, true | |
setInterval @tick.bind(@), 500 | |
destroy: () -> | |
@stop().empty().output.Dispose() | |
load: (source_list, refit) -> | |
@empty() if refit | |
@repertory.merge source_list | |
return @ | |
eject: (record_id = @repertory.current) -> | |
if @cache is @repertory.request record_id, 'cache' then @stop() | |
@repertory.remove(record_id) | |
return @ | |
empty: () -> | |
@stop().state = 'setup' | |
@repertory.clear() | |
return @ | |
rewind: (progress = 0) -> | |
@cache.rewind progress | |
return @ | |
next: () -> | |
@signal 'next_signal', @repertory.next(@random) | |
if @is_on then @play() else @stop() | |
return @ | |
play: (src = @repertory.selected) -> | |
# Necessary preparations. | |
@stop().state = 'playing' | |
if typeof src is 'number' | |
src = @repertory.sublink @repertory.request(src), "cache", list_idx = src | |
# Actual playing procedures. | |
@cache = new Record src | |
@output.Initialize(@cache.audio) | |
@output.Play() | |
# Event emitters. | |
@signal 'play_signal', @now_playing, list_idx | |
if @pulse_interval then @emit_pulse() | |
return @ | |
stop: () -> | |
@cache?.destroy() | |
@output.Stop() | |
@state = 'halt' | |
@cache = 0[@signal "stop_signal", @cache] | |
return @ | |
switch: (turn_on = @state isnt 'playing') -> | |
# Necessary check. | |
if @is_empty then @state = 'setup'; throw new Error "no records to setup playback" | |
# State-machine switching. | |
switch @state | |
when 'playing' then (@output.Pause(); @state = 'paused') unless turn_on | |
when 'paused' then (@output.Resume(); @state = 'playing') if turn_on | |
when 'idle' then @stop() unless turn_on | |
when 'halt', 'setup' then if turn_on | |
if @state = 'setup' and @random then @next() | |
@state = 'idle' | |
return @ | |
signal: (id, args...) -> | |
@emit id, args... if @emitter | |
emit_pulse: () -> | |
if status = @now_playing | |
@signal 'pulse', status | |
setTimeout @emit_pulse.bind(@), @pulse_interval | |
return @ | |
tick: () -> | |
try | |
if @is_on and not @now_playing | |
if not @is_empty | |
if @state is 'playing' then @next() else @play() | |
else @stop() | |
catch error | |
@stop().signal 'jam', error | |
return @ | |
# --Accumulators. | |
Δstate = '[I am error]' | |
Δcache = undefined | |
# --Getters/setters. | |
@getter 'now_playing', -> return @cache if @output.PlaybackState.Equals(PlaybackState.Playing) | |
@getter 'is_empty', -> @repertory.is_empty | |
@getter 'is_on', -> @state in ['playing', 'idle'] | |
@getter 'volume', -> @output.Volume | |
@setter 'volume', (val) -> @output.Volume = val | |
@getter 'state', -> Δstate | |
@setter 'state', (val) -> Δstate = val; @signal 'state_switch', val | |
@getter 'cache', -> Δcache | |
@setter 'cache', (val) -> Δcache = val; @signal 'cache_switch', val | |
# ==Usage sandbox== | |
(mbox = new MusicBox [ | |
"http://142.4.202.125:8006/128mp3" | |
"http://us4.internet-radio.com:8135" | |
"http://broadcast.rantradio.com:8888" | |
"http://50.97.94.44:8166" | |
"http://108.61.154.147:5986" | |
"http://212.83.172.11:8651" | |
"http://stream.chipbit.net:8000" | |
], true) | |
.on 'play_signal', (status, index) -> console.log "#{index+1} ~> [#{status.title}]" | |
.on 'jam', (error) -> console.log "-------/!/JAM/!/-------\n#{error.stack}"; process.exit() | |
.switch on | |
setInterval (-> mbox.next()), 1000 * 60 * 2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment