OMNi

OmniDAW API · 9 min

Receptury

Działające przykłady dla OmniDAW API: pętla, utwór, automatyka, miks, mastering pod streaming, stemy, krojenie breaka, wtyczki.

Każda receptura to krótki skrypt w Pythonie z użyciem klienta Pythona. Wynikają jedna z drugiej, więc zmienne przechodzą z receptury do receptury, ale każdą część da się też uruchomić osobno, gdy masz id torów z omni.tracks.list(). Każde pokazane wywołanie zostało uruchomione na działającym Omni i wszystko można cofnąć przez omni.edit.undo().

WSKAZÓWKA

Wskazówka. Wszystko, co robi asystent, można zrobić tymi samymi wywołaniami, a są to dokładnie narzędzia, które wystawia serwer MCP. Gdy zastanawiasz się, co zrobi narzędzie AI, najpierw wypróbuj wywołanie tutaj.

1. Pierwszy kontakt

python
from omnidaw import Omni

omni = Omni()
print(omni.session.info())
for track in omni.tracks.list():
    print(track["index"], track["name"], track["kind"], track["id"])

session.info mówi o projekcie, częstotliwości próbkowania oraz liczbie torów i scen. tracks.list daje id każdego toru, które przekazujesz w dalszych wywołaniach.

2. Pętla: akordy, bas, melodia i perkusja

python
key = {"root": "F", "mode": "minor"}
prog = {"key": key, "chords": "i VI III VII"}

keys = omni.tracks.create(kind="midi", name="Keys", instrument="builtin://piano-synth")["id"]
bass = omni.tracks.create(kind="midi", name="Bass", instrument="builtin://omni-synth")["id"]
lead = omni.tracks.create(kind="midi", name="Lead", instrument="builtin://omni-synth")["id"]
drums = omni.tracks.create(kind="midi", name="Drums", instrument="builtin://tomic")["id"]

# Chords as a pad, the same progression as a bass line
omni.clips.write_progression(track=keys, slot=0,
    progression={**prog, "perform": {"pattern": "pad"}})
omni.clips.write_progression(track=bass, slot=0,
    progression={**prog, "perform": {"chords": False, "bass": True}})

# A melody that follows the chords of the Keys clip
omni.clips.generate_melody(track=lead, slot=0, style="synth_arp", key=key, bars=4,
    register="high", chords_from={"track": keys, "slot": 0})

# Drums for the TOMNIC drum synth: x = hit, X = accent, . = rest, one character per 1/16
omni.clips.write_drums(track=drums, slot=0, lanes=[
    {"channel": "kick",  "steps": "x...x...x...x..."},
    {"channel": "snare", "steps": "....X.......X..."},
    {"channel": "hihat", "steps": "x.x.x.x.x.x.x.x.", "swing": 30, "velocity": 70},
])

omni.scenes.launch(scene=0)     # plays scene row 0 of every track

theory.progression pokazuje, w co zamienia się tekst progresji, niczego nie zapisując, a theory.library i theory.vocabulary wymieniają 240 gotowych progresji oraz każdą nazwę stylu, wzorca i trybu, jaką przyjmują pozostałe wywołania. clips.generate_melody przyjmuje styl melodii z tej listy: stepwise_flow, minimal_cell, classical_arch, funk_groove, celtic_reel, bebop_line, arpeggio_loop, synth_arp, sequence_run, ambient_drift, tension_build, modal_motive. To samo ziarno (seed) zawsze daje tę samą melodię.

3. Od pętli do utworu

Daj pozostałym wierszom scen więcej do grania, a potem ułóż sceny jako sekcje:

python
# Scene row 1: a fuller version of each part
for track in (keys, bass, lead):
    omni.clips.duplicate(track=track, slot=0, to_slot=1)
omni.clips.write_drums(track=drums, slot=1, lanes=[
    {"channel": "kick",  "steps": "x..x..x...x..x.."},
    {"channel": "snare", "steps": "....X.......X..X"},
    {"channel": "hihat", "steps": "xxxxxxxxxxxxxxxx", "velocity": 60},
])

song = omni.arrangement.build_song(sections=[
    {"scene": 0, "bars": 4, "name": "Intro"},
    {"scene": 1, "bars": 8, "name": "Drop"},
])
print(song["end_beat"], [s["name"] for s in song["sections"]])
omni.arrangement.play(from_beat=0)

build_song kopiuje klip każdego toru z danego wiersza sceny na oś czasu na zadaną liczbę taktów i dodaje marker na sekcję. Odmawia nadpisania osi czasu, na której już są klipy; przekaż clear_existing=True albo późniejszy start_beat, by budować obok istniejącego materiału. Potem arrangement.edit_clip, copy_clip i remove_clip kształtują pojedyncze klipy, na przykład żeby wyciszyć bas w ostatnich taktach dropu.

4. Ruch: automatyka

Znajdź klucz parametru, a potem zapisz punkty w beatach, z wartościami w jednostce samego parametru:

python
cutoff = omni.params.list(track=lead, filter="cutoff")["params"][0]
print(cutoff["key"], cutoff["name"], cutoff["min"], cutoff["max"])

lo, hi = cutoff["min"], cutoff["max"]
omni.automation.write(track=lead, param=cutoff["key"], curve="smooth", points=[
    {"beat": 16, "value": lo + 0.2 * (hi - lo)},    # closed at the start of the drop
    {"beat": 48, "value": lo + 0.9 * (hi - lo)},    # open at the end
])

# Volume is in dB and pan from -1 to 1; no key needed
omni.automation.write(track=lead, param="volume", points=[
    {"beat": 0, "value": -30}, {"beat": 16, "value": -6},
])
print(omni.automation.list(track=lead))

Kluczem parametru jest instrument:<id> dla instrumentu i fx:<slot>:<index> dla insertu, dokładnie tak, jak zwraca go params.list. Działa dla instrumentów wbudowanych oraz wtyczek VST3 i CLAP.

5. Posłuchaj miksu i popraw go

analysis.capture nagrywa master (albo jeden tor) przez kilka sekund, gdy gra transport, i analizuje to w tle. Poczekaj na zadanie, potem odczytaj liczby:

python
omni.arrangement.play(from_beat=16)
analysis = omni.wait(omni.analysis.capture(source="master", seconds=8)["job"])

print("integrated", analysis["loudness"]["integrated_lufs"], "LUFS")
print("true peak ", analysis["level"]["true_peak_dbtp"], "dBTP")
print("balance   ", analysis["tonal_balance"])      # sub, bass, low_mid, mid, high_mid, high, air
print("stereo    ", analysis["stereo"])

Potem działaj na podstawie tego, co słyszysz. Kompresor na basie i trochę pogłosu na leadzie przez szynę return:

python
omni.devices.add(chain={"track": bass}, device="Compressor")
print([p["name"] for p in omni.devices.get(chain={"track": bass}, slot=0)["params"]])
omni.devices.set_params(chain={"track": bass}, slot=0,
                        values={"Ratio": 3.0, "Threshold": -18.0})

space = omni.mixer.add_return(name="Space")["returns"][-1]["index"]
omni.devices.add(chain={"return_bus": space}, device="Reverb")
omni.mixer.set_send(track=lead, bus=space, level_db=-12)

Urządzenia przyjmują parametry po nazwie, w prawdziwych jednostkach; devices.get wymienia je z wartością, zakresem i jednostką. Nagraj ponownie, żeby zobaczyć efekt. Aby znaleźć, gdzie dwa tory walczą o te same częstotliwości, nagraj każdy osobno (source={"track": id}) i wywołaj analysis.compare(a=job_a, b=job_b).

6. Mastering pod serwis streamingowy

Wybierz cel dostawy (master.targets je wymienia: spotify, apple_music, youtube, tidal, amazon_music, deezer, podcast, ebu_r128, atsc_a85, club), zapętl najgłośniejszą część i pozwól Omni zbudować pierwszy łańcuch z pomiaru. Potem domknij różnicę w pętli:

python
omni.arrangement.set_loop(start_beat=16, end_beat=48, enabled=True)
omni.arrangement.play(from_beat=16)

job = omni.analysis.capture(source="master", seconds=15)["job"]
omni.wait(job)

chain = omni.master.build_chain(job=job, target="spotify")   # glue compressor + true-peak limiter
limiter = chain["limiter_slot"]

for _ in range(5):
    job = omni.analysis.capture(source="master", seconds=15)["job"]
    omni.wait(job)
    verdict = omni.master.check(job=job, target="spotify")
    print(verdict["integrated_lufs"], verdict["true_peak_dbtp"], verdict["notes"])
    if verdict["passed"]:
        break
    gain = next(p for p in omni.devices.get(chain="master", slot=limiter)["params"]
                if p["name"] == "Input Gain")
    omni.devices.set_params(chain="master", slot=limiter, values={
        "Input Gain": min(gain["max"], gain["value"] + verdict["gain_to_target_db"])})

result = omni.wait(omni.export.render(path="C:/Music/my-song.wav", target="spotify", fast=True)["job"])
print(result["path"], result["analysis"]["loudness"]["integrated_lufs"])

Pętla ma znaczenie: limiter nie przekazuje całego dodatkowego wzmocnienia wejścia na głośność, więc jedna korekta zwykle ląduje w połowie drogi. W teście na miksie drum and bass potrzeba było dwóch rund, by przejść z -16,3 do -14,7 LUFS przy true peaku -3 dBTP. Nagrywaj co najmniej 15 sekund, żeby pomiar głośności miał sens. build_chain odmawia, gdy master ma już Limiter; wtedy dostrój go przez devices.set_params.

export.render z fast=True renderuje offline, szybciej niż w czasie rzeczywistym, a wynik zadania zawiera analizę zapisanego pliku. Bez fast utwór jest odtwarzany przez urządzenie audio w czasie rzeczywistym. Oba zapisują tylko do bezwzględnych ścieżek .wav w istniejących folderach i nie nadpisują, dopóki nie przekażesz overwrite=True.

7. Stemy

python
result = omni.wait(omni.export.stems(folder="C:/Music/stems")["job"])
for f in result["files"]:
    print(f["name"], f["path"])

Jeden WAV na tor, renderowany offline: każdy stem to jego tor solo, z jego returnami i bez łańcucha mastera, chyba że przekażesz through_master=True. Przekaż tracks=[...], by wyrenderować tylko niektóre.

8. Pokrój break i zagraj go z powrotem

Najpierw pliki audio muszą być dozwolone: użytkownik włącza Let clients read audio files w Ustawieniach Omni, AI Create. Sprawdź "files" in omni.capabilities.

python
drum_stem = next(f for f in result["files"] if f["name"] == "Drums")

chop = omni.tracks.create(kind="audio", name="Chop")["id"]
omni.audio.import_(track=chop, slot=0, path=drum_stem["path"])

# Cut into sixteenths at the project tempo (division = slices per beat)
sliced = omni.sampler.slice(track=chop, slot=0, mode="beats", division=4)
print(len(sliced["slices"]), "slices; the first:", sliced["slices"][0])

# Replay eight slices as a new pattern (start and duration in beats)
order = [0, 0, 6, 2, 4, 0, 6, 7]
omni.clips.write_slices(track=chop, slot=0, length_beats=2, steps=[
    {"slice": 8 + s, "start": i * 0.25, "duration": 0.25, "velocity": 110 if i % 4 == 0 else 90}
    for i, s in enumerate(order)
])
omni.clips.launch(track=chop, slot=0)

Pocięty klip gra przez swoje nuty, więc może też zagrać melodię z jednego slice'a: powtarzaj ten sam slice z innym pitch (półtony) w każdym kroku. sampler.slice tnie też mode="transients" (z sensitivity) i mode="equal" (z count); klip mieści najwyżej 92 slice'y, więc krój krótką pętlę, nie cały utwór. sampler.set zmienia tryb odtwarzania, nutę bazową, transpozycję i obwiednię.

9. Audio na nuty

python
bass_stem = next(f for f in result["files"] if f["name"] == "Bass")
notes = omni.wait(omni.audio.transcribe(path=bass_stem["path"], mode="bass")["job"])
print(notes["note_count"], "notes; the first:", notes["notes"][0])

mode to poly (akordy), melody (jedna linia) lub bass. Wynik zawiera nuty w beatach w tempie projektu, gotowe dla clips.write_midi. Aby transkrybować audio, które już jest w slocie sesji, przekaż track i slot zamiast path. Renderowanie stemów miksu i ich transkrypcja to szybki sposób, by zamienić nagranie w edytowalne MIDI.

10. Syntezator-wtyczka i jego presety

python
plugins = [i for i in omni.instruments.list() if i["group"] == "Plugin"]
print([(p["name"], p["format"]) for p in plugins][:8])

surge = omni.tracks.create(kind="midi", name="Surge", instrument="Surge XT")["id"]
found = omni.instruments.presets(track=surge, query="factory leads")
print(found["total"], "presets;", [p["name"] for p in found["presets"][:5]])
omni.instruments.load_preset(track=surge, path=found["presets"][0]["path"])

# Its parameters, read from the plugin itself (they appear once the plugin has loaded)
import time
for _ in range(20):
    params = omni.params.list(track=surge, filter="cutoff")["params"]
    if params:
        break
    time.sleep(0.5)
print(params[:2])

instruments.list pokazuje instrumenty wbudowane i każdą wtyczkę-instrument zeskanowaną przez Omni. Wtyczka ładuje się w tle, więc jej parametry pojawiają się chwilę po utworzeniu toru. Presety to pliki .fxp zainstalowane razem z wtyczką. Zewnętrzne efekty działają tak samo przez devices.types i devices.add.

11. Efekt MIDI zrenderowany do nut

Efekty i generatory MIDI (Arpeggiator, Markov, L-System i inne; midi_fx.types je wymienia) mogą działać na żywo albo być zagrane offline do klipu, żebyś mógł edytować nuty:

python
omni.midi_fx.add(track=keys, device="Arpeggiator")
omni.midi_fx.set_params(track=keys, slot=0, values={"Octaves": 2})

# The arpeggiator hears the chord in slot 0 and writes what it plays into slot 2
clip = omni.midi_fx.render(track=keys, slot=0, to_slot=2, bars=2, input_slot=0)
print(clip["note_count"], "notes")

Po renderze efekt jest omijany (bypass), żeby klip nie był przetwarzany dwa razy. Generator taki jak Markov nie potrzebuje nut wejściowych.

12. Mapa tempa i groove

python
omni.tempo.set_map(points=[
    {"beat": 0, "bpm": 124},
    {"beat": 32, "bpm": 132, "curve": "linear"},     # ramps up from beat 32
])
omni.groove.set(style="mpc", amount=0.55, humanize_ms=4)
omni.groove.set_track(track=bass, follow=False)        # a tight bass under swung drums

Klipy na osi czasu zachowują swoje beaty przy zmianie mapy tempa. tempo.set_time_signatures zmienia metrum od wskazanego taktu.

13. Cofanie

python
# Take back everything the script did, and stop at the first step that is not an API step
step = omni.edit.undo()
print("undid", step["label"])
while (step["next_undo"] or "").startswith("API:"):
    step = omni.edit.undo()
    print("undid", step["label"])

Każda z powyższych edycji to jeden krok z etykietą API: .... edit.redo przywraca krok. Wynik edit.undo nazywa też następny krok (next_undo), więc skrypt może się zatrzymać, zanim dojdzie do czegoś, co użytkownik zrobił ręcznie. Okno Historii Cofania w Omni pokazuje te same kroki.