gingo 1.0.0__cp312-cp312-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gingo/__init__.py +106 -0
- gingo/__init__.pyi +476 -0
- gingo/__main__.py +1219 -0
- gingo/_gingo.cpython-312-darwin.so +0 -0
- gingo/audio.py +507 -0
- gingo/py.typed +0 -0
- gingo-1.0.0.dist-info/METADATA +1098 -0
- gingo-1.0.0.dist-info/RECORD +11 -0
- gingo-1.0.0.dist-info/WHEEL +5 -0
- gingo-1.0.0.dist-info/entry_points.txt +3 -0
- gingo-1.0.0.dist-info/licenses/LICENSE +21 -0
|
Binary file
|
gingo/audio.py
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Audio synthesis and playback for gingo objects.
|
|
3
|
+
|
|
4
|
+
Renders Note, Chord, Scale, Field, Tree, Sequence, and event objects
|
|
5
|
+
to audio using simple waveform synthesis. Zero external dependencies
|
|
6
|
+
for synthesis and WAV export; ``simpleaudio`` (optional) for playback.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from gingo import Note, Chord, Scale
|
|
11
|
+
from gingo.audio import play, to_wav, Waveform
|
|
12
|
+
|
|
13
|
+
play(Note("C"))
|
|
14
|
+
play(Chord("Am7"), waveform="square")
|
|
15
|
+
play(Scale("C", "major"), duration=0.3)
|
|
16
|
+
to_wav(Chord("G7"), "g7.wav")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import math
|
|
22
|
+
import struct
|
|
23
|
+
import wave
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, List, Union
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Waveform
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
class Waveform(Enum):
|
|
34
|
+
"""Available waveform shapes for synthesis."""
|
|
35
|
+
SINE = "sine"
|
|
36
|
+
SQUARE = "square"
|
|
37
|
+
SAWTOOTH = "sawtooth"
|
|
38
|
+
TRIANGLE = "triangle"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sine(phase: float) -> float:
|
|
42
|
+
return math.sin(2.0 * math.pi * phase)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _square(phase: float) -> float:
|
|
46
|
+
return 1.0 if (phase % 1.0) < 0.5 else -1.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _sawtooth(phase: float) -> float:
|
|
50
|
+
return 2.0 * (phase % 1.0) - 1.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _triangle(phase: float) -> float:
|
|
54
|
+
t = phase % 1.0
|
|
55
|
+
return 4.0 * t - 1.0 if t < 0.5 else 3.0 - 4.0 * t
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_WAVE_FUNCS = {
|
|
59
|
+
Waveform.SINE: _sine,
|
|
60
|
+
Waveform.SQUARE: _square,
|
|
61
|
+
Waveform.SAWTOOTH: _sawtooth,
|
|
62
|
+
Waveform.TRIANGLE: _triangle,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Envelope
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
class Envelope:
|
|
71
|
+
"""ADSR amplitude envelope.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
attack: Ramp-up time in seconds.
|
|
75
|
+
decay: Time to drop from peak to sustain level.
|
|
76
|
+
sustain: Sustain amplitude (0.0–1.0).
|
|
77
|
+
release: Ramp-down time in seconds at note end.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
attack: float = 0.01,
|
|
83
|
+
decay: float = 0.08,
|
|
84
|
+
sustain: float = 0.6,
|
|
85
|
+
release: float = 0.2,
|
|
86
|
+
) -> None:
|
|
87
|
+
self.attack = attack
|
|
88
|
+
self.decay = decay
|
|
89
|
+
self.sustain = sustain
|
|
90
|
+
self.release = release
|
|
91
|
+
|
|
92
|
+
def amplitude(self, t: float, note_duration: float) -> float:
|
|
93
|
+
"""Return amplitude at time *t* for a note lasting *note_duration* s."""
|
|
94
|
+
sustain_end = note_duration - self.release
|
|
95
|
+
# Note too short for full ADSR — simple fade in/out
|
|
96
|
+
if sustain_end < self.attack + self.decay:
|
|
97
|
+
half = note_duration / 2.0
|
|
98
|
+
if half <= 0:
|
|
99
|
+
return 0.0
|
|
100
|
+
if t < half:
|
|
101
|
+
return t / half
|
|
102
|
+
return max(0.0, 1.0 - (t - half) / half)
|
|
103
|
+
|
|
104
|
+
if t < self.attack:
|
|
105
|
+
return t / self.attack if self.attack > 0 else 1.0
|
|
106
|
+
if t < self.attack + self.decay:
|
|
107
|
+
frac = (t - self.attack) / self.decay if self.decay > 0 else 1.0
|
|
108
|
+
return 1.0 - (1.0 - self.sustain) * frac
|
|
109
|
+
if t < sustain_end:
|
|
110
|
+
return self.sustain
|
|
111
|
+
if t < note_duration:
|
|
112
|
+
frac = (t - sustain_end) / self.release if self.release > 0 else 1.0
|
|
113
|
+
return self.sustain * (1.0 - frac)
|
|
114
|
+
return 0.0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# Internal rendering
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def _render_tone(
|
|
122
|
+
frequency: float,
|
|
123
|
+
duration_sec: float,
|
|
124
|
+
sample_rate: int,
|
|
125
|
+
waveform: Waveform,
|
|
126
|
+
amplitude: float,
|
|
127
|
+
envelope: Envelope,
|
|
128
|
+
) -> list[float]:
|
|
129
|
+
"""Render a single-frequency tone."""
|
|
130
|
+
n = int(duration_sec * sample_rate)
|
|
131
|
+
fn = _WAVE_FUNCS[waveform]
|
|
132
|
+
inv_sr = 1.0 / sample_rate
|
|
133
|
+
samples = [0.0] * n
|
|
134
|
+
for i in range(n):
|
|
135
|
+
t = i * inv_sr
|
|
136
|
+
samples[i] = fn(frequency * t) * amplitude * envelope.amplitude(t, duration_sec)
|
|
137
|
+
return samples
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _render_chord_tones(
|
|
141
|
+
frequencies: list[float],
|
|
142
|
+
duration_sec: float,
|
|
143
|
+
sample_rate: int,
|
|
144
|
+
waveform: Waveform,
|
|
145
|
+
amplitude: float,
|
|
146
|
+
envelope: Envelope,
|
|
147
|
+
strum: float = 0.0,
|
|
148
|
+
) -> list[float]:
|
|
149
|
+
"""Render several frequencies mixed together (chord).
|
|
150
|
+
|
|
151
|
+
With *strum* > 0 each successive note starts a little later,
|
|
152
|
+
producing a strummed / arpeggiated feel instead of a flat block.
|
|
153
|
+
"""
|
|
154
|
+
if not frequencies:
|
|
155
|
+
return [0.0] * int(duration_sec * sample_rate)
|
|
156
|
+
fn = _WAVE_FUNCS[waveform]
|
|
157
|
+
inv_sr = 1.0 / sample_rate
|
|
158
|
+
per_note = amplitude / len(frequencies)
|
|
159
|
+
|
|
160
|
+
# Total buffer includes the extra strum spread
|
|
161
|
+
total_strum = strum * (len(frequencies) - 1)
|
|
162
|
+
total_n = int((duration_sec + total_strum) * sample_rate)
|
|
163
|
+
note_n = int(duration_sec * sample_rate)
|
|
164
|
+
mixed = [0.0] * total_n
|
|
165
|
+
|
|
166
|
+
for idx, freq in enumerate(frequencies):
|
|
167
|
+
offset = int(idx * strum * sample_rate)
|
|
168
|
+
for i in range(note_n):
|
|
169
|
+
pos = offset + i
|
|
170
|
+
if pos >= total_n:
|
|
171
|
+
break
|
|
172
|
+
t = i * inv_sr
|
|
173
|
+
mixed[pos] += fn(freq * t) * per_note * envelope.amplitude(t, duration_sec)
|
|
174
|
+
return mixed
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _render_silence(duration_sec: float, sample_rate: int) -> list[float]:
|
|
178
|
+
return [0.0] * int(duration_sec * sample_rate)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Chord voicing helpers
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def _chord_frequencies(chord: Any, octave: int, tuning: float) -> list[float]:
|
|
186
|
+
"""Compute frequencies for each chord tone with ascending voicing.
|
|
187
|
+
|
|
188
|
+
Notes above the root (by pitch-class) stay in *octave*; notes below
|
|
189
|
+
the root move up one octave so the chord is voiced in close position.
|
|
190
|
+
"""
|
|
191
|
+
root_semi = chord.root().semitone()
|
|
192
|
+
freqs: list[float] = []
|
|
193
|
+
for note in chord.notes():
|
|
194
|
+
note_oct = octave if note.semitone() >= root_semi else octave + 1
|
|
195
|
+
freqs.append(note.frequency(note_oct, tuning))
|
|
196
|
+
return freqs
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Object → segments conversion
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
# Segment = ("tone", [freq], dur_sec)
|
|
204
|
+
# | ("chord", [freqs], dur_sec)
|
|
205
|
+
# | ("silence", dur_sec)
|
|
206
|
+
|
|
207
|
+
def _obj_to_segments(
|
|
208
|
+
obj: Any,
|
|
209
|
+
octave: int,
|
|
210
|
+
duration: float,
|
|
211
|
+
tuning: float,
|
|
212
|
+
) -> list[tuple]:
|
|
213
|
+
"""Convert any gingo object to a list of audio segments."""
|
|
214
|
+
from gingo._gingo import (
|
|
215
|
+
Note, Chord, Scale, Field, Tree, Sequence,
|
|
216
|
+
NoteEvent, ChordEvent, Rest,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if isinstance(obj, Note):
|
|
220
|
+
return [("tone", [obj.frequency(octave, tuning)], duration)]
|
|
221
|
+
|
|
222
|
+
if isinstance(obj, NoteEvent):
|
|
223
|
+
return [("tone", [obj.frequency(tuning)], duration)]
|
|
224
|
+
|
|
225
|
+
if isinstance(obj, Chord):
|
|
226
|
+
return [("chord", _chord_frequencies(obj, octave, tuning), duration)]
|
|
227
|
+
|
|
228
|
+
if isinstance(obj, ChordEvent):
|
|
229
|
+
freqs = [ne.frequency(tuning) for ne in obj.note_events()]
|
|
230
|
+
return [("chord", freqs, duration)]
|
|
231
|
+
|
|
232
|
+
if isinstance(obj, Rest):
|
|
233
|
+
return [("silence", duration)]
|
|
234
|
+
|
|
235
|
+
if isinstance(obj, Scale):
|
|
236
|
+
segs: list[tuple] = []
|
|
237
|
+
for note in obj:
|
|
238
|
+
segs.append(("tone", [note.frequency(octave, tuning)], duration))
|
|
239
|
+
# Complete with the tonic one octave up
|
|
240
|
+
segs.append(("tone", [obj.tonic().frequency(octave + 1, tuning)], duration))
|
|
241
|
+
return segs
|
|
242
|
+
|
|
243
|
+
if isinstance(obj, Field):
|
|
244
|
+
return [
|
|
245
|
+
("chord", _chord_frequencies(ch, octave, tuning), duration)
|
|
246
|
+
for ch in obj
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
if isinstance(obj, Tree):
|
|
250
|
+
# Play the diatonic chords via the corresponding Field
|
|
251
|
+
field = Field(obj.tonic().name(), str(obj.type()).rsplit(".", 1)[-1])
|
|
252
|
+
return [
|
|
253
|
+
("chord", _chord_frequencies(ch, octave, tuning), duration)
|
|
254
|
+
for ch in field
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
if isinstance(obj, Sequence):
|
|
258
|
+
tempo = obj.tempo()
|
|
259
|
+
segs = []
|
|
260
|
+
for i in range(len(obj)):
|
|
261
|
+
event = obj[i]
|
|
262
|
+
if isinstance(event, NoteEvent):
|
|
263
|
+
segs.append((
|
|
264
|
+
"tone",
|
|
265
|
+
[event.frequency(tuning)],
|
|
266
|
+
tempo.seconds(event.duration()),
|
|
267
|
+
))
|
|
268
|
+
elif isinstance(event, ChordEvent):
|
|
269
|
+
segs.append((
|
|
270
|
+
"chord",
|
|
271
|
+
[ne.frequency(tuning) for ne in event.note_events()],
|
|
272
|
+
tempo.seconds(event.duration()),
|
|
273
|
+
))
|
|
274
|
+
elif isinstance(event, Rest):
|
|
275
|
+
segs.append(("silence", tempo.seconds(event.duration())))
|
|
276
|
+
return segs
|
|
277
|
+
|
|
278
|
+
if isinstance(obj, (list, tuple)):
|
|
279
|
+
segs = []
|
|
280
|
+
for item in obj:
|
|
281
|
+
if isinstance(item, str):
|
|
282
|
+
try:
|
|
283
|
+
ch = Chord(item)
|
|
284
|
+
segs.append(("chord", _chord_frequencies(ch, octave, tuning), duration))
|
|
285
|
+
except (ValueError, RuntimeError):
|
|
286
|
+
try:
|
|
287
|
+
n = Note(item)
|
|
288
|
+
segs.append(("tone", [n.frequency(octave, tuning)], duration))
|
|
289
|
+
except (ValueError, RuntimeError):
|
|
290
|
+
pass
|
|
291
|
+
else:
|
|
292
|
+
segs.extend(_obj_to_segments(item, octave, duration, tuning))
|
|
293
|
+
return segs
|
|
294
|
+
|
|
295
|
+
raise TypeError(f"Cannot play object of type {type(obj).__name__}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _render_segments(
|
|
299
|
+
segments: list[tuple],
|
|
300
|
+
sample_rate: int,
|
|
301
|
+
waveform: Waveform,
|
|
302
|
+
amplitude: float,
|
|
303
|
+
envelope: Envelope,
|
|
304
|
+
strum: float = 0.0,
|
|
305
|
+
gap: float = 0.0,
|
|
306
|
+
) -> list[float]:
|
|
307
|
+
"""Render all segments into a flat sample buffer.
|
|
308
|
+
|
|
309
|
+
*gap* inserts a short silence between consecutive notes/chords,
|
|
310
|
+
giving each one space to breathe (like lifting fingers between keys).
|
|
311
|
+
"""
|
|
312
|
+
samples: list[float] = []
|
|
313
|
+
gap_samples = int(gap * sample_rate) if gap > 0 else 0
|
|
314
|
+
last = len(segments) - 1
|
|
315
|
+
|
|
316
|
+
for i, seg in enumerate(segments):
|
|
317
|
+
kind = seg[0]
|
|
318
|
+
if kind == "tone":
|
|
319
|
+
samples.extend(_render_tone(
|
|
320
|
+
seg[1][0], seg[2], sample_rate, waveform, amplitude, envelope,
|
|
321
|
+
))
|
|
322
|
+
elif kind == "chord":
|
|
323
|
+
samples.extend(_render_chord_tones(
|
|
324
|
+
seg[1], seg[2], sample_rate, waveform, amplitude, envelope,
|
|
325
|
+
strum,
|
|
326
|
+
))
|
|
327
|
+
elif kind == "silence":
|
|
328
|
+
samples.extend(_render_silence(seg[1], sample_rate))
|
|
329
|
+
|
|
330
|
+
# Insert gap between segments (not after the last one)
|
|
331
|
+
if gap_samples and i < last and kind != "silence":
|
|
332
|
+
samples.extend([0.0] * gap_samples)
|
|
333
|
+
|
|
334
|
+
return samples
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
# WAV output
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
def _write_wav(path: str, samples: list[float], sample_rate: int) -> None:
|
|
342
|
+
"""Write float samples (–1…+1) to a 16-bit mono WAV file."""
|
|
343
|
+
n = len(samples)
|
|
344
|
+
int_samples = [0] * n
|
|
345
|
+
for i in range(n):
|
|
346
|
+
s = samples[i]
|
|
347
|
+
if s > 1.0:
|
|
348
|
+
s = 1.0
|
|
349
|
+
elif s < -1.0:
|
|
350
|
+
s = -1.0
|
|
351
|
+
int_samples[i] = int(s * 32767)
|
|
352
|
+
data = struct.pack(f"<{n}h", *int_samples)
|
|
353
|
+
with wave.open(path, "w") as wf:
|
|
354
|
+
wf.setnchannels(1)
|
|
355
|
+
wf.setsampwidth(2)
|
|
356
|
+
wf.setframerate(sample_rate)
|
|
357
|
+
wf.writeframes(data)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
# Playback
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
def _play_samples(samples: list[float], sample_rate: int) -> None:
|
|
365
|
+
"""Play rendered samples through the system audio output."""
|
|
366
|
+
n = len(samples)
|
|
367
|
+
int_samples = [0] * n
|
|
368
|
+
for i in range(n):
|
|
369
|
+
s = samples[i]
|
|
370
|
+
if s > 1.0:
|
|
371
|
+
s = 1.0
|
|
372
|
+
elif s < -1.0:
|
|
373
|
+
s = -1.0
|
|
374
|
+
int_samples[i] = int(s * 32767)
|
|
375
|
+
audio_data = struct.pack(f"<{n}h", *int_samples)
|
|
376
|
+
|
|
377
|
+
# Strategy 1: simpleaudio (cross-platform, preferred)
|
|
378
|
+
try:
|
|
379
|
+
import simpleaudio as sa
|
|
380
|
+
play_obj = sa.play_buffer(audio_data, 1, 2, sample_rate)
|
|
381
|
+
play_obj.wait_done()
|
|
382
|
+
return
|
|
383
|
+
except ImportError:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
# Strategy 2: temp WAV + system player
|
|
387
|
+
import os
|
|
388
|
+
import subprocess
|
|
389
|
+
import sys
|
|
390
|
+
import tempfile
|
|
391
|
+
|
|
392
|
+
fd, tmp_path = tempfile.mkstemp(suffix=".wav")
|
|
393
|
+
os.close(fd)
|
|
394
|
+
try:
|
|
395
|
+
_write_wav(tmp_path, samples, sample_rate)
|
|
396
|
+
if sys.platform == "darwin":
|
|
397
|
+
subprocess.run(["afplay", tmp_path], check=True, capture_output=True)
|
|
398
|
+
elif sys.platform.startswith("linux"):
|
|
399
|
+
for cmd in (
|
|
400
|
+
["aplay", tmp_path],
|
|
401
|
+
["paplay", tmp_path],
|
|
402
|
+
["ffplay", "-nodisp", "-autoexit", tmp_path],
|
|
403
|
+
):
|
|
404
|
+
try:
|
|
405
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
|
406
|
+
return
|
|
407
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
408
|
+
continue
|
|
409
|
+
raise RuntimeError(
|
|
410
|
+
"No audio player found. Install simpleaudio: "
|
|
411
|
+
"pip install gingo[audio]"
|
|
412
|
+
)
|
|
413
|
+
elif sys.platform == "win32":
|
|
414
|
+
import time
|
|
415
|
+
os.startfile(tmp_path) # type: ignore[attr-defined]
|
|
416
|
+
time.sleep(n / sample_rate + 0.5)
|
|
417
|
+
finally:
|
|
418
|
+
try:
|
|
419
|
+
os.unlink(tmp_path)
|
|
420
|
+
except OSError:
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# Helpers
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
def _parse_waveform(waveform: Any) -> Waveform:
|
|
429
|
+
if isinstance(waveform, Waveform):
|
|
430
|
+
return waveform
|
|
431
|
+
if isinstance(waveform, str):
|
|
432
|
+
try:
|
|
433
|
+
return Waveform(waveform.lower())
|
|
434
|
+
except ValueError:
|
|
435
|
+
valid = ", ".join(w.value for w in Waveform)
|
|
436
|
+
raise ValueError(f"Unknown waveform '{waveform}'. Valid: {valid}") from None
|
|
437
|
+
raise TypeError(f"waveform must be Waveform or str, got {type(waveform).__name__}")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
# Public API
|
|
442
|
+
# ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
def play(
|
|
445
|
+
obj: Any,
|
|
446
|
+
*,
|
|
447
|
+
octave: int = 4,
|
|
448
|
+
duration: float = 0.5,
|
|
449
|
+
waveform: Union[Waveform, str] = Waveform.SINE,
|
|
450
|
+
amplitude: float = 0.8,
|
|
451
|
+
envelope: Envelope | None = None,
|
|
452
|
+
strum: float = 0.03,
|
|
453
|
+
gap: float = 0.05,
|
|
454
|
+
tuning: float = 440.0,
|
|
455
|
+
sample_rate: int = 44100,
|
|
456
|
+
) -> None:
|
|
457
|
+
"""Play a gingo object through the system audio output.
|
|
458
|
+
|
|
459
|
+
Accepts Note, Chord, Scale, Field, Tree, Sequence, NoteEvent,
|
|
460
|
+
ChordEvent, Rest, or a list of chord/note names.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
obj: The gingo object to play.
|
|
464
|
+
octave: Base octave (default 4 = middle-C octave).
|
|
465
|
+
duration: Seconds per note/chord (default 0.5).
|
|
466
|
+
Ignored for Sequence (uses its own tempo and durations).
|
|
467
|
+
waveform: ``"sine"``, ``"square"``, ``"sawtooth"``, or ``"triangle"``.
|
|
468
|
+
amplitude: Volume 0.0–1.0 (default 0.8).
|
|
469
|
+
envelope: ADSR envelope. Default: attack=0.01, decay=0.08,
|
|
470
|
+
sustain=0.6, release=0.2.
|
|
471
|
+
strum: Delay in seconds between each note of a chord (default 0.03).
|
|
472
|
+
Creates a strummed / arpeggiated feel. Set to 0 for simultaneous.
|
|
473
|
+
gap: Silence in seconds between consecutive notes/chords (default 0.05).
|
|
474
|
+
Gives each sound space to breathe. Set to 0 for legato.
|
|
475
|
+
tuning: A4 reference in Hz (default 440).
|
|
476
|
+
sample_rate: Samples per second (default 44100).
|
|
477
|
+
"""
|
|
478
|
+
wf = _parse_waveform(waveform)
|
|
479
|
+
env = envelope or Envelope()
|
|
480
|
+
segments = _obj_to_segments(obj, octave=octave, duration=duration, tuning=tuning)
|
|
481
|
+
samples = _render_segments(segments, sample_rate, wf, amplitude, env, strum, gap)
|
|
482
|
+
_play_samples(samples, sample_rate)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def to_wav(
|
|
486
|
+
obj: Any,
|
|
487
|
+
path: Union[str, Path],
|
|
488
|
+
*,
|
|
489
|
+
octave: int = 4,
|
|
490
|
+
duration: float = 0.5,
|
|
491
|
+
waveform: Union[Waveform, str] = Waveform.SINE,
|
|
492
|
+
amplitude: float = 0.8,
|
|
493
|
+
envelope: Envelope | None = None,
|
|
494
|
+
strum: float = 0.03,
|
|
495
|
+
gap: float = 0.05,
|
|
496
|
+
tuning: float = 440.0,
|
|
497
|
+
sample_rate: int = 44100,
|
|
498
|
+
) -> None:
|
|
499
|
+
"""Render a gingo object to a WAV audio file.
|
|
500
|
+
|
|
501
|
+
Same object types and parameters as :func:`play`.
|
|
502
|
+
"""
|
|
503
|
+
wf = _parse_waveform(waveform)
|
|
504
|
+
env = envelope or Envelope()
|
|
505
|
+
segments = _obj_to_segments(obj, octave=octave, duration=duration, tuning=tuning)
|
|
506
|
+
samples = _render_segments(segments, sample_rate, wf, amplitude, env, strum, gap)
|
|
507
|
+
_write_wav(str(path), samples, sample_rate)
|
gingo/py.typed
ADDED
|
File without changes
|