Skip to content

Commit 1471a6e

Browse files
soundfont
1 parent 0be59d3 commit 1471a6e

File tree

10 files changed

+213
-14
lines changed

10 files changed

+213
-14
lines changed

assets/soundfont/32MbGMStereo.sf2

31 MB
Binary file not shown.

components/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ members = [
3939
"reverb",
4040
"rhymel",
4141
"sinbank",
42-
"sonic",
42+
"sonic", "soundfont",
4343
"stft",
4444
"strummer",
4545
"tape",

components/liner/src/lib.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,29 @@ impl Line {
192192
}
193193
json!(notes)
194194
}
195+
196+
fn duration(&self, sample_rate: u32, run_size: usize) -> f32 {
197+
let mut line = Line {
198+
deltamsgs: self.deltamsgs.clone(),
199+
ticks_per_quarter: self.ticks_per_quarter,
200+
..Default::default()
201+
};
202+
line.reset();
203+
let mut samples: u32 = 0;
204+
loop {
205+
samples += run_size as u32;
206+
if line.advance(
207+
samples,
208+
run_size,
209+
sample_rate,
210+
None,
211+
None,
212+
) {
213+
break;
214+
}
215+
}
216+
samples as f32 / sample_rate as f32
217+
}
195218
}
196219

197220
struct Queue {
@@ -261,6 +284,7 @@ component!(
261284
"advance": {"args": ["seconds"]},
262285
"skip_line": {"args": [{"desc": "number of lines", "default": 1}]},
263286
"multiply_deltas": {"args": ["multiplier"]},
287+
"duration": {},
264288
},
265289
);
266290

@@ -296,6 +320,9 @@ impl ComponentTrait for Component {
296320
while let Ok(mut line) = self.queue.recv.try_recv() {
297321
let line_index = (*line).index;
298322
(*line).index = (*self.lines[line_index]).index;
323+
while line_index >= self.lines.len() {
324+
self.lines.push(Box::new(Line::default()));
325+
}
299326
self.lines[line_index] = line;
300327
}
301328
self.samples += self.run_size as u32;
@@ -368,11 +395,11 @@ impl Component {
368395
let line_index: usize = body.arg(0)?;
369396
let ticks_per_quarter: u32 = body.arg(1)?;
370397
let deltamsgs = body.arg(2)?;
371-
while line_index >= self.lines.len() {
372-
self.lines.push(Box::new(Line::default()));
373-
}
374398
if let Ok(immediate) = body.kwarg("immediate") {
375399
if immediate {
400+
while line_index >= self.lines.len() {
401+
self.lines.push(Box::new(Line::default()));
402+
}
376403
self.lines[line_index] = Box::new(Line::new(
377404
&deltamsgs,
378405
ticks_per_quarter,
@@ -441,4 +468,18 @@ impl Component {
441468
}
442469
Ok(None)
443470
}
471+
472+
fn duration_cmd(&mut self, _body: serde_json::Value) -> CmdResult {
473+
if self.run_size == 0 {
474+
return Err(err!("run size is 0").into());
475+
}
476+
if self.sample_rate == 0 {
477+
return Err(err!("sample rate is 0").into());
478+
}
479+
let mut duration: f32 = 0.0;
480+
for line in &self.lines {
481+
duration = line.duration(self.sample_rate, self.run_size).max(duration);
482+
}
483+
Ok(Some(json!(duration)))
484+
}
444485
}

components/soundfont/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "soundfont"
3+
version = "1.0.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib"]
8+
9+
[dependencies]
10+
dlal-component-base = { path = "../base" }
11+
rustysynth = "1.3.1"

components/soundfont/src/lib.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use dlal_component_base::{component, serde_json, Body, CmdResult};
2+
3+
use rustysynth;
4+
5+
use std::fs::File;
6+
use std::sync::Arc;
7+
8+
component!(
9+
{"in": ["midi"], "out": ["audio"]},
10+
[
11+
"run_size",
12+
"sample_rate",
13+
"uni",
14+
"check_audio",
15+
{"name": "field_helpers", "fields": ["soundfont_path"], "kinds": ["r", "json"]},
16+
],
17+
{
18+
soundfont_path: String,
19+
synth: Option<rustysynth::Synthesizer>,
20+
buffer: Vec<f32>,
21+
},
22+
{
23+
"soundfont_load": {
24+
"args": ["path"],
25+
},
26+
},
27+
);
28+
29+
impl ComponentTrait for Component {
30+
fn run(&mut self) {
31+
let synth = match self.synth.as_mut() {
32+
Some(synth) => synth,
33+
_ => return,
34+
};
35+
let audio = match &self.output {
36+
Some(output) => output.audio(self.run_size).unwrap(),
37+
None => return,
38+
};
39+
synth.render(audio, &mut self.buffer);
40+
}
41+
42+
fn midi(&mut self, msg: &[u8]) {
43+
let synth = match self.synth.as_mut() {
44+
Some(synth) => synth,
45+
_ => return,
46+
};
47+
if msg.len() < 3 {
48+
return;
49+
}
50+
synth.process_midi_message(
51+
(msg[0] & 0x0f) as i32,
52+
(msg[0] & 0xf0) as i32,
53+
msg[1] as i32,
54+
msg[2] as i32,
55+
);
56+
}
57+
58+
fn join(&mut self, _body: serde_json::Value) -> CmdResult {
59+
self.buffer.resize(self.run_size, 0.0);
60+
if !self.soundfont_path.is_empty() {
61+
self.soundfont_load()
62+
} else {
63+
Ok(None)
64+
}
65+
}
66+
67+
fn from_json_cmd(&mut self, body: serde_json::Value) -> CmdResult {
68+
field_helper_from_json!(self, body);
69+
if !self.soundfont_path.is_empty() {
70+
self.soundfont_load()
71+
} else {
72+
Ok(None)
73+
}
74+
}
75+
}
76+
77+
impl Component {
78+
fn soundfont_load_cmd(&mut self, body: serde_json::Value) -> CmdResult {
79+
self.soundfont_path = body.arg(0)?;
80+
self.soundfont_load()?;
81+
Ok(None)
82+
}
83+
84+
fn soundfont_load(&mut self) -> CmdResult {
85+
let mut file = File::open(&self.soundfont_path)?;
86+
let soundfont = Arc::new(rustysynth::SoundFont::new(&mut file)?);
87+
let mut settings = rustysynth::SynthesizerSettings::new(self.sample_rate as i32);
88+
settings.block_size = self.run_size;
89+
self.synth = Some(rustysynth::Synthesizer::new(&soundfont, &settings)?);
90+
Ok(None)
91+
}
92+
}

skeleton/dlal/_component.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from . import _logging
2-
from ._utils import DIR
2+
from ._utils import DIR, JsonEncoder
33

44
import obvious
55

@@ -104,11 +104,15 @@ def command_detach(self, name, args=[], kwargs={}):
104104

105105
def command_immediate(self, name, args=[], kwargs={}):
106106
log('debug', f'{self.name} {name} {args} {kwargs}')
107-
result = self._lib.command(self._raw, json.dumps({
108-
'name': name,
109-
'args': args,
110-
'kwargs': kwargs,
111-
}).encode('utf-8'))
107+
body = json.dumps(
108+
{
109+
'name': name,
110+
'args': args,
111+
'kwargs': kwargs,
112+
},
113+
cls=JsonEncoder,
114+
)
115+
result = self._lib.command(self._raw, body.encode('utf-8'))
112116
if not result: return
113117
result = json.loads(result.decode('utf-8'))
114118
if type(result) == dict and 'error' in result:

skeleton/dlal/_utils.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
from collections.abc import Iterable
2+
import json
23
import os
4+
from pathlib import Path
35
import random
46
import re
57
import socket
68

7-
DIR = os.path.dirname(os.path.realpath(__file__))
8-
REPO_DIR = os.path.dirname(os.path.dirname(DIR))
9-
ASSETS_DIR = os.path.join(REPO_DIR, 'assets')
10-
DEPS_DIR = os.path.join(REPO_DIR, 'deps')
9+
DIR = Path(__file__).resolve().parent
10+
REPO_DIR = DIR.parent.parent
11+
ASSETS_DIR = REPO_DIR / 'assets'
12+
DEPS_DIR = REPO_DIR / 'deps'
1113

1214
class NoContext:
1315
def __enter__(*args, **kwargs): pass
1416
def __exit__(*args, **kwargs): pass
1517

18+
class JsonEncoder(json.JSONEncoder):
19+
def default(self, o):
20+
if isinstance(o, Path):
21+
return str(o)
22+
1623
def snake_to_upper_camel_case(s):
1724
return ''.join(i.capitalize() for i in s.split('_'))
1825

skeleton/dlal/soundfont.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from ._component import Component
2+
3+
from ._utils import ASSETS_DIR
4+
5+
import math
6+
import os
7+
8+
class Soundfont(Component):
9+
def __init__(self, **kwargs):
10+
Component.__init__(self, 'soundfont', **kwargs)
11+
from ._skeleton import Immediate
12+
with Immediate():
13+
self.soundfont_load(ASSETS_DIR / 'soundfont/32MbGMStereo.sf2')

systems/soundfont.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import dlal
2+
3+
import argparse
4+
5+
parser = argparse.ArgumentParser()
6+
parser.add_argument('--midi-path', '-m')
7+
args = parser.parse_args()
8+
9+
audio = dlal.Audio(driver=True)
10+
comm = dlal.Comm()
11+
liner = dlal.Liner()
12+
soundfont = dlal.Soundfont()
13+
tape = dlal.Tape()
14+
15+
for i in range(16):
16+
liner.connect(soundfont)
17+
dlal.connect(
18+
soundfont,
19+
[audio, tape],
20+
)
21+
22+
duration = None
23+
if args.midi_path:
24+
liner.load(args.midi_path, immediate=True)
25+
duration = liner.duration()
26+
27+
dlal.typical_setup(duration=duration)

web/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545

4646
.component-sinbank,
4747
.component-sonic,
48+
.component-soundfont,
4849
.component-train,
4950
.component-osc
5051
{
@@ -181,6 +182,9 @@
181182
await contextOptionPage('sonic', 'sonic.html');
182183
await contextOptionPage('webboard', 'webboard.html');
183184
break;
185+
case 'soundfont':
186+
await contextOptionPage('webboard', 'webboard.html');
187+
break;
184188
case 'tape':
185189
contextOption(dropdown, 'play', play, name);
186190
break;

0 commit comments

Comments
 (0)