Skip to content

Commit

Permalink
--wip--
Browse files Browse the repository at this point in the history
  • Loading branch information
wolfbiter committed Oct 15, 2016
1 parent ef8d234 commit 47ef30a
Show file tree
Hide file tree
Showing 13 changed files with 134 additions and 76 deletions.
21 changes: 8 additions & 13 deletions app/lib/soundtouch.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ FifoSampleBuffer.prototype.clear = function() {
// TODO(TECHDEBT): window.BUFFER_SIZE set by mix builder
window.MAX_BUFFER_SIZE = 16384;
window.BUFFER_SIZE = window.MAX_BUFFER_SIZE / 8;
const DSP_BUFFER_LENGTH = 128;
const SAMPLE_DRIFT_TOLERANCE = 512;

export function SoundtouchBufferSource(buffer) {
Expand Down Expand Up @@ -69,8 +70,7 @@ SoundtouchBufferSource.prototype = {
}
};

// TODO(TRACKMULTIGRID): audioBpm not a constant
export function createSoundtouchNode({ audioContext, filter, startTime, offsetTime, endTime, audioBpm, defaultPitch }) {
export function createSoundtouchNode({ audioContext, filter, startTime, offsetTime, endTime, defaultPitch }) {
const channelCount = 2;
const windowBufferSize = window.BUFFER_SIZE;

Expand All @@ -80,15 +80,14 @@ export function createSoundtouchNode({ audioContext, filter, startTime, offsetTi
return;
}

audioBpm = isValidNumber(audioBpm) ? audioBpm : 128; // TODO(TECHDEBT): share default bpm

const samples = new Float32Array(windowBufferSize * channelCount);
const sampleRate = audioContext.sampleRate || 44100;
const startSample = ~~(offsetTime * sampleRate);

filter.sourcePosition = startSample;

const filterStartPosition = filter.position;
const soundtouch = filter.pipe;

function onaudioprocess({
type,
Expand All @@ -103,14 +102,9 @@ export function createSoundtouchNode({ audioContext, filter, startTime, offsetTi
const l = outputs[0][0];
const r = outputs[0][1];

// naively take first pitch value for this sample
// TODO(MULTIGRID): average tempo, pitch across buffer
const pitch = parameters.pitch && parameters.pitch[0];
const soundtouch = filter.pipe;

// TODO(MULTIGRID): need to minimize dspBufLength.
// is it possible to align dspBufLength with automation clip TICKs?
const syncBpm = parameters.bpm && parameters.bpm[0];
const tempo = (isValidNumber(syncBpm) && isValidNumber(audioBpm)) ? (syncBpm / audioBpm) : 1;
const tempo = parameters.tempo && parameters.tempo[0];

if (isValidNumber(pitch)) {
soundtouch.pitchSemitones = pitch;
Expand Down Expand Up @@ -173,14 +167,15 @@ export function createSoundtouchNode({ audioContext, filter, startTime, offsetTi
numberOfInputs: 2,
numberOfOutputs: 2,
bufferLength: windowBufferSize,
dspBufLength: DSP_BUFFER_LENGTH,
parameters: [
{
name: 'pitch',
defaultValue: defaultPitch,
},
{
name: 'bpm',
defaultValue: 128,
name: 'tempo',
defaultValue: 1,
},
{
name: 'isPlaying',
Expand Down
3 changes: 1 addition & 2 deletions app/lib/web-audio/soundtouch-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default Ember.ObjectProxy.extend(
outputNode: null,

// TODO(V2): transpose dynamic
start(startTime, offsetTime, endTime, audioBpm, transpose) {
start(startTime, offsetTime, endTime, transpose) {
// Ember.Logger.log('currentTime', this.get('audioContext.currentTime'));
// Ember.Logger.log('startSource', startTime, offsetTime);
this.stop();
Expand All @@ -36,7 +36,6 @@ export default Ember.ObjectProxy.extend(
startTime,
offsetTime,
endTime,
audioBpm,
defaultPitch: transpose,
});
this.set('node', node);
Expand Down
20 changes: 19 additions & 1 deletion app/mixins/playable-arrangement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Ember from 'ember';

import d3 from 'd3';

import withDefault from 'linx/lib/computed/with-default';
import Metronome from './playable-arrangement/metronome';
import WebAudioMergerNode from 'linx/lib/web-audio/merger-node';
Expand All @@ -14,7 +16,7 @@ export default Ember.Mixin.create({

// required params
clips: null,
bpmScale: null, // or beatGrid
bpmControlPoints: null,
audioContext: null,

// optional params
Expand Down Expand Up @@ -57,6 +59,22 @@ export default Ember.Mixin.create({
'beatGrid': 'beatGrid',
}),

bpmScale: Ember.computed('bpmControlPoints.@each.{beat,value}', 'beatCount', function() {
const beatCount = this.get('beatCount');
const bpmControlPoints = this.get('bpmControlPoints');

const bpmControlBeats = bpmControlPoints.mapBy('beat');
const bpmControlValues = bpmControlPoints.mapBy('value');

const firstBpmValue = bpmControlValues.get('firstObject');
const lastBpmValue = bpmControlValues.get('lastObject');

return d3.scale.linear()
.domain([0].concat(bpmControlBeats).concat([beatCount]))
.range([firstBpmValue].concat(bpmControlValues).concat([lastBpmValue]))
.clamp(true);
}),

beatGrid: Ember.computed('bpmScale', 'beatCount', 'timeSignature', function() {
return BeatGrid.create(this.getProperties('bpmScale', 'beatCount', 'timeSignature'));
}),
Expand Down
4 changes: 3 additions & 1 deletion app/mixins/playable-arrangement/automatable-clip/control.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import RequireAttributes from 'linx/lib/require-attributes';
import { isValidNumber } from 'linx/lib/utils';

export const CONTROL_TYPE_VOLUME = 'gain';
export const CONTROL_TYPE_BPM = 'bpm';
export const CONTROL_TYPE_BPM = 'bpm'; // currently only exists in transition
export const CONTROL_TYPE_TEMPO = 'tempo';
export const CONTROL_TYPE_PITCH = 'pitch';
export const CONTROL_TYPE_DELAY_WET = 'delay-wet';
export const CONTROL_TYPE_DELAY_CUTOFF = 'delay-cutoff';
Expand All @@ -17,6 +18,7 @@ export const CONTROL_TYPE_FILTER_LOWPASS_CUTOFF = 'filter-lowpass-cutoff';
export const CONTROL_TYPES = [
CONTROL_TYPE_VOLUME,
CONTROL_TYPE_BPM,
CONTROL_TYPE_TEMPO,
CONTROL_TYPE_PITCH,
CONTROL_TYPE_DELAY_WET,
CONTROL_TYPE_DELAY_CUTOFF,
Expand Down
20 changes: 9 additions & 11 deletions app/mixins/playable-arrangement/track-clip.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import {
default as AutomatableClipControlMixin,
CONTROL_TYPE_VOLUME,
CONTROL_TYPE_BPM,
CONTROL_TYPE_TEMPO,
CONTROL_TYPE_PITCH,
CONTROL_TYPE_DELAY_WET,
CONTROL_TYPE_DELAY_CUTOFF,
Expand All @@ -43,11 +43,11 @@ const TrackVolumeControl = Ember.Object.extend(
defaultValue: 1,
});

const TrackBpmControl = Ember.Object.extend(
AutomatableClipControlMixin('soundtouchNode.bpm'), {
const TrackTempoControl = Ember.Object.extend(
AutomatableClipControlMixin('soundtouchNode.tempo'), {

type: CONTROL_TYPE_BPM,
defaultValue: 128, // TODO(TECHDEBT): bpm default constant used in many places
type: CONTROL_TYPE_TEMPO,
defaultValue: 1,
});

const TrackPitchControl = Ember.Object.extend(
Expand Down Expand Up @@ -110,7 +110,7 @@ export default Ember.Mixin.create(
controls: Ember.computed(function() {
return [
TrackVolumeControl.create({ clip: this }),
TrackBpmControl.create({ clip: this }),
TrackTempoControl.create({ clip: this }),
TrackPitchControl.create({ clip: this }),
TrackDelayWetControl.create({ clip: this }),
TrackDelayCutoffControl.create({ clip: this }),
Expand Down Expand Up @@ -140,7 +140,6 @@ export default Ember.Mixin.create(
audioBeatCount: subtract('audioEndBeat', 'audioStartBeat'),
audioDuration: subtract('audioEndTime', 'audioStartTime'),
audioBarCount: subtract('audioEndBar', 'audioStartBar'),
audioBpm: Ember.computed.reads('audioMeta.bpm'),

getCurrentAudioBeat() {
const currentClipBeat = this.getCurrentClipBeat();
Expand All @@ -163,14 +162,13 @@ export default Ember.Mixin.create(
return audioBeatGrid.beatToTime(audioStartBeat + clipBeat);
},

// TODO(TRACKMULTIGRID): audioBpm not constant
audioScheduleDidChange: Ember.observer('audioBinary.isReady', 'audioStartBeat', 'audioBeatCount', 'audioBpm', 'transpose', 'gain', function() {
audioScheduleDidChange: Ember.observer('audioBinary.isReady', 'audioStartBeat', 'audioBeatCount', 'transpose', 'gain', function() {
Ember.run.once(this, 'startSource');
}).on('schedule'),

startSource() {
if (this.get('isScheduled') && this.get('audioBinary.isReady')) {
const { audioBpm, transpose } = this.getProperties('audioBpm', 'transpose');
const { transpose } = this.getProperties('transpose');
// if starting in past, start now instead
let startTime = Math.max(this.getAbsoluteTime(), this.getAbsoluteStartTime());
let offsetTime = this.getCurrentAudioTime();
Expand All @@ -184,7 +182,7 @@ export default Ember.Mixin.create(

Ember.Logger.log('startTrack', this.get('track.title'), startTime, offsetTime, endTime, transpose);
const node = this.get('soundtouchNode');
node && node.start(startTime, offsetTime, endTime, audioBpm, transpose);
node && node.start(startTime, offsetTime, endTime, transpose);
} else {
this.stopSource();
}
Expand Down
41 changes: 13 additions & 28 deletions app/models/mix.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { flatten, isValidNumber } from 'linx/lib/utils';
export default DS.Model.extend(
CrudMixin,
PlayableArrangementMixin,
OrderedHasManyMixin('_mixItems'), {
new OrderedHasManyMixin('_mixItems'), {

// implement ordered has many
orderedHasManyItemModelName: 'mix/item',
Expand All @@ -28,33 +28,7 @@ export default DS.Model.extend(
// implement playable-arrangement
session: Ember.inject.service(),
audioContext: Ember.computed.reads('session.audioContext'),
bpmScale: Ember.computed('_bpmControlPoints.@each.{beat,value}', 'beatCount', function() {
const beatCount = this.get('beatCount');
const bpmControlPoints = this.get('_bpmControlPoints');

const bpmControlBeats = bpmControlPoints.mapBy('beat');
const bpmControlValues = bpmControlPoints.mapBy('value');

const firstBpmValue = bpmControlValues.get('firstObject');
const lastBpmValue = bpmControlValues.get('lastObject');

return d3.scale.linear()
.domain([0].concat(bpmControlBeats).concat([beatCount]))
.range([firstBpmValue].concat(bpmControlValues).concat([lastBpmValue]))
.clamp(true);
}),

tracks: Ember.computed.mapBy('items', 'track'),
transitions: Ember.computed.mapBy('items', 'transition'),

trackClips: Ember.computed.mapBy('items', 'trackClip'),
transitionClips: Ember.computed.mapBy('items', 'transitionClip'),
clips: Ember.computed.uniq('trackClips', 'transitionClips'),

_transitionBpmControlPoints: Ember.computed('[email protected]', function() {
return flatten(this.get('transitions').without(undefined).mapBy('bpmControlPoints')).without(undefined);
}),
_bpmControlPoints: Ember.computed('bpm',
bpmControlPoints: Ember.computed('bpm',
'_transitionBpmControlPoints.@each.{beat,value,transitionStartBeat}', function() {
const bpmControlPoints = this.get('_transitionBpmControlPoints');

Expand Down Expand Up @@ -84,6 +58,17 @@ export default DS.Model.extend(
}
}),

tracks: Ember.computed.mapBy('items', 'track'),
transitions: Ember.computed.mapBy('items', 'transition'),

trackClips: Ember.computed.mapBy('items', 'trackClip'),
transitionClips: Ember.computed.mapBy('items', 'transitionClip'),
clips: Ember.computed.uniq('trackClips', 'transitionClips'),

_transitionBpmControlPoints: Ember.computed('[email protected]', function() {
return flatten(this.get('transitions').without(undefined).mapBy('bpmControlPoints')).without(undefined);
}),

trackAt(index) {
const item = this.objectAt(index);
return item && item.get('track.content');
Expand Down
41 changes: 30 additions & 11 deletions app/models/mix/transition.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import _ from 'npm:underscore';
import ReadinessMixin from 'linx/mixins/readiness';
import PlayableArrangementMixin from 'linx/mixins/playable-arrangement';
import DependentRelationshipMixin from 'linx/mixins/models/dependent-relationship';
import withDefaultModel from 'linx/lib/computed/with-default-model';

import {
CONTROL_TYPE_VOLUME,
Expand All @@ -19,10 +20,12 @@ import { isValidNumber } from 'linx/lib/utils';

export default DS.Model.extend(
PlayableArrangementMixin,
DependentRelationshipMixin('fromTrackAutomationClips'),
DependentRelationshipMixin('toTrackAutomationClips'),
DependentRelationshipMixin('bpmClip'),
ReadinessMixin('isTransitionReady'), {
new DependentRelationshipMixin('fromTrackAutomationClips'),
new DependentRelationshipMixin('toTrackAutomationClips'),
new DependentRelationshipMixin('fromTrackTempoClip'),
new DependentRelationshipMixin('toTrackTempoClip'),
new DependentRelationshipMixin('bpmClip'),
new ReadinessMixin('isTransitionReady'), {

title: DS.attr('string'),
description: DS.attr('string'),
Expand All @@ -33,21 +36,37 @@ export default DS.Model.extend(
fromTrackClip: Ember.computed.reads('transitionClip.fromTrackClip'),
toTrackClip: Ember.computed.reads('transitionClip.toTrackClip'),

_fromTrackTempoClip: DS.belongsTo('mix/transition/from-track-tempo-clip', { async: true }),
fromTrackTempoClip: withDefaultModel('_fromTrackTempoClip', function() {
return this.get('store').createRecord('mix/transition/from-track-tempo-clip', {
transition: this,
});
}),

_toTrackTempoClip: DS.belongsTo('mix/transition/to-track-tempo-clip', { async: true }),
toTrackTempoClip: withDefaultModel('_toTrackTempoClip', function() {
return this.get('store').createRecord('mix/transition/to-track-tempo-clip', {
transition: this,
});
}),

fromTrackAutomationClips: DS.hasMany('mix/transition/from-track-automation-clip'),
toTrackAutomationClips: DS.hasMany('mix/transition/to-track-automation-clip'),

// implementing PlayableArrangement
audioContext: Ember.computed.reads('transitionClip.audioContext'),
outputNode: Ember.computed.reads('transitionClip.outputNode.content'),
clips: Ember.computed.uniq('fromTrackAutomationClips', 'toTrackAutomationClips', '_bpmClips'),
beatGrid: Ember.computed.reads('transitionClip.mix.beatGrid'),
clips: Ember.computed.uniq('fromTrackAutomationClips', 'toTrackAutomationClips', '_tempoClips'),
bpmControlPoints: Ember.computed.reads('bpmClip.controlPoints'),

_bpmClips: Ember.computed('bpmClip', function() {
return [this.get('bpmClip')].filter((clip) => !!clip);
_tempoClips: Ember.computed('bpmClip', 'fromTrackTempoClip', 'toTrackTempoClip', function() {
return [
this.get('bpmClip'),
this.get('fromTrackTempoClip'),
this.get('toTrackTempoClip'),
].filter((clip) => !!clip);
}),

bpmControlPoints: Ember.computed.reads('bpmClip.controlPoints'),

// optimizes this transition, with given constraints
// TODO(REFACTOR2): rethink this. convert to ember-concurrency
optimize({
Expand Down Expand Up @@ -211,7 +230,7 @@ export default DS.Model.extend(
// TODO(TRACKMULTIGRID): needs update
let fromTrackBpm = this.get('fromTrackClip.track.audioMeta.bpm');
fromTrackBpm = isValidNumber(fromTrackBpm) ? fromTrackBpm : 128;
let toTrackBpm = this.get('fromTrackClip.track.audioMeta.bpm')
let toTrackBpm = this.get('fromTrackClip.track.audioMeta.bpm');
toTrackBpm = isValidNumber(toTrackBpm) ? toTrackBpm : fromTrackBpm;

bpmClip.addControlPoints([
Expand Down
14 changes: 7 additions & 7 deletions app/models/mix/transition/automation-clip.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,24 @@ export default AutomationClip.extend({
_controlPoints: DS.hasMany('mix/transition/automation-clip/control-point', { async: true }),

arrangement: Ember.computed.reads('transition'),
beatCount: Ember.computed.reads('transition.beatCount'),
transitionBeatCount: Ember.computed.reads('transition.beatCount'),

// transition automation-clip must have controlPoints within transition
_updateControlPoints: Ember.observer('beatCount', function() {
const beatCount = this.get('beatCount')
// Ember.Logger.log('_updateControlPoints', beatCount);
_updateControlPoints: Ember.observer('transitionBeatCount', function() {
const transitionBeatCount = this.get('transitionBeatCount')
// Ember.Logger.log('_updateControlPoints', transitionBeatCount);

if (isValidNumber(beatCount)) {
if (isValidNumber(transitionBeatCount)) {
this.get('controlPoints').forEach((controlPoint) => {
const oldBeat = controlPoint.get('beat');

let newBeat;
if (controlPoint.get('isFirstItem')) {
newBeat = 0;
} else if (controlPoint.get('isLastItem')) {
newBeat = beatCount;
newBeat = transitionBeatCount;
} else {
newBeat = clamp(0, oldBeat, beatCount);
newBeat = clamp(0, oldBeat, transitionBeatCount);
}

controlPoint.set('beat', newBeat);
Expand Down
1 change: 0 additions & 1 deletion app/models/mix/transition/from-track-automation-clip.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import DS from 'ember-data';
import MixTransitionAutomationClip from './automation-clip';

export default MixTransitionAutomationClip.extend({
transition: DS.belongsTo('mix/transition'),

// overrides
targetClip: Ember.computed.reads('transition.fromTrackClip'),
Expand Down
Loading

0 comments on commit 47ef30a

Please sign in to comment.