paulrosen / abcjs

javascript for rendering abc music notation
Other
1.93k stars 284 forks source link

Is there an easier way to transpose than this? #951

Open benlieb opened 1 year ago

benlieb commented 1 year ago

First of all thanks for the amazing work on this!

I wanted to have an interface where users can transpose. This is in a React context fwiw. You can see this live [here](https://music.benlieb.dev/abc/show?abc=X%3A%201%0AT%3A%20The%20New%20Land%0AR%3A%20waltz%0AM%3A%203%2F4%0AL%3A%201%2F8%0AK%3A%20Fmaj%0ACDE%7C%3AF4FG%7CAc3d2%7Cc3AG2%7CF3G(3AGF%7CB3AGA%7CBd3f2%7C%0Ad3BG2%7CG3G(3AGF%7CA3FAF%7CA2c2f2%7Ca3gf2%7Cd3efg%7C%0Aa3gf2%7Cge3c2%7Cd4de%7C1%20d3cAG%3A%7C2%20d3efg%7C%7C%0A%7C%3Aa3gab%7Ca2g2f2%7CB4Bc%7CB2d2f2%7Cg3fga%7Cg2f2e2%7CA4AB%7CA2c2e2%7C%0Af3efg%7Cf2e2d2%7CG4GA%7CG2F2D2%7CcA3F2%7CGE3C2%7CD4DE%7CD6%3A%7C%0A).

It took me quite a bit of fiddling with this to get it to work. The main issues that I needed were to save the original visualObj from the first render, in order to make transpositions work. My understanding of that is from these line from the transposition demo:

var output = ABCJS.strTranspose(abc, visualObj, steps)
outputEl.innerText = output
var newVisualObj = ABCJS.renderAbc("paper2", output, renderParams)

To be honest I'm not sure why the visualObj is needed in the ABCJS.strTranspose call, but it seems to be. Perhaps there's a more direct way?

Here's my full components:

import React, { useEffect, useRef, useState } from 'react'
import abcjs from 'abcjs';
import AbcAudioPlayer from './AbcAudioPlayer';

export default function ShowAbcApp(props) {
  const queryParameters = new URLSearchParams(window.location.search)
  const paramsAbc = queryParameters.get('abc')
  const defaultInput = paramsAbc || "X:1\nK:D\nD4|\n"
  const [abcInput, setAbcInput] = useState(defaultInput)
  const [transpose, setTranspose] = useState(0)
  const [origVisualObj, setOrigVisualObj] = useState(null)
  const [visualObj, setVisualObj] = useState(null)
  const renderEl = useRef(null)

  const getLink = () => {
    return `?abc=${encodeURIComponent(abcInput)}`
  }

  useEffect(() => {
    if ( !visualObj ) {
      // initial render
      const visualObj = abcjs.renderAbc( renderEl.current, abcInput)
      setOrigVisualObj(visualObj)
      setVisualObj(visualObj)
    } else {
      var transposedAbcString = abcjs.strTranspose(abcInput, origVisualObj, transpose)
      var transposedVisualObj = abcjs.renderAbc(renderEl.current, transposedAbcString)
      setVisualObj(transposedVisualObj)
    }
  }, [renderEl.current, abcInput, transpose])

  return (
    <>
    <textarea 
      rows={10}
      cols={80}
      value={abcInput} 
      onChange={(e) => setAbcInput(e.target.value)} />
    <br/>
    <input type="number" value={transpose} onChange={(e) => setTranspose(e.target.value)} />
    <a href={getLink()}>Link to this notation</a>
    <br/>
    <br/>

    <div id="show-abc-app">
      <AbcAudioPlayer visualObj={visualObj} />
      <div className="rendered-abc-container" ref={renderEl} />
    </div>
    </>
  )
}
import React, { useEffect, useRef, useState } from 'react'
import abcjs, { synth } from 'abcjs';

export default function AbcAudioPlayer({ visualObj, midiTranspose } ) {
  const renderEl = useRef(null)

  const initSyntControl = () => {
    const synthControl = new abcjs.synth.SynthController();
    synthControl.load(
      renderEl.current, 
      {}, //cursorControl
      {
        displayLoop: true, 
        displayRestart: true, 
        displayPlay: true, 
        displayProgress: true, 
        displayWarp: true
      }
    );
    return synthControl;
  }

  const initAudio = () => {
    var myContext = new AudioContext();
    const synthControl = initSyntControl();

    const synth = new abcjs.synth.CreateSynth();

    synth.init({
      audioContext: myContext,
      millisecondsPerMeasure: 500,
      visualObj: visualObj[0],
      options: {
        midiTranspose: 2,
      }
    }).then((results) => {
      synthControl.setTune(visualObj[0], false, {})
    }).catch(function (reason) {
      console.log(reason)
    });

  }
  useEffect(() => {
    if (!visualObj || !renderEl.current) return
    initAudio()
  }, [renderEl.current, visualObj, midiTranspose])

  return (
    <>
      <div className="audio-player-container" ref={renderEl} />
    </>
  )
}
paulrosen commented 1 year ago

The reason the visualObj is needed is that it is the same processing that is needed for transposing. I could do it inside strTranspose but for common uses it is likely that it is already on the page. Note that you can use ABCJS.renderAbc("*", ...) to create that object if needed without displaying it.