wip jamtrack player

This commit is contained in:
Nuwan 2024-07-31 11:27:56 +05:30
parent 5b750cc3d9
commit 7af01a6c61
9 changed files with 569 additions and 66 deletions

View File

@ -3313,6 +3313,21 @@
}
}
},
"@fingerprintjs/fingerprintjs": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-4.4.3.tgz",
"integrity": "sha512-sm0ZmDp5Oeq8hQTf+bAHKsuuteVAYme/YOY9UPP/GrUBrR5Fzl1P5oOv6F5LvyBrO7qLjU5HQkfU0MmFte/8xA==",
"requires": {
"tslib": "^2.4.1"
},
"dependencies": {
"tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
}
}
},
"@fortawesome/fontawesome-common-types": {
"version": "0.2.35",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz",

View File

@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@farfetch/react-context-responsive": "^1.5.0",
"@fingerprintjs/fingerprintjs": "^4.4.3",
"@fortawesome/fontawesome-free": "^5.15.1",
"@fortawesome/fontawesome-svg-core": "^1.2.30",
"@fortawesome/free-brands-svg-icons": "^5.14.0",

View File

@ -0,0 +1,252 @@
import React, { useState, useEffect } from 'react';
import { Table, Row, Col, Input, Button } from 'reactstrap';
import JKInstrumentIcon from '../profile/JKInstrumentIcon';
import Select from 'react-select';
import { useForm, Controller } from 'react-hook-form';
import { createMixdown } from '../../helpers/rest';
const JKCreateCustomMix = ({ jamTrack }) => {
const [tracks, setTracks] = useState([]);
//const [selectedTracks, setSelectedTracks] = useState([]);
const TEMPO_OPTIONS = [
{ value: '0', label: 'Original tempo' },
{ value: '-5', label: 'Slower by 5%' },
{ value: '-10', label: 'Slower by 10%' },
{ value: '-15', label: 'Slower by 15%' },
{ value: '-20', label: 'Slower by 20%' },
{ value: '-25', label: 'Slower by 25%' },
{ value: '-30', label: 'Slower by 30%' },
{ value: '-35', label: 'Slower by 35%' },
{ value: '-40', label: 'Slower by 40%' },
{ value: '-45', label: 'Slower by 45%' },
{ value: '-50', label: 'Slower by 50%' },
{ value: '-60', label: 'Slower by 60%' },
{ value: '-70', label: 'Slower by 70%' },
{ value: '-80', label: 'Slower by 80%' },
{ value: '5', label: 'Faster by 5%' },
{ value: '10', label: 'Faster by 10%' },
{ value: '15', label: 'Faster by 15%' },
{ value: '20', label: 'Faster by 20%' },
{ value: '30', label: 'Faster by 30%' },
{ value: '40', label: 'Faster by 40%' },
{ value: '50', label: 'Faster by 50%' }
];
const PITCH_OPTIONS = [
{ value: '0', label: 'Original pitch' },
{ value: '-1', label: 'Down 1 semitone' },
{ value: '-2', label: 'Down 2 semitone' },
{ value: '-3', label: 'Down 3 semitone' },
{ value: '-4', label: 'Down 4 semitone' },
{ value: '-5', label: 'Down 5 semitone' },
{ value: '-6', label: 'Down 6 semitone' },
{ value: '-7', label: 'Down 7 semitone' },
{ value: '-8', label: 'Down 8 semitone' },
{ value: '-9', label: 'Down 9 semitone' },
{ value: '-10', label: 'Down 10 semitone' },
{ value: '-11', label: 'Down 11 semitone' },
{ value: '-12', label: 'Down 12 semitone' },
{ value: '1', label: 'Up 1 semitone' },
{ value: '2', label: 'Up 2 semitone' },
{ value: '3', label: 'Up 3 semitone' },
{ value: '4', label: 'Up 4 semitone' },
{ value: '5', label: 'Up 5 semitone' },
{ value: '6', label: 'Up 6 semitone' },
{ value: '7', label: 'Up 7 semitone' },
{ value: '8', label: 'Up 8 semitone' },
{ value: '9', label: 'Up 9 semitone' },
{ value: '10', label: 'Up 10 semitone' },
{ value: '11', label: 'Up 11 semitone' },
{ value: '12', label: 'Up 12 semitone' }
];
const {
control,
handleSubmit,
formState: { errors },
setValue,
getValues
} = useForm({
defaultValues: {
mixName: '',
tempo: {
value: '0',
label: 'Original tempo'
},
pitch: {
value: '0',
label: 'Original pitch'
},
mixdownTracks: []
}
});
const onSubmit = data => {
console.log('data', data);
const _tracks = [];
let countIn = false;
const selectedTracks = getValues('mixdownTracks');
tracks.forEach(track => {
const muted = selectedTracks.includes(track.id);
if (track.id === 'count-in') {
if (countIn === false) {
countIn = !muted;
}
} else {
_tracks.push({
id: track.id,
mute: selectedTracks.includes(track.id)
});
}
});
setValue('mixdownTracks', _tracks);
const mixData = {
jamTrackID: jamTrack.id,
name: data.mixName,
settings: { speed: data.tempo.value, pitch: data.pitch.value, 'count-in': countIn, tracks: _tracks }
};
console.log('mixData', mixData);
createMixdown(mixData)
.then(response => {
console.log('mixdown created', response);
//TODO: add this mixdown to global state of jamtrack mixdowns
})
.catch(error => {
console.error('mixdown create error', error);
});
};
const toggleTrack = e => {
const trackId = e.target.value;
const selectedTracks = getValues('mixdownTracks');
if (selectedTracks.includes(trackId)) {
//setSelectedTracks(selectedTracks.filter(track => track !== trackId));
setValue('mixdownTracks', selectedTracks.filter(track => track !== trackId));
} else {
//setSelectedTracks([...selectedTracks, trackId]);
setValue('mixdownTracks', [...selectedTracks, trackId]);
}
};
useEffect(() => {
if (jamTrack) {
setTracks(jamTrack.tracks.filter(track => track.track_type === 'Track' || track.track_type === 'Click'));
}
}, [jamTrack]);
const trackName = track => {
if (track.track_type === 'Track' || track.track_type === 'Click') {
if (track.track_type === 'Click') {
return 'Clicktrack';
} else if (track.instrument) {
const instrumentDescription = track.instrument.description;
let part = '';
if (track.part && track.part !== instrumentDescription) {
part = `(${track.part})`;
}
return `${instrumentDescription} ${part}`;
}
}
};
return (
<>
<p>
Mute any tracks you like. Adjust the pitch or tempo of playback. Then give your mix a descriptive name, and
click the Create Mix button. It will take few minutes for us to create your custom mix.
</p>
<form onSubmit={handleSubmit(onSubmit)}>
<Row>
<Col>
<Table striped bordered className="fs--1">
<thead className="bg-200 text-900">
<tr>
<th>Tracks {tracks.length > 0 && <>({tracks.length})</>}</th>
<th>Mute</th>
</tr>
</thead>
<tbody>
{tracks &&
tracks.map((track, index) => (
<tr key={index}>
<td>
<JKInstrumentIcon instrumentId={track.instrumentId} instrumentName={trackName(track)} />
<span className="ml-1">{trackName(track)}</span>
</td>
<td>
<input type="checkbox" value={track.id} onClick={toggleTrack} />
</td>
</tr>
))}
</tbody>
</Table>
<Controller
name="mixdownTracks"
control={control}
rules={{
required: 'Select at least one track to create a mix'
}}
render={({ field }) => <Input type='hidden' {...field} />}
/>
{errors.mixdownTracks && (
<div className="text-danger">
<small>{errors.mixdownTracks.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-1">
<Col>Tempo</Col>
<Col>
<Controller
name="tempo"
control={control}
render={({ field }) => <Select {...field} options={TEMPO_OPTIONS} />}
/>
</Col>
</Row>
<Row className="mb-1">
<Col>Pitch</Col>
<Col>
<Controller
name="pitch"
control={control}
render={({ field }) => <Select {...field} options={PITCH_OPTIONS} />}
/>
</Col>
</Row>
<Row className="mb-1">
<Col>Mix Name</Col>
<Col>
<Controller
name="mixName"
control={control}
rules={{
required: 'Mix name is required'
}}
render={({ field }) => <Input {...field} />}
/>
{errors.mixName && (
<div className="text-danger">
<small>{errors.mixName.message}</small>
</div>
)}
</Col>
</Row>
<Row>
<Col>
<Button>Create Mix</Button>
</Col>
</Row>
</form>
</>
);
};
export default JKCreateCustomMix;

View File

@ -6,6 +6,8 @@ import { Card, CardBody, Row, Col, Progress } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { getJamTrack, getUserDetail, postUserEvent, userOpenedJamTrackWebPlayer } from '../../helpers/rest';
import JKJamTrackPlayer from './JKJamTrackPlayer';
import JKMyJamTrackMixes from './JKMyJamTrackMixes';
import JKCreateCustomMix from './JKCreateCustomMix';
import { useAuth } from '../../context/UserAuth';
const JKJamTrack = () => {
@ -23,6 +25,7 @@ const JKJamTrack = () => {
setLoading(true);
const resp = await getJamTrack({ id });
const data = await resp.json();
console.log('jam track 123', data);
setJamTrack(data);
} catch (error) {
console.log('Error when fetching jam track', error);
@ -58,26 +61,24 @@ const JKJamTrack = () => {
fetchJamTrack();
}, [id]);
return (
<Row>
<Col>
<Card className="mx-auto mb-4">
<FalconCardHeader title={t('jamtrack.player.title')} titleClass="font-weight-semi-bold" />
<CardBody className="pt-3">
{jamTrack && <JKJamTrackPlayer jamTrack={jamTrack} />}
</CardBody>
<CardBody className="pt-3">{jamTrack && <JKJamTrackPlayer jamTrack={jamTrack} />}</CardBody>
</Card>
<Card className="mx-auto">
<FalconCardHeader title={t('jamtrack.my_mixes.title')} titleClass="font-weight-semi-bold" />
<CardBody className="pt-3" />
<CardBody className="pt-3">{jamTrack && <JKMyJamTrackMixes jamTrack={jamTrack} />}</CardBody>
</Card>
</Col>
<Col>
<Card className="mx-auto">
<FalconCardHeader title={t('jamtrack.create_mix.title')} titleClass="font-weight-semi-bold" />
<CardBody className="pt-3" />
<CardBody className="pt-3">
{ jamTrack && <JKCreateCustomMix jamTrack={jamTrack} /> }
</CardBody>
</Card>
</Col>
<Col />

View File

@ -0,0 +1,101 @@
import React, { useState, useEffect, useMemo } from 'react';
import Select from 'react-select';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Row, Col } from 'reactstrap';
import PropTypes from 'prop-types';
import { markMixdownActive } from '../../helpers/rest';
import useJamTrackAudio from '../../hooks/useJamTrackAudio';
const JKJamTrackPlayer = ({ jamTrack }) => {
const [mixes, setMixes] = useState([]);
const [options, setOptions] = useState([]);
const [selectedMix, setSelectedMix] = useState(null);
const { audioUrls, loadJamTrack } = useJamTrackAudio(jamTrack);
const handleChange = selectedOption => {
const mix = mixes.find(mix => mix.value === selectedOption.value);
setSelectedMix(mix);
};
useEffect(() => {
if (jamTrack) {
console.log('_JamTrack_ jamTrack', jamTrack);
const _opts = jamTrack.mixdowns.map(mix => ({ value: mix.id, label: mix.name }));
_opts.unshift({ value: 'original', label: 'Original' });
setOptions(_opts);
//set the default mix to the original
const activeMix = jamTrack.mixdowns.find(mix => mix.id === jamTrack.last_mixdown_id)
console.log('_JamTrack_ activeMix', activeMix);
setSelectedMix(activeMix);
}
}, [jamTrack]);
const activateMasterTrack = async () => {
console.log('playing original');
await markMixdownActive({ id: jamTrack.id, mixdown_id: null });
await loadJamTrack();
};
const activateCustomMix = async () => {
console.log('playing mix', selectedMix.value);
try {
await markMixdownActive({ id: jamTrack.id, mixdown_id: selectedMix.value });
await loadJamTrack();
}catch(error){
console.log('Error when activating custom mix', error);
}
};
useEffect(() => {
if (!selectedMix) {
return;
}
console.log('_JamTrack_ selectedMix', selectedMix);
if (selectedMix.value === 'original') {
//console.log('_JAMTRACK_ activating master track');
activateMasterTrack().then(() => {
//TODO: commiunicate with the client back end. Following is copied from the Rails front end
//SessionActions.mixdownActive({id:null})
});
} else {
//console.log('_JAMTRACK_ activating custom mix:', selectedMix);
activateCustomMix().then(() => {
//TODO: commiunicate with the client back end. Following is copied from the Rails front end
//context.jamClient.JamTrackStopPlay();
//SessionActions.mixdownActive(mixdown)
});
}
}, [selectedMix]);
return (
<>
<Select options={mixes} placeholder="Select Mix" onChange={handleChange} value={selectedMix} />
{ JSON.stringify(audioUrls)}
<Row className='mt-2'>
<Col>
{audioUrls.length > 0 && (
<figure>
<audio controls style={{ width: '100%'}}>
{audioUrls.map((url, index) => (
<source key={index} src={url} type={`audio/${url.split('.').pop()}`} />
))}
</audio>
</figure>
)}
</Col>
</Row>
</>
);
};
JKJamTrackPlayer.propTypes = {
jamTrack: PropTypes.object.isRequired
};
export default JKJamTrackPlayer;

View File

@ -1,73 +1,87 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import Select from 'react-select';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Row, Col } from 'reactstrap';
import PropTypes from 'prop-types';
import { markMixdownActive } from '../../helpers/rest';
import useJamTrackAudio from '../../hooks/useJamTrackAudio';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
const JKJamTrackPlayer = ({ jamTrack }) => {
const [mixes, setMixes] = useState([]);
const [selectedMix, setSelectedMix] = useState(null);
const { audioUrls } = useJamTrackAudio(jamTrack);
const handleChange = selectedOption => {
setSelectedMix(selectedOption);
};
const [options, setOptions] = useState([]);
const [selectedOption, setSelectedOption] = useState(null);
const fpPromise = FingerprintJS.load();
const [audioUrl, setAudioUrl] = useState(null);
const audioRef = useRef(null);
useEffect(() => {
if (jamTrack) {
const mixes = jamTrack.mixdowns.map(mix => ({ value: mix.id, label: mix.name, mix }));
mixes.unshift({ value: 'original', label: 'Original', jamTrack });
setMixes(mixes);
//set the default mix to the original
setSelectedMix(mixes[0]);
console.log('_JamTrackPlayer_ jamTrack', jamTrack);
const opts = jamTrack.mixdowns.map(mix => ({ value: mix.id, label: mix.name }));
opts.unshift({ value: 'original', label: 'Original' });
setOptions(opts);
if (jamTrack.last_mixdown_id) {
setSelectedOption(opts.find(opt => opt.value === jamTrack.last_mixdown_id));
} else {
setSelectedOption(opts[0]);
}
}
}, [jamTrack]);
const activateMasterTrack = async () => {
console.log('playing original');
await markMixdownActive({ id: selectedMix.jamTrack.id, mixdown_id: null });
};
const activateCustomMix = async () => {
console.log('playing mix', selectedMix.value);
await markMixdownActive({ id: selectedMix.jamTrack.id, mixdown_id: selectedMix.value });
const handleOnChange = selectedOption => {
const option = options.find(opt => opt.value === selectedOption.value);
setSelectedOption(option);
};
useEffect(() => {
if (!selectedMix) {
if (!selectedOption) {
return;
}
if (selectedMix.value === 'original') {
console.log('activating master track');
activateMasterTrack().then(() => {
//TODO: commiunicate with the client back end. Following is copied from the Rails front end
//SessionActions.mixdownActive({id:null})
});
console.log('_JamTrackPlayer_ selectedOption', selectedOption);
if (selectedOption.value === 'original') {
const audioUrl = getMasterTrack();
setAudioUrl(audioUrl);
if(audioRef.current)
audioRef.current.load();
} else {
console.log('activating custom mix:', selectedMix.value);
activateCustomMix().then(() => {
//TODO: commiunicate with the client back end. Following is copied from the Rails front end
//context.jamClient.JamTrackStopPlay();
//SessionActions.mixdownActive(mixdown)
//it's a mixdown
getMixdown().then(audioUrl => {
setAudioUrl(audioUrl);
if(audioRef.current)
audioRef.current.load();
});
}
}, [selectedMix]);
}, [selectedOption]);
const getMasterTrack = () => {
const masterTrack = jamTrack.tracks.find(track => track.track_type === 'Master');
console.log('_JamTrackPlayer_ master', masterTrack);
if (masterTrack) {
const audioUrl = masterTrack.preview_mp3_url;
return audioUrl;
}
};
const getMixdown = async () => {
const activeMix = jamTrack.mixdowns.find(mix => mix.id === selectedOption.value);
const fp = await fpPromise;
const result = await fp.get();
const audioUrl =
process.env.REACT_APP_API_BASE_URL +
`/mixdowns/${activeMix.id}/download.mp3?file_type=mp3&sample_rate=48&mark=${result.visitorId}`;
return audioUrl;
};
return (
<>
<Select options={mixes} placeholder="Select Mix" onChange={handleChange} value={selectedMix} />
<Row className='mt-2'>
<Select options={options} placeholder="Select Mix" onChange={handleOnChange} value={selectedOption} />
<Row className="mt-2">
<Col>
{audioUrls.length > 0 && (
{audioUrl && (
<figure>
<audio controls style={{ width: '100%'}}>
{audioUrls.map((url, index) => (
<source key={index} src={url} type={`audio/${url.split('.').pop()}`} />
))}
<audio controls style={{ width: '100%' }} ref={audioRef}>
<source src={audioUrl} type={`audio/${audioUrl.split('.').pop()}`} />
</audio>
</figure>
)}

View File

@ -0,0 +1,78 @@
import React, { useState, useEffect } from 'react';
import { Table } from 'reactstrap';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
const JKMyJamTrackMixes = ({ jamTrack }) => {
const [mixes, setMixes] = useState([]);
const fpPromise = FingerprintJS.load();
useEffect(() => {
if(!jamTrack) {
return;
}
setMixes(jamTrack.mixdowns)
}, []);
const downloadJamTrack = async () => {
console.log('Downloading JamTrack');
if(!jamTrack.can_download) {
console.log('Cannot download JamTrack');
return
}
const fp = await fpPromise;
const result = await fp.get();
const redirectTo = `${process.env.REACT_APP_API_BASE_URL}/jamtracks/${jamTrack.id}/stems/master/download.mp3?file_type=mp3&download=1&mark=${result.visitorId}`;
window.open(redirectTo, '_blank');
}
const downloadMix = async (mixId) => {
console.log('Download mixdown')
const mixdown = mixes.find(m => m.id === mixId);
const mixdownPackage = mixdown.packages.find(p => p.file_type === 'mp3');
if(mixdownPackage?.signing_state == 'SIGNED'){
const fp = await fpPromise;
const result = await fp.get();
const redirectTo = `${process.env.REACT_APP_API_BASE_URL}/mixdowns/${mixdown.id}/download.mp3?file_type=mp3&sample_rate=48&download=1&mark=${result.visitorId}`
window.open(redirectTo, '_blank');
}
}
const deleteMix = (mixId) => {
if(window.confirm("Delete this custom mix?")){
console.log("Deleting mixdown", mixId)
}
}
return (
<>
<p>You can save a maximum of 5 mixes on JamKazam. If you need to make more mixes, download a mix to save it, then delete it to make more room</p >
<Table striped bordered className="fs--1" >
<thead className="bg-200 text-900">
<tr>
<th>Mix</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>Full JamTrack</td>
<td>
<button onClick={downloadJamTrack}>Download</button>
</td>
</tr>
{mixes.map(mix => (
<tr key={mix.id}>
<td>{mix.name}</td>
<td>
<button onClick={() => downloadMix(mix.id)}>Download</button>
<button onClick={() => deleteMix(mix.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</Table>
</>
);
};
export default JKMyJamTrackMixes;

View File

@ -512,3 +512,14 @@ export const markMixdownActive = options => {
.catch(error => reject(error));
});
}
export const createMixdown = options => {
return new Promise((resolve, reject) => {
apiFetch(`/mixdowns`, {
method: 'POST',
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error));
});
}

View File

@ -1,33 +1,63 @@
import { useState, useEffect } from 'react';
import { getJamTrack } from '../helpers/rest';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
const useJamTrackAudio = (jamTrack) => {
const useJamTrackAudio = jamTrack => {
const [audioUrls, setAudioUrls] = useState([]);
const [jamTrackRecord, setJamTrackRecord] = useState(jamTrack);
const fpPromise = FingerprintJS.load();
const loadMedia = () => {
const activeMixdown = jamTrack.mixdowns.find(mixdown => mixdown.id === jamTrack.last_mixdown_id)
const activeStem = jamTrack.tracks.find(stem => stem.id === jamTrack.last_stem_id);
const loadJamTrack = async () => {
//console.log('_JAMTRACK_ loading jam track');
try {
const resp = await getJamTrack({ id: jamTrack.id });
const data = await resp.json();
setJamTrackRecord(data);
} catch (error) {
console.log('Error when fetching jam track', error);
}
};
if ( activeStem ) {
} else if ( activeMixdown ) {
const updateMedia = async () => {
//console.log('_JAMTRACK_ updating media', jamTrackRecord);
const activeMixdown = jamTrackRecord.mixdowns.find(mixdown => mixdown.id === jamTrackRecord.last_mixdown_id);
const activeStem = jamTrackRecord.tracks.find(stem => stem.id === jamTrackRecord.last_stem_id);
if (activeStem) {
//console.log('_JAMTRACK_ this is a stem', activeStem);
} else if (activeMixdown) {
//console.log('_JAMTRACK_ this is a mixdown', activeMixdown);
const fp = await fpPromise;
const result = await fp.get();
const audioUrl =
process.env.REACT_APP_API_BASE_URL +
`/mixdowns/${activeMixdown.id}/download.mp3?file_type=mp3&sample_rate=48&mark=${result.visitorId}`;
console.log('mixdown audioUrl', audioUrl);
setAudioUrls([audioUrl]);
} else if (jamTrack) {
const masterTrack = jamTrack.tracks.find(track => track.track_type === 'Master');
//console.log('_JAMTRACK_ this is the master track', masterTrack);
if (masterTrack) {
setAudioUrls([masterTrack.preview_mp3_url]);
}
}
};
}
// useEffect(() => {
// if (!jamTrack) {
// return;
// }
// loadJamTrack();
// }, [jamTrack]);
useEffect(() => {
if (!jamTrack) {
return;
if (jamTrackRecord) {
updateMedia();
}
loadMedia();
}, [jamTrack]);
}, [jamTrackRecord]);
return { audioUrls };
return { audioUrls, loadJamTrack };
};
export default useJamTrackAudio;
export default useJamTrackAudio;