diff --git a/backend-golang/midi.go b/backend-golang/midi.go index 47b10b6..b4d518f 100644 --- a/backend-golang/midi.go +++ b/backend-golang/midi.go @@ -23,6 +23,7 @@ type MIDIMessage struct { var ports []Port var input rtmidi.MIDIIn +var out rtmidi.MIDIOut var activeIndex int = -1 var lastNoteTime time.Time @@ -33,6 +34,14 @@ func (a *App) midiLoop() { runtime.EventsEmit(a.ctx, "midiError", err.Error()) return } + out, err = rtmidi.NewMIDIOutDefault() + if err != nil { + runtime.EventsEmit(a.ctx, "midiError", err.Error()) + } + err = out.OpenPort(0, "") + if err != nil { + runtime.EventsEmit(a.ctx, "midiError", err.Error()) + } ticker := time.NewTicker(500 * time.Millisecond) go func() { for { @@ -55,7 +64,7 @@ func (a *App) midiLoop() { func (a *App) OpenMidiPort(index int) error { if input == nil { - return errors.New("failed to initialize MIDI") + return errors.New("failed to initialize MIDI input") } if activeIndex == index { return nil @@ -126,7 +135,7 @@ func (a *App) OpenMidiPort(index int) error { func (a *App) CloseMidiPort() error { if input == nil { - return errors.New("failed to initialize MIDI") + return errors.New("failed to initialize MIDI input") } if activeIndex == -1 { return nil @@ -140,3 +149,16 @@ func (a *App) CloseMidiPort() error { } return nil } + +func (a *App) PlayNote(msg MIDIMessage) error { + if out == nil { + return errors.New("failed to initialize MIDI output") + } + channelByte := byte(msg.Channel) + if msg.MessageType == "NoteOn" { + out.SendMessage([]byte{0x90 | channelByte, byte(msg.Note), byte(msg.Velocity)}) + } else if msg.MessageType == "NoteOff" { + out.SendMessage([]byte{0x80 | channelByte, byte(msg.Note), byte(msg.Velocity)}) + } + return nil +} diff --git a/frontend/src/pages/AudiotrackManager/AudiotrackButton.tsx b/frontend/src/pages/AudiotrackManager/AudiotrackButton.tsx index 79ace3e..29fe80c 100644 --- a/frontend/src/pages/AudiotrackManager/AudiotrackButton.tsx +++ b/frontend/src/pages/AudiotrackManager/AudiotrackButton.tsx @@ -12,13 +12,15 @@ export const AudiotrackButton: FC<{ size?: 'small' | 'medium' | 'large', shape?: 'rounded' | 'circular' | 'square'; appearance?: 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent'; -}> = ({ size, shape, appearance }) => { + setPrompt: (prompt: string) => void; +}> = ({ size, shape, appearance, setPrompt }) => { const { t } = useTranslation(); return { if (!data.open) { flushMidiRecordingContent(); commonStore.setRecordingTrackId(''); + commonStore.setPlayingTrackId(''); } }}> @@ -30,7 +32,7 @@ export const AudiotrackButton: FC<{ - + diff --git a/frontend/src/pages/AudiotrackManager/AudiotrackEditor.tsx b/frontend/src/pages/AudiotrackManager/AudiotrackEditor.tsx index 0a727e1..1e29183 100644 --- a/frontend/src/pages/AudiotrackManager/AudiotrackEditor.tsx +++ b/frontend/src/pages/AudiotrackManager/AudiotrackEditor.tsx @@ -9,11 +9,13 @@ import { ArrowAutofitWidth20Regular, Delete16Regular, MusicNote220Regular, + Pause16Regular, + Play16Filled, Play16Regular, Record16Regular, Stop16Filled } from '@fluentui/react-icons'; -import { Button, Card, Slider, Text, Tooltip } from '@fluentui/react-components'; +import { Button, Card, DialogTrigger, Slider, Text, Tooltip } from '@fluentui/react-components'; import { useWindowSize } from 'usehooks-ts'; import commonStore from '../../stores/commonStore'; import classnames from 'classnames'; @@ -26,6 +28,7 @@ import { import { toast } from 'react-toastify'; import { ToastOptions } from 'react-toastify/dist/types'; import { flushMidiRecordingContent, refreshTracksTotalTime } from '../../utils'; +import { PlayNote } from '../../../wailsjs/go/backend_golang/App'; const snapValue = 25; const minimalMoveTime = 8; // 1000/125=8ms wait_events=125 @@ -109,9 +112,7 @@ const midiMessageToToken = (msg: MidiMessage) => { let dropRecordingTime = false; -export const midiMessageHandler = (data: MidiMessage) => { - if (data.messageType === 'NoteOff') - return; +export const midiMessageHandler = async (data: MidiMessage) => { if (data.messageType === 'ControlChange') { commonStore.setInstrumentType(Math.round(data.value / 127 * (InstrumentTypeNameMap.length - 1))); displayCurrentInstrumentType(); @@ -122,8 +123,15 @@ export const midiMessageHandler = (data: MidiMessage) => { dropRecordingTime = false; return; } + data = { + ...data, + instrument: commonStore.instrumentType + }; commonStore.setRecordingRawContent([...commonStore.recordingRawContent, data]); commonStore.setRecordingContent(commonStore.recordingContent + midiMessageToToken(data)); + + //TODO data.channel = data.instrument; + PlayNote(data); } }; @@ -180,7 +188,7 @@ const Track: React.FC = observer(({ ); }); -const AudiotrackEditor: FC = observer(() => { +const AudiotrackEditor: FC<{ setPrompt: (prompt: string) => void }> = observer(({ setPrompt }) => { const { t } = useTranslation(); const viewControlsContainerRef = useRef(null); @@ -243,7 +251,7 @@ const AudiotrackEditor: FC = observer(() => {
- } /> + } /> } onClick={() => { commonStore.setTracks([]); commonStore.setTrackScale(1); @@ -348,23 +356,38 @@ const AudiotrackEditor: FC = observer(() => {
: } - size="small" shape="circular" - appearance="subtle" onClick={() => { - flushMidiRecordingContent(); + size="small" shape="circular" appearance="subtle" + onClick={() => { + flushMidiRecordingContent(); + commonStore.setPlayingTrackId(''); - if (commonStore.recordingTrackId === track.id) { + if (commonStore.recordingTrackId === track.id) { + commonStore.setRecordingTrackId(''); + } else { + dropRecordingTime = true; + setSelectedTrackId(track.id); + + commonStore.setRecordingTrackId(track.id); + commonStore.setRecordingContent(track.content); + commonStore.setRecordingRawContent(track.rawContent.slice()); + } + }} /> + : } + size="small" shape="circular" appearance="subtle" + onClick={() => { + flushMidiRecordingContent(); commonStore.setRecordingTrackId(''); - } else { - dropRecordingTime = true; - setSelectedTrackId(track.id); - commonStore.setRecordingTrackId(track.id); - commonStore.setRecordingContent(track.content); - commonStore.setRecordingRawContent(track.rawContent.slice()); - } - }} /> - } size="small" shape="circular" - appearance="subtle" /> + if (commonStore.playingTrackId === track.id) { + commonStore.setPlayingTrackId(''); + } else { + setSelectedTrackId(track.id); + + commonStore.setPlayingTrackId(track.id); + } + }} /> } size="small" shape="circular" appearance="subtle" onClick={() => { const tracks = commonStore.tracks.slice().filter(t => t.id !== track.id); @@ -416,9 +439,52 @@ const AudiotrackEditor: FC = observer(() => {
} - + + +
); }); diff --git a/frontend/src/pages/Composition.tsx b/frontend/src/pages/Composition.tsx index 0c3739f..037c9be 100644 --- a/frontend/src/pages/Composition.tsx +++ b/frontend/src/pages/Composition.tsx @@ -23,7 +23,7 @@ import { OpenMidiPort, OpenSaveFileDialogBytes } from '../../wailsjs/go/backend_golang/App'; -import { getServerRoot, toastWithButton } from '../utils'; +import { getServerRoot, getSoundFont, toastWithButton } from '../utils'; import { CompositionParams } from '../types/composition'; import { useMediaQuery } from 'usehooks-ts'; import { AudiotrackButton } from './AudiotrackManager/AudiotrackButton'; @@ -71,20 +71,8 @@ const CompositionPanel: FC = observer(() => { }; const setSoundFont = async () => { - let soundUrl: string; - if (commonStore.compositionParams.useLocalSoundFont) - soundUrl = 'assets/sound-font'; - else - soundUrl = !commonStore.settings.giteeUpdatesSource ? - `https://raw.githubusercontent.com/josStorer/sgm_plus/master` : - `https://gitee.com/josc146/sgm_plus/raw/master`; - const fallbackUrl = 'https://cdn.jsdelivr.net/gh/josstorer/sgm_plus'; - await fetch(soundUrl + '/soundfont.json').then(r => { - if (!r.ok) - soundUrl = fallbackUrl; - }).catch(() => soundUrl = fallbackUrl); if (playerRef.current) { - playerRef.current.soundFont = soundUrl; + playerRef.current.soundFont = await getSoundFont(); } }; @@ -302,7 +290,7 @@ const CompositionPanel: FC = observer(() => { ) } - +
} /> } diff --git a/frontend/src/startup.ts b/frontend/src/startup.ts index c24afaf..9c85c39 100644 --- a/frontend/src/startup.ts +++ b/frontend/src/startup.ts @@ -146,6 +146,6 @@ async function initMidi() { commonStore.setMidiPorts(data); }); EventsOn('midiMessage', async (data: MidiMessage) => { - (await import('./pages/AudiotrackManager/AudiotrackEditor')).midiMessageHandler(data); + await (await import('./pages/AudiotrackManager/AudiotrackEditor')).midiMessageHandler(data); }); } diff --git a/frontend/src/stores/commonStore.ts b/frontend/src/stores/commonStore.ts index caf8fc2..f3714ca 100644 --- a/frontend/src/stores/commonStore.ts +++ b/frontend/src/stores/commonStore.ts @@ -107,8 +107,9 @@ class CommonStore { trackTotalTime: number = tracksMinimalTotalTime; trackCurrentTime: number = 0; trackPlayStartTime: number = 0; + playingTrackId: string = ''; recordingTrackId: string = ''; - recordingContent: string = ''; // used to improve performance, and I'm too lazy to maintain an ID dictionary for this + recordingContent: string = ''; // used to improve performance of midiMessageHandler, and I'm too lazy to maintain an ID dictionary for this (although that would be better for realtime effects) recordingRawContent: MidiMessage[] = []; // configs currentModelConfigIndex: number = 0; @@ -444,6 +445,10 @@ class CommonStore { setRecordingRawContent(value: MidiMessage[]) { this.recordingRawContent = value; } + + setPlayingTrackId(value: string) { + this.playingTrackId = value; + } } export default new CommonStore(); \ No newline at end of file diff --git a/frontend/src/types/composition.ts b/frontend/src/types/composition.ts index 6d82af0..dedbc18 100644 --- a/frontend/src/types/composition.ts +++ b/frontend/src/types/composition.ts @@ -32,6 +32,7 @@ export type MidiMessage = { velocity: number; control: number; value: number; + instrument: InstrumentType; } export enum InstrumentType { diff --git a/frontend/src/utils/index.tsx b/frontend/src/utils/index.tsx index 0b13256..1d875c6 100644 --- a/frontend/src/utils/index.tsx +++ b/frontend/src/utils/index.tsx @@ -511,6 +511,22 @@ export function flushMidiRecordingContent() { commonStore.setRecordingRawContent([]); } +export async function getSoundFont() { + let soundUrl: string; + if (commonStore.compositionParams.useLocalSoundFont) + soundUrl = 'assets/sound-font'; + else + soundUrl = !commonStore.settings.giteeUpdatesSource ? + `https://raw.githubusercontent.com/josStorer/sgm_plus/master` : + `https://gitee.com/josc146/sgm_plus/raw/master`; + const fallbackUrl = 'https://cdn.jsdelivr.net/gh/josstorer/sgm_plus'; + await fetch(soundUrl + '/soundfont.json').then(r => { + if (!r.ok) + soundUrl = fallbackUrl; + }).catch(() => soundUrl = fallbackUrl); + return soundUrl; +} + export function getSupportedCustomCudaFile(isBeta: boolean) { if ([' 10', ' 16', ' 20', ' 30', 'MX', 'Tesla P', 'Quadro P', 'NVIDIA P', 'TITAN X', 'TITAN RTX', 'RTX A', 'Quadro RTX 4000', 'Quadro RTX 5000', 'Tesla T4', 'NVIDIA A10', 'NVIDIA A40'].some(v => commonStore.status.device_name.includes(v))) diff --git a/frontend/wailsjs/go/backend_golang/App.d.ts b/frontend/wailsjs/go/backend_golang/App.d.ts index dfba0d1..fe79aaf 100755 --- a/frontend/wailsjs/go/backend_golang/App.d.ts +++ b/frontend/wailsjs/go/backend_golang/App.d.ts @@ -48,6 +48,8 @@ export function OpenSaveFileDialogBytes(arg1:string,arg2:string,arg3:Array; +export function PlayNote(arg1:backend_golang.MIDIMessage):Promise; + export function ReadFileInfo(arg1:string):Promise; export function ReadJson(arg1:string):Promise; diff --git a/frontend/wailsjs/go/backend_golang/App.js b/frontend/wailsjs/go/backend_golang/App.js index c7f3afa..9173a12 100755 --- a/frontend/wailsjs/go/backend_golang/App.js +++ b/frontend/wailsjs/go/backend_golang/App.js @@ -94,6 +94,10 @@ export function PauseDownload(arg1) { return window['go']['backend_golang']['App']['PauseDownload'](arg1); } +export function PlayNote(arg1) { + return window['go']['backend_golang']['App']['PlayNote'](arg1); +} + export function ReadFileInfo(arg1) { return window['go']['backend_golang']['App']['ReadFileInfo'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 30edb01..745c392 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -18,6 +18,28 @@ export namespace backend_golang { this.modTime = source["modTime"]; } } + export class MIDIMessage { + messageType: string; + channel: number; + note: number; + velocity: number; + control: number; + value: number; + + static createFrom(source: any = {}) { + return new MIDIMessage(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.messageType = source["messageType"]; + this.channel = source["channel"]; + this.note = source["note"]; + this.velocity = source["velocity"]; + this.control = source["control"]; + this.value = source["value"]; + } + } }