Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spike/mix variable tempo #401

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/components/mix-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export default Ember.Component.extend(
this.send('playpause');
})),

_exitTransitionOnEscape: Ember.on(keyDown('Escape'), makeKeybinding(function(e) {
this.send('selectTransition', null);
})),

_pauseMix: Ember.on('willDestroyElement', function() {
this.send('pause');
}),
Expand Down Expand Up @@ -300,6 +304,7 @@ export default Ember.Component.extend(

seekToBeat(beat) {
const quantizedBeat = this._quantizeBeat(beat);
console.log('seekToBeat', beat, quantizedBeat);

this.get('mix').seekToBeat(quantizedBeat);
this.trigger('seekToBeat', quantizedBeat);
Expand Down
15 changes: 10 additions & 5 deletions app/components/mix-builder/precision-controls/track.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ const KeyboardBeatJumpMixin = Ember.Mixin.create(BEAT_JUMP_KEYBINDINGS.reduce(
{}
));

const SECONDS_TO_ANALYZE = 20;

export default Ember.Component.extend(
KeyboardBeatJumpMixin,
EKMixin,
Expand All @@ -134,8 +136,6 @@ export default Ember.Component.extend(
const clip = this.get('clip');
const beatGrid = clip.get('audioMeta.beatGrid');

console.log('beatJump', beats, direction, isNudge, clip.get('track.title'));

if (isNudge || this.get('isToTrackClip')) {
const audioStartBeat = clip.get('audioStartBeat');
const newAudioStartBeat = audioStartBeat - (beats * direction);
Expand Down Expand Up @@ -167,13 +167,18 @@ export default Ember.Component.extend(
analyzeTrack() {
const task = this.get('beatDetection.analyzeTrackTask');
const track = this.get('track');
task.perform(track).then(({ peaks, intervals }) => {
const trackClip = this.get('clip');
const trackClip = this.get('clip.content');
const currentAudioTime = trackClip.getCurrentAudioTime();
console.log({ currentAudioTime})

task.perform(track, {
startTime: currentAudioTime - SECONDS_TO_ANALYZE / 2,
endTime: currentAudioTime + SECONDS_TO_ANALYZE / 2,
}).then(({ peaks, intervals }) => {

console.log('analyze track markers', peaks, intervals);
trackClip.setProperties({
markers: peaks,
// audioStartTime: peaks[0].time,
});
});
},
Expand Down
40 changes: 36 additions & 4 deletions app/components/mix-visual/track-clip.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import MixVisualClipMixin from 'linx/mixins/components/mix-visual/clip';

import { constantTernary, propertyOrDefault } from 'linx/lib/computed/ternary';
import { FROM_TRACK_COLOR, TO_TRACK_COLOR } from 'linx/components/mix-builder';
import { isValidNumber } from 'linx/lib/utils';

const MARKER_CLICK_WINDOW = 0.05; // [s] how close to a marker a click has to be

export default ArrangementVisualTrackClip.extend(
MixVisualClipMixin, {
Expand All @@ -16,9 +19,12 @@ export default ArrangementVisualTrackClip.extend(

actions: {
onClick() {
// if (this.get('selectedTransition')) {
if (this.get('selectedTransition')) {
Ember.run.next(() => {
this._checkForMarkerClick();
});
this.sendAction('selectClip', this.get('clip'));
// }
}
},

onDrag(d3Context, d, dBeats) {
Expand All @@ -34,6 +40,32 @@ export default ArrangementVisualTrackClip.extend(
},
},

// if marker was clicked, set as beatgrid and clear markers
_checkForMarkerClick() {
const trackClip = this.get('clip.content');
const currentAudioTime = trackClip.getCurrentAudioTime();
const markers = trackClip.get('markers') || [];

const marker = markers.find(({ time }) => {
return (currentAudioTime - MARKER_CLICK_WINDOW <= time) &&
(currentAudioTime + MARKER_CLICK_WINDOW >= time);
});

// TODO(TRACKMULTIGRID): refactor to use dynamic beatgrids instead of static time
if (marker && isValidNumber(marker.time)) {
console.log('clicked marker', marker);

const arrangement = trackClip.get('arrangement.content');
const quantizedBeat = arrangement.get('beatGrid').quantizeBeat(arrangement.getCurrentBeat());
const quantizedAudioTime = trackClip.getAudioTimeFromArrangementBeat(quantizedBeat);

const prevAudioStartTime = trackClip.get('audioStartTime');
const timeDelta = quantizedAudioTime - marker.time;
trackClip.set('audioStartTime', prevAudioStartTime - timeDelta);
trackClip.set('markers', []);
}
},

// used to keep track of where audio was when drag started
_dragStartBeat: 0,

Expand All @@ -51,8 +83,8 @@ export default ArrangementVisualTrackClip.extend(
row: constantTernary('isSelectedToTrackClip', 2, 0),

waveColor: Ember.computed('isSelectedFromTrackClip', 'isSelectedToTrackClip', function() {
if (this.get('isSelectedFromTrackClip')) return FROM_TRACK_COLOR;
if (this.get('isSelectedToTrackClip')) return TO_TRACK_COLOR;
if (this.get('isSelectedFromTrackClip')) { return FROM_TRACK_COLOR; }
if (this.get('isSelectedToTrackClip')) { return TO_TRACK_COLOR; }
return 'steelblue';
}),

Expand Down
16 changes: 8 additions & 8 deletions app/initializers/catch-errors.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Ember from 'ember';

// all uncaught errors will be caught here
// you can use `message` to make sure it's the error you're looking for
// returning true overrides the default window behaviour
window.onerror = function(message, file, lineNumber, columnNumber, error) {
console.warn(message, error && error.stack);
window.error = error;
return true;
};
// // all uncaught errors will be caught here
// // you can use `message` to make sure it's the error you're looking for
// // returning true overrides the default window behaviour
// window.onerror = function(message, file, lineNumber, columnNumber, error) {
// console.warn(message, error && error.stack);
// window.error = error;
// return true;
// };

export default {
name: 'CatchErrors',
Expand Down
27 changes: 12 additions & 15 deletions app/lib/soundtouch.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ FifoSampleBuffer.prototype.clear = function() {
//
// TODO(TECHDEBT): window.BUFFER_SIZE set by mix builder
window.MAX_BUFFER_SIZE = 16384;
window.BUFFER_SIZE = MAX_BUFFER_SIZE / 8;
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,13 +70,12 @@ SoundtouchBufferSource.prototype = {
}
};

export function createSoundtouchNode({ audioContext, filter, startTime, offsetTime, endTime, defaultTempo, defaultPitch }) {
console.log('createSoundtouchNode')
export function createSoundtouchNode({ audioContext, filter, startTime, offsetTime, endTime, defaultPitch }) {
const channelCount = 2;
const windowBufferSize = window.BUFFER_SIZE;

if (!(audioContext && filter
&& isValidNumber(startTime) && isValidNumber(offsetTime) && isValidNumber(endTime))) {
if (!(audioContext && filter &&
isValidNumber(startTime) && isValidNumber(offsetTime) && isValidNumber(endTime))) {
Ember.Logger.warn('Must provide all params to createSoundtouchNode', endTime);
return;
}
Expand All @@ -87,6 +87,7 @@ export function createSoundtouchNode({ audioContext, filter, startTime, offsetTi
filter.sourcePosition = startSample;

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

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

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

if (isValidNumber(pitch)) {
soundtouch.pitchSemitones = pitch;
Expand All @@ -125,7 +125,7 @@ export function createSoundtouchNode({ audioContext, filter, startTime, offsetTi
// if playing, calculate expected vs actual position
if (extractFrameCount !== 0) {
const actualElapsedSamples = Math.max(0, filter.position - filterStartPosition + extractFrameCount);
const elapsedTime = Math.min(audioContext.currentTime, endTime) - startTime;
const elapsedTime = Math.min(playbackTime, endTime) - startTime;
const expectedElapsedSamples = Math.max(0, elapsedTime * sampleRate);
const sampleDelta = ~~(expectedElapsedSamples - actualElapsedSamples);

Expand All @@ -137,7 +137,7 @@ export function createSoundtouchNode({ audioContext, filter, startTime, offsetTi

// if we're behind where we should be, extract dummy frames to catch up
if (sampleDelta > 0) {
// console.log("DRIFT", sampleDelta, extractFrameCount, windowBufferSize);
// console.log("DRIFT", playbackTime, sampleDelta, extractFrameCount, windowBufferSize);
const dummySamples = new Float32Array(sampleDelta * channelCount);
const dummyFramesExtracted = filter.extract(dummySamples, sampleDelta);

Expand All @@ -158,27 +158,24 @@ export function createSoundtouchNode({ audioContext, filter, startTime, offsetTi
r[i] = (samples[filterFrame * 2 + 1] * isPlaying[i]) || 0;
filterFrame += isPlaying[i];
}
};
}

defaultPitch = parseFloat(defaultPitch);
defaultPitch = isValidNumber(defaultPitch) ? defaultPitch : 0;

defaultTempo = parseFloat(defaultTempo);
defaultTempo = isValidNumber(defaultTempo) ? defaultTempo : 1;

const node = new AudioWorkerNode(audioContext, onaudioprocess, {
numberOfInputs: 2,
numberOfOutputs: 2,
bufferLength: windowBufferSize,
dspBufLength: windowBufferSize,
dspBufLength: DSP_BUFFER_LENGTH,
parameters: [
{
name: 'pitch',
defaultValue: defaultPitch,
},
{
name: 'tempo',
defaultValue: defaultTempo,
defaultValue: 1,
},
{
name: 'isPlaying',
Expand Down
4 changes: 4 additions & 0 deletions app/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export const isValidNumber = function(number) {
return isNumber(number) && isFinite(number);
};

export const validNumberOrDefault = function(number, _default) {
return isValidNumber(number) ? number : _default;
};

export const isObject = function(object) {
return Ember.typeOf(object) === 'object';
};
Expand Down
2 changes: 1 addition & 1 deletion app/lib/web-audio/gain-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default Ember.ObjectProxy.extend(
const node = this.get('node');
const value = this.get('value');

console.log('update gain value', value, isValidNumber(value))
// console.log('update gain value', value, isValidNumber(value))
if (node && isValidNumber(value)) {
node.gain.value = value;
}
Expand Down
5 changes: 2 additions & 3 deletions app/lib/web-audio/soundtouch-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export default Ember.ObjectProxy.extend(
node: null, // set by `start` method, unset by `disconnect`
outputNode: null,

// TODO(V2): TODO(MULTIGRID): tempo, transpose dynamic
start(startTime, offsetTime, endTime, tempo, transpose) {
// TODO(V2): transpose dynamic
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,
defaultTempo: tempo,
defaultPitch: transpose,
});
this.set('node', node);
Expand Down
Loading