sinshu / meltysynth

A SoundFont MIDI synthesizer for .NET
Other
130 stars 15 forks source link

[Discussion] add a synth.selectPreset function? #40

Closed chipweinberger closed 9 months ago

chipweinberger commented 9 months ago

In my README, I have these rather wordy docs.

You might want to select the patchNumber & bankNumber
of the instrument you want to play, if it's not 0.

Look through synth.soundFont.presets[X].patchNumber & bankNumber
for all valid instrument choices.

Example for setting patchNumer:

  int patchNumber = synth.soundFont.presets[0].patchNumber 
  synth.processMidiMessage(
    channel:0, 
    command:0xC0, // 0xC0 = program changes
    data1:patchNumber, 
    data2:0
  );

Example for setting bankNumber:

  int bankNumber = synth.soundFont.presets[0].bankNumber 
  synth.processMidiMessage(
    channel:0, 
    command:0xB0, // 0xB0 = control change
    data1:0x00,   // 0x00 = bank select
    data2:bankNumber,
  );

So simplify things, perhaps we could add a synth. selectPreset(int channel, int preset) function?

void selectPreset(int channel, int preset)
{
    if (preset >= synth.soundFont.presets.length) {
        throw InvalidArgument('selectInstrumentPreset: invalid preset $preset >= ${synth.soundFont.presets.length}');
    }

    this.processMidiMessage(
        channel: channel, 
        command: 0xC0, // program change
        data1: this.soundFont.presets[preset].patchNumber, 
        data2: 0
    );

    this.processMidiMessage(
        channel: channel, 
        command: 0xB0, // control change
        data1: 0x00,   // bank select
        data2: this.soundFont.presets[preset].bankNumber,
    );
}

Curious your thoughts @sinshu

chipweinberger commented 9 months ago

this way my example code is simple:


import 'package:dart_melty_soundfont/dart_melty_soundfont.dart';
import 'package:flutter/services.dart' show rootBundle;

// Create the synthesizer.
ByteData bytes = await rootBundle.load('assets/akai_steinway.sf2');

Synthesizer synth = Synthesizer.loadByteData(bytes, 
    SynthesizerSettings(
        sampleRate: 44100, 
        blockSize: 64, 
        maximumPolyphony: 64, 
        enableReverbAndChorus: true,
    ));

// select the first instrument preset
synth.selectPreset(channel: 0, preset: 0);

// Turn on some notes
synth.noteOn(channel: 0, key: 72, velocity: 120);
synth.noteOn(channel: 0, key: 76, velocity: 120);
synth.noteOn(channel: 0, key: 79, velocity: 120);
synth.noteOn(channel: 0, key: 82, velocity: 120);

// Render the waveform (3 seconds)
ArrayInt16 buf16 = ArrayInt16.zeros(numShorts: 44100 * 3);

synth.renderMonoInt16(buf16);
sinshu commented 9 months ago

I was considering adding several useful methods to Synthesizer. For instance, a method like the one below that simplifies program changes:

public static void ChangeProgram(this Synthesizer synthesizer, int channel, int value)
{
    synthesizer.ProcessMidiMessage(channel, 0xC0, value, 0);
}

However, I pondered 🤔 Would it be better for value to be int, or should it represent the GM sound set as an enum, or should it have overloads for both? Moreover, if there's ChangeProgram, it seems logical to also have methods like ChangeBank, SetExpression, SetPitchBend, and so on. Should the argument for SetExpression range from 1-127 or would 0.0-1.0 be more intuitive? After much deliberation on such matters, I concluded that users should create such utility methods according to their preferences. Hence, I decided not to provide them.

By the way, in dart_melty_soundfont's simple chord sample, does the mention of the preset serve to avoid silence when the SoundFont doesn't have a preset assigned to #0? If so, you might want to implement a fallback mechanism to a default preset when a preset is not found. #17 and related commits https://github.com/sinshu/meltysynth/pull/19/commits/d1f8ebe8a86b877831de47e7e3052fc7e5b11edf, https://github.com/sinshu/meltysynth/commit/f5078d1e6a7410299a07fd8eec19e21a20449cf6 might be helpful references.

chipweinberger commented 9 months ago

I concluded that users should create such utility methods according to their preferences.

That's a reasonable conclusion.

for changeProgram I would use an integer, and also provide an enum that can be converted to integer.

for setPitchBend, etc would use the integer values as well.

By the way, in dart_melty_soundfont's simple chord sample, does the mention of the preset serve to avoid silence when the SoundFont doesn't have a preset assigned to #0? If so, you might want to implement a fallback mechanism to a default preset when a preset is not found. https://github.com/sinshu/meltysynth/issues/17 and related commits https://github.com/sinshu/meltysynth/commit/d1f8ebe8a86b877831de47e7e3052fc7e5b11edf, https://github.com/sinshu/meltysynth/commit/f5078d1e6a7410299a07fd8eec19e21a20449cf6 might be helpful references.

Yes, that is the reason. Also, so users know how to switch instruments.

  • The first preset in the preset array might not be suitable for default. For example, the first preset of TimGM6mb.sf2 is the flute. I believe that the default preset should be the piano if the sf2 is GM compatible.

  • If the preset with non-zero bank number can be fallback to the GM sound set (bank #0), it should do so.

I suppose I am okay with the first preset being a flute. In my example code, I now print all the presets and their names so users know what is available. That said, I'm not sure if the sf2 preset.name + instrument.name is useful.

https://github.com/sinshu/meltysynth/commit/d1f8ebe8a86b877831de47e7e3052fc7e5b11edf is an interesting change. So if a complicated midi file with many instruments uses an instrument that is not available in the soundfont, you will get the default preset instead of silence?

chipweinberger commented 9 months ago

I pushed https://github.com/chipweinberger/dart_melty_soundfont/commit/90befd4bd3531bbc49b718f022eb7d823574d85a to match your 2 commits

sinshu commented 9 months ago

https://github.com/sinshu/meltysynth/commit/d1f8ebe8a86b877831de47e7e3052fc7e5b11edf is an interesting change. So if a complicated midi file with many instruments uses an instrument that is not available in the soundfont, you will get the default preset instead of silence?

Yes, every instrument might turn into a piano or something 😅

To the best of my knowledge, the behavior when a preset is not found is not explicitly stated in either the SoundFont or MIDI specification, so I think it’s ultimately up to the author to decide based on their preferences. Personally, considering the user experience, I thought it was a good decision to make sure that sound is produced as soon as NoteOn is executed 🤔

chipweinberger commented 9 months ago

yes that makes sense. thanks for your help :)